├── .prettierignore
├── client
├── utils
│ ├── dataUrlToBlob.js
│ ├── ceil.js
│ ├── flatMap.js
│ ├── round.js
│ ├── fixPositionToGrid.js
│ ├── tests
│ │ ├── range.test.js
│ │ ├── ceil.test.js
│ │ ├── round.test.js
│ │ ├── constrainLocactionToShape.test.js
│ │ ├── throttle.test.js
│ │ ├── fixPositionToGrid.test.js
│ │ ├── relativePosition.test.js
│ │ ├── generateElementId.test.js
│ │ └── fixDeltaToGrid.test.js
│ ├── throttle.js
│ ├── range.js
│ ├── relativePosition.js
│ ├── index.js
│ ├── fixDeltaToGrid.js
│ ├── diagramBoundingBox.js
│ ├── generateElementId.js
│ ├── constrainLocationToShape.js
│ └── elementsWithLocations.js
├── components
│ ├── Checkbox
│ │ ├── styles.js
│ │ └── index.js
│ ├── Slider
│ │ ├── slider.css
│ │ ├── index.js
│ │ └── styles.js
│ ├── Select
│ │ ├── styles.js
│ │ ├── option.js
│ │ └── index.js
│ ├── Code
│ │ ├── index.js
│ │ └── styles.js
│ ├── Label
│ │ ├── styles.js
│ │ └── index.js
│ ├── RedButton
│ │ ├── index.js
│ │ └── styles.js
│ ├── ToggleButton
│ │ ├── styles.js
│ │ └── index.js
│ ├── Button
│ │ ├── index.js
│ │ └── styles.js
│ ├── Input
│ │ ├── styles.js
│ │ └── index.js
│ ├── Overlay
│ │ ├── Header
│ │ │ ├── index.js
│ │ │ └── styles.js
│ │ ├── styles.js
│ │ ├── index.js
│ │ └── test.js
│ ├── Collapsible
│ │ ├── styles.js
│ │ └── index.js
│ ├── ColorPicker
│ │ ├── styles.js
│ │ ├── index.js
│ │ └── test.js
│ ├── index.js
│ ├── Splittable
│ │ └── test.js
│ └── Text
│ │ └── index.js
├── actions
│ ├── info
│ │ ├── index.js
│ │ ├── creators
│ │ │ ├── zoomIn.js
│ │ │ ├── zoomOut.js
│ │ │ ├── toggleGrid.js
│ │ │ ├── toggleAnchors.js
│ │ │ ├── toggleHistory.js
│ │ │ ├── toggleHotkeys.js
│ │ │ ├── setZoom.js
│ │ │ ├── panDiagram.js
│ │ │ ├── toggleExportModal.js
│ │ │ ├── setDiagramTitle.js
│ │ │ ├── setGridSize.js
│ │ │ ├── togglePatternModal.js
│ │ │ ├── index.js
│ │ │ └── togglePatternModalInitialVis.js
│ │ ├── storage.js
│ │ ├── types.js
│ │ └── storage.test.js
│ ├── elements
│ │ ├── index.js
│ │ ├── creators
│ │ │ ├── clearElements.js
│ │ │ ├── clearSelection.js
│ │ │ ├── deleteSelection.js
│ │ │ ├── splitElement.js
│ │ │ ├── addAnchors.js
│ │ │ ├── loadDiagram.js
│ │ │ ├── loadPattern.js
│ │ │ ├── snapSelectedElements.js
│ │ │ ├── deleteElements.js
│ │ │ ├── placeElement.js
│ │ │ ├── setElementAttrs.js
│ │ │ ├── moveSelectedElements.js
│ │ │ ├── selectElements.js
│ │ │ ├── addPropagators.js
│ │ │ ├── setAnchorLocations.js
│ │ │ ├── alignSelectedAnchors.js
│ │ │ ├── mergeElements.js
│ │ │ ├── addElements.js
│ │ │ ├── saveDiagram.js
│ │ │ └── index.js
│ │ ├── types.js
│ │ └── tests
│ │ │ └── propagators.test.js
│ └── history
│ │ ├── index.js
│ │ ├── creators
│ │ ├── redo.js
│ │ ├── undo.js
│ │ ├── goto.js
│ │ ├── commit.js
│ │ ├── index.js
│ │ └── withCommit.js
│ │ ├── types.js
│ │ └── test.js
├── interface
│ ├── Toolbar
│ │ ├── utils
│ │ │ ├── index.js
│ │ │ ├── anchorsInSpec.js
│ │ │ └── tests
│ │ │ │ └── anchorsInSpec.test.js
│ │ ├── ItemPalette
│ │ │ ├── images
│ │ │ │ ├── em.png
│ │ │ │ ├── circle.png
│ │ │ │ ├── dashed.png
│ │ │ │ ├── gluon.png
│ │ │ │ ├── line.png
│ │ │ │ ├── text.png
│ │ │ │ └── index.js
│ │ │ ├── paletteItem.js
│ │ │ ├── styles.js
│ │ │ └── index.js
│ │ ├── SelectionSummary
│ │ │ ├── PropagatorSummary
│ │ │ │ ├── GluonSummary
│ │ │ │ │ ├── styles.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── styles.js
│ │ │ │ ├── FermionSummary
│ │ │ │ │ ├── styles.js
│ │ │ │ │ └── index.js
│ │ │ │ └── ElectroWeakSummary
│ │ │ │ │ └── index.js
│ │ │ ├── ShapeSummary
│ │ │ │ ├── styles.js
│ │ │ │ └── index.js
│ │ │ ├── ButtonRow
│ │ │ │ ├── styles.js
│ │ │ │ └── index.js
│ │ │ ├── TextSummary
│ │ │ │ ├── styles.js
│ │ │ │ └── index.js
│ │ │ ├── Container
│ │ │ │ ├── styles.js
│ │ │ │ └── index.js
│ │ │ ├── styles.js
│ │ │ ├── MultiRow
│ │ │ │ ├── styles.js
│ │ │ │ └── index.js
│ │ │ ├── Header
│ │ │ │ ├── styles.js
│ │ │ │ └── index.js
│ │ │ ├── Label
│ │ │ │ ├── styles.js
│ │ │ │ └── index.js
│ │ │ ├── Row
│ │ │ │ ├── index.js
│ │ │ │ └── styles.js
│ │ │ ├── SliderRow
│ │ │ │ ├── styles.js
│ │ │ │ └── index.js
│ │ │ ├── AnchorSummary
│ │ │ │ └── styles.js
│ │ │ └── index.js
│ │ ├── Footer
│ │ │ ├── styles.js
│ │ │ └── index.js
│ │ ├── styles.js
│ │ └── specs.js
│ ├── Sidebar
│ │ ├── GridSizeControl
│ │ │ ├── slider.css
│ │ │ ├── styles.js
│ │ │ └── index.js
│ │ ├── ZoomLevelControl
│ │ │ ├── slider.css
│ │ │ ├── styles.js
│ │ │ └── index.js
│ │ ├── HistorySummary
│ │ │ ├── styles.js
│ │ │ ├── Entry
│ │ │ │ ├── index.js
│ │ │ │ └── styles.js
│ │ │ ├── index.js
│ │ │ └── test.js
│ │ ├── HotkeySummary
│ │ │ ├── styles.js
│ │ │ ├── Hotkey
│ │ │ │ ├── styles.js
│ │ │ │ └── index.js
│ │ │ └── index.js
│ │ ├── TitleControl
│ │ │ ├── styles.js
│ │ │ └── index.js
│ │ ├── ButtonGrid
│ │ │ └── styles.js
│ │ ├── styles.js
│ │ └── index.js
│ ├── PatternModal
│ │ ├── patterns
│ │ │ ├── blank
│ │ │ │ ├── preview.js
│ │ │ │ └── index.js
│ │ │ ├── index.js
│ │ │ └── dy
│ │ │ │ └── index.js
│ │ ├── styles.js
│ │ ├── Choice
│ │ │ ├── styles.js
│ │ │ └── index.js
│ │ └── index.js
│ ├── Diagram
│ │ ├── Text
│ │ │ ├── styles.js
│ │ │ └── index.js
│ │ ├── Grid
│ │ │ ├── styles.js
│ │ │ └── index.js
│ │ ├── Propagator
│ │ │ ├── styles.js
│ │ │ ├── Fermion
│ │ │ │ ├── styles.js
│ │ │ │ ├── index.js
│ │ │ │ └── Arrow.js
│ │ │ ├── Dashed
│ │ │ │ └── index.js
│ │ │ ├── Gluon
│ │ │ │ ├── index.js
│ │ │ │ └── withoutEndcaps.js
│ │ │ ├── Label
│ │ │ │ ├── relLocForLabel.js
│ │ │ │ ├── locationForLabel.js
│ │ │ │ └── index.js
│ │ │ ├── Align.js
│ │ │ ├── index.js
│ │ │ └── ElectroWeak
│ │ │ │ └── index.js
│ │ ├── Anchor
│ │ │ ├── styles.js
│ │ │ └── index.js
│ │ ├── SelectionRectangle
│ │ │ ├── styles.js
│ │ │ └── index.js
│ │ ├── styles.js
│ │ ├── Shape
│ │ │ ├── Parton
│ │ │ │ └── index.js
│ │ │ └── index.js
│ │ └── Patterns.js
│ ├── App
│ │ ├── Alert
│ │ │ ├── index.js
│ │ │ └── styles.js
│ │ ├── styles.js
│ │ ├── index.js
│ │ └── reset.css
│ ├── index.js
│ ├── ExportModal
│ │ ├── styles.js
│ │ └── index.js
│ └── Title
│ │ ├── index.js
│ │ ├── styles.js
│ │ └── tests
│ │ └── title.test.js
├── sagas
│ ├── placeElements
│ │ ├── placeShapes.js
│ │ ├── placeAnchor.js
│ │ ├── placeText.js
│ │ ├── tests
│ │ │ ├── placeAnchor.test.js
│ │ │ ├── placeText.test.js
│ │ │ └── placeShapes.test.js
│ │ └── placePropagator.js
│ ├── index.js
│ ├── withCommit
│ │ ├── index.js
│ │ └── test.js
│ ├── loadPattern
│ │ ├── index.js
│ │ └── test.js
│ ├── splitElement
│ │ ├── test.js
│ │ ├── index.js
│ │ ├── anchor.js
│ │ ├── shape.js
│ │ └── propagator.js
│ └── loadDiagram
│ │ ├── test.js
│ │ └── index.js
├── store
│ ├── browser.js
│ ├── tests
│ │ ├── store.test.js
│ │ └── browser.test.js
│ ├── index.js
│ └── diagram
│ │ └── elements
│ │ ├── selection.js
│ │ └── propagators.js
├── colors.js
├── index.js
└── index.html
├── server
├── test
│ ├── feynman.sty
│ └── diagram.tex
├── templates
│ ├── error.tex.tmpl
│ ├── diagram.tex.tmpl
│ ├── string.tex.tmpl
│ └── base.tex.tmpl
├── go.mod
├── charts
│ ├── service.yaml
│ ├── cert.yaml
│ └── deployment.yaml
├── Dockerfile
└── main.go
├── config
├── __mocks__
│ ├── styleMock.js
│ └── fileMock.js
├── _redirects
├── setupJest.js
├── projectPaths.js
└── webpack.js
├── .babelrc
├── .travis.yml
├── .gitignore
└── README.md
/.prettierignore:
--------------------------------------------------------------------------------
1 | package.json
2 |
--------------------------------------------------------------------------------
/client/utils/dataUrlToBlob.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/components/Checkbox/styles.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/test/feynman.sty:
--------------------------------------------------------------------------------
1 | ../feynman.sty
--------------------------------------------------------------------------------
/server/templates/error.tex.tmpl:
--------------------------------------------------------------------------------
1 | error!
2 |
--------------------------------------------------------------------------------
/config/__mocks__/styleMock.js:
--------------------------------------------------------------------------------
1 | module.exports = {}
--------------------------------------------------------------------------------
/server/templates/diagram.tex.tmpl:
--------------------------------------------------------------------------------
1 | {% .String %}
2 |
--------------------------------------------------------------------------------
/config/__mocks__/fileMock.js:
--------------------------------------------------------------------------------
1 | module.exports = 'test-file-stub'
--------------------------------------------------------------------------------
/server/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/alecaivazis/feynman
2 |
3 | go 1.17
4 |
--------------------------------------------------------------------------------
/client/actions/info/index.js:
--------------------------------------------------------------------------------
1 | export * from './types'
2 | export * from './creators'
3 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/utils/index.js:
--------------------------------------------------------------------------------
1 | export anchorsInSpec from './anchorsInSpec'
2 |
--------------------------------------------------------------------------------
/client/actions/elements/index.js:
--------------------------------------------------------------------------------
1 | export * from './types'
2 | export * from './creators'
3 |
--------------------------------------------------------------------------------
/client/actions/history/index.js:
--------------------------------------------------------------------------------
1 | export * from './types'
2 | export * from './creators'
3 |
--------------------------------------------------------------------------------
/client/components/Slider/slider.css:
--------------------------------------------------------------------------------
1 | .rc-slider-track {
2 | background-color: #444 !important;
3 | }
--------------------------------------------------------------------------------
/client/interface/Sidebar/GridSizeControl/slider.css:
--------------------------------------------------------------------------------
1 | .rc-slider-track {
2 | background-color: #444 !important;
3 | }
--------------------------------------------------------------------------------
/client/interface/Sidebar/ZoomLevelControl/slider.css:
--------------------------------------------------------------------------------
1 | .rc-slider-track {
2 | background-color: #444 !important;
3 | }
--------------------------------------------------------------------------------
/client/utils/ceil.js:
--------------------------------------------------------------------------------
1 | const ceilTo = (n, to) => (to === 0 ? n : Math.ceil(n / to) * to)
2 |
3 | export default ceilTo
4 |
--------------------------------------------------------------------------------
/client/interface/PatternModal/patterns/blank/preview.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 |
4 | export default () =>
5 |
--------------------------------------------------------------------------------
/client/actions/history/creators/redo.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { REDO } from '../types'
3 |
4 | export default msg => ({
5 | type: REDO,
6 | })
7 |
--------------------------------------------------------------------------------
/client/actions/history/creators/undo.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { UNDO } from '../types'
3 |
4 | export default msg => ({
5 | type: UNDO,
6 | })
7 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/ItemPalette/images/em.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlecAivazis/feynman/HEAD/client/interface/Toolbar/ItemPalette/images/em.png
--------------------------------------------------------------------------------
/client/utils/flatMap.js:
--------------------------------------------------------------------------------
1 | // 💪
2 | export default function(arr, lambda) {
3 | return Array.prototype.concat.apply([], (arr || []).map(lambda))
4 | }
5 |
--------------------------------------------------------------------------------
/client/interface/PatternModal/patterns/index.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import blank from './blank'
3 | import dy from './dy'
4 |
5 | export default [blank, dy]
6 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/ItemPalette/images/circle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlecAivazis/feynman/HEAD/client/interface/Toolbar/ItemPalette/images/circle.png
--------------------------------------------------------------------------------
/client/interface/Toolbar/ItemPalette/images/dashed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlecAivazis/feynman/HEAD/client/interface/Toolbar/ItemPalette/images/dashed.png
--------------------------------------------------------------------------------
/client/interface/Toolbar/ItemPalette/images/gluon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlecAivazis/feynman/HEAD/client/interface/Toolbar/ItemPalette/images/gluon.png
--------------------------------------------------------------------------------
/client/interface/Toolbar/ItemPalette/images/line.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlecAivazis/feynman/HEAD/client/interface/Toolbar/ItemPalette/images/line.png
--------------------------------------------------------------------------------
/client/interface/Toolbar/ItemPalette/images/text.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlecAivazis/feynman/HEAD/client/interface/Toolbar/ItemPalette/images/text.png
--------------------------------------------------------------------------------
/client/interface/Toolbar/SelectionSummary/PropagatorSummary/GluonSummary/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | button: {
3 | width: '100%',
4 | },
5 | }
6 |
--------------------------------------------------------------------------------
/client/actions/info/creators/zoomIn.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { ZOOM_IN } from 'actions/info/types'
3 |
4 | export default () => ({
5 | type: ZOOM_IN,
6 | })
7 |
--------------------------------------------------------------------------------
/client/actions/info/creators/zoomOut.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { ZOOM_OUT } from 'actions/info/types'
3 |
4 | export default () => ({
5 | type: ZOOM_OUT,
6 | })
7 |
--------------------------------------------------------------------------------
/server/templates/string.tex.tmpl:
--------------------------------------------------------------------------------
1 | \fontsize{{% .FontSize %}}{{% .BaseLine %}}
2 | \textcolor{{% .Color %}}{{%if .MathMode %}${% end %}{% .String %}{%if .MathMode %}${% end %}}
3 |
--------------------------------------------------------------------------------
/client/actions/history/creators/goto.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { GOTO } from '../types'
3 |
4 | export default sha => ({
5 | type: GOTO,
6 | payload: sha,
7 | })
8 |
--------------------------------------------------------------------------------
/client/actions/info/creators/toggleGrid.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { TOGGLE_GRID } from 'actions/info/types'
3 |
4 | export default () => ({
5 | type: TOGGLE_GRID,
6 | })
7 |
--------------------------------------------------------------------------------
/config/_redirects:
--------------------------------------------------------------------------------
1 | # Redirects from what the browser requests to what we serve
2 |
3 | # redirect to the latex service
4 | /latex/* http://104.196.244.203/:splat 200
5 |
--------------------------------------------------------------------------------
/client/actions/history/creators/commit.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { COMMIT } from '../types'
3 |
4 | export default msg => ({
5 | type: COMMIT,
6 | payload: msg,
7 | })
8 |
--------------------------------------------------------------------------------
/client/interface/Diagram/Text/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | notSelected: {
3 | color: 'black',
4 | },
5 | selected: {
6 | color: 'blue',
7 | },
8 | }
9 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/SelectionSummary/ShapeSummary/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | colorPicker: {
3 | marginLeft: 'auto',
4 | marginRight: 10,
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015",
4 | "react",
5 | "stage-0"
6 | ],
7 | "plugins": [
8 | "transform-decorators-legacy"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/client/actions/info/creators/toggleAnchors.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { TOGGLE_ANCHORS } from 'actions/info/types'
3 |
4 | export default () => ({
5 | type: TOGGLE_ANCHORS,
6 | })
7 |
--------------------------------------------------------------------------------
/client/actions/info/creators/toggleHistory.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { TOGGLE_HISTORY } from 'actions/info/types'
3 |
4 | export default () => ({
5 | type: TOGGLE_HISTORY,
6 | })
7 |
--------------------------------------------------------------------------------
/client/actions/info/creators/toggleHotkeys.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { TOGGLE_HOTKEYS } from 'actions/info/types'
3 |
4 | export default () => ({
5 | type: TOGGLE_HOTKEYS,
6 | })
7 |
--------------------------------------------------------------------------------
/client/interface/Diagram/Grid/styles.js:
--------------------------------------------------------------------------------
1 | import { sighGrey } from 'colors'
2 |
3 | export default {
4 | container: {},
5 | gridLine: {
6 | stroke: sighGrey,
7 | },
8 | }
9 |
--------------------------------------------------------------------------------
/client/actions/elements/creators/clearElements.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { CLEAR_ELEMENTS } from 'actions/elements'
3 |
4 | export default () => ({
5 | type: CLEAR_ELEMENTS,
6 | })
7 |
--------------------------------------------------------------------------------
/client/actions/elements/creators/clearSelection.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { CLEAR_SELECTION } from 'actions/elements'
3 |
4 | export default () => ({
5 | type: CLEAR_SELECTION,
6 | })
7 |
--------------------------------------------------------------------------------
/client/components/Select/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | select: {
3 | backgroundColor: 'white',
4 | height: 24,
5 | fontSize: 12,
6 | },
7 | option: {},
8 | }
9 |
--------------------------------------------------------------------------------
/client/interface/Sidebar/HistorySummary/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | container: {
3 | marginTop: 10,
4 | maxHeight: 110,
5 | overflowY: 'auto',
6 | },
7 | }
8 |
--------------------------------------------------------------------------------
/client/interface/Sidebar/HotkeySummary/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | container: {
3 | flexDirection: 'column',
4 | },
5 | hotkey: {
6 | padding: 7,
7 | },
8 | }
9 |
--------------------------------------------------------------------------------
/client/actions/elements/creators/deleteSelection.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { DELETE_SELECTION } from 'actions/elements'
3 |
4 | export default () => ({
5 | type: DELETE_SELECTION,
6 | })
7 |
--------------------------------------------------------------------------------
/client/actions/info/creators/setZoom.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { SET_ZOOM } from 'actions/info/types'
3 |
4 | export default level => ({
5 | type: SET_ZOOM,
6 | payload: level,
7 | })
8 |
--------------------------------------------------------------------------------
/client/actions/history/creators/index.js:
--------------------------------------------------------------------------------
1 | export commit from './commit'
2 | export goto from './goto'
3 | export redo from './redo'
4 | export undo from './undo'
5 | export withCommit from './withCommit'
6 |
--------------------------------------------------------------------------------
/client/actions/info/creators/panDiagram.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { PAN_DIAGRAM } from 'actions/info/types'
3 |
4 | export default pan => ({
5 | type: PAN_DIAGRAM,
6 | payload: pan,
7 | })
8 |
--------------------------------------------------------------------------------
/client/actions/info/creators/toggleExportModal.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { TOGGLE_EXPORT_MODAL } from 'actions/info/types'
3 |
4 | export default () => ({
5 | type: TOGGLE_EXPORT_MODAL,
6 | })
7 |
--------------------------------------------------------------------------------
/config/setupJest.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import { configure } from 'enzyme'
3 | import Adapter from 'enzyme-adapter-react-16'
4 | import 'babel-polyfill'
5 |
6 | configure({ adapter: new Adapter() })
7 |
--------------------------------------------------------------------------------
/client/actions/elements/creators/splitElement.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { SPLIT_ELEMENT } from 'actions/elements'
3 |
4 | export default payload => ({
5 | type: SPLIT_ELEMENT,
6 | payload,
7 | })
8 |
--------------------------------------------------------------------------------
/client/actions/info/creators/setDiagramTitle.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { SET_TITLE } from 'actions/info/types'
3 |
4 | export default title => ({
5 | type: SET_TITLE,
6 | payload: title,
7 | })
8 |
--------------------------------------------------------------------------------
/client/actions/info/creators/setGridSize.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { SET_GRID_SIZE } from 'actions/info/types'
3 |
4 | export default size => ({
5 | type: SET_GRID_SIZE,
6 | payload: size,
7 | })
8 |
--------------------------------------------------------------------------------
/client/actions/info/creators/togglePatternModal.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { TOGGLE_PATTERN_MODAL } from 'actions/info/types'
3 |
4 | export default () => ({
5 | type: TOGGLE_PATTERN_MODAL,
6 | })
7 |
--------------------------------------------------------------------------------
/client/interface/Diagram/Propagator/styles.js:
--------------------------------------------------------------------------------
1 | import { brightBlue } from 'colors'
2 |
3 | export const selected = {
4 | stroke: brightBlue,
5 | }
6 |
7 | export default {
8 | selected,
9 | }
10 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/SelectionSummary/ButtonRow/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | container: {
3 | marginTop: 5,
4 | justifyContent: 'center',
5 | padding: '0 30px',
6 | },
7 | }
8 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/SelectionSummary/TextSummary/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | container: {},
3 | input: {
4 | height: 32,
5 | fontSize: 16,
6 | width: '100%',
7 | },
8 | }
9 |
--------------------------------------------------------------------------------
/client/utils/round.js:
--------------------------------------------------------------------------------
1 | // if we are rounding to zero, don't round at all. Otherwise, round to the nearest bucket
2 | const round = (n, to) => (to === 0 ? n : Math.round(n / to) * to)
3 |
4 | export default round
5 |
--------------------------------------------------------------------------------
/client/actions/elements/creators/addAnchors.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { ADD_ANCHORS } from 'actions/elements'
3 |
4 | export default (...anchors) => ({
5 | type: ADD_ANCHORS,
6 | payload: anchors,
7 | })
8 |
--------------------------------------------------------------------------------
/client/actions/elements/creators/loadDiagram.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { LOAD_DIAGRAM } from 'actions/elements'
3 |
4 | export default diagram => ({
5 | type: LOAD_DIAGRAM,
6 | payload: diagram,
7 | })
8 |
--------------------------------------------------------------------------------
/client/actions/elements/creators/loadPattern.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { LOAD_PATTERN } from 'actions/elements'
3 |
4 | export default pattern => ({
5 | type: LOAD_PATTERN,
6 | payload: pattern,
7 | })
8 |
--------------------------------------------------------------------------------
/client/actions/elements/creators/snapSelectedElements.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { SNAP_SELECTED_ELEMENTS } from 'actions/elements'
3 |
4 | export default pattern => ({
5 | type: SNAP_SELECTED_ELEMENTS,
6 | })
7 |
--------------------------------------------------------------------------------
/client/actions/elements/creators/deleteElements.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { DELETE_ELEMENTS } from '../types'
3 |
4 | export default (...targets) => ({
5 | type: DELETE_ELEMENTS,
6 | payload: targets,
7 | })
8 |
--------------------------------------------------------------------------------
/client/actions/elements/creators/placeElement.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { PLACE_ELEMENTS } from 'actions/elements'
3 |
4 | export default element => ({
5 | type: PLACE_ELEMENTS,
6 | payload: element,
7 | })
8 |
--------------------------------------------------------------------------------
/client/actions/elements/creators/setElementAttrs.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { SET_ELEMENT_ATTRS } from '../types'
3 |
4 | export default (...updates) => ({
5 | type: SET_ELEMENT_ATTRS,
6 | payload: updates,
7 | })
8 |
--------------------------------------------------------------------------------
/client/actions/elements/creators/moveSelectedElements.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { MOVE_SELECTED_ELEMENTS } from '../types'
3 |
4 | export default move => ({
5 | type: MOVE_SELECTED_ELEMENTS,
6 | payload: move,
7 | })
8 |
--------------------------------------------------------------------------------
/client/actions/elements/creators/selectElements.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { SELECT_ELEMENTS } from 'actions/elements'
3 |
4 | export default (...selection) => ({
5 | type: SELECT_ELEMENTS,
6 | payload: selection,
7 | })
8 |
--------------------------------------------------------------------------------
/client/actions/elements/creators/addPropagators.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { ADD_PROPAGATORS } from 'actions/elements'
3 |
4 | export default (...propagators) => ({
5 | type: ADD_PROPAGATORS,
6 | payload: propagators,
7 | })
8 |
--------------------------------------------------------------------------------
/client/actions/elements/creators/setAnchorLocations.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { SET_ANCHOR_LOCATIONS } from 'actions/elements'
3 |
4 | export default (...anchors) => ({
5 | type: SET_ANCHOR_LOCATIONS,
6 | payload: anchors,
7 | })
8 |
--------------------------------------------------------------------------------
/client/interface/Diagram/Propagator/Fermion/styles.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { selected } from '../styles'
3 |
4 | export default {
5 | container: {},
6 | selected: {
7 | fill: selected.stroke,
8 | },
9 | }
10 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/SelectionSummary/Container/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | container: {
3 | display: 'flex',
4 | flexDirection: 'column',
5 | alignItems: 'center',
6 | flexGrow: 1,
7 | },
8 | }
9 |
--------------------------------------------------------------------------------
/client/actions/elements/creators/alignSelectedAnchors.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { ALIGN_SELECTED_ANCHORS } from 'actions/elements'
3 |
4 | export default direction => ({
5 | type: ALIGN_SELECTED_ANCHORS,
6 | payload: direction,
7 | })
8 |
--------------------------------------------------------------------------------
/client/actions/history/types.js:
--------------------------------------------------------------------------------
1 | export const COMMIT = '@history/COMMIT'
2 | export const UNDO = '@history/UNDO'
3 | export const REDO = '@history/REDO'
4 | export const GOTO = '@history/GOTO'
5 | export const WITH_COMMIT = '@history/WITH_COMMIT'
6 |
--------------------------------------------------------------------------------
/server/charts/service.yaml:
--------------------------------------------------------------------------------
1 | kind: Service
2 | apiVersion: v1
3 | metadata:
4 | name: latex
5 | spec:
6 | type: LoadBalancer
7 | selector:
8 | app: latex
9 | ports:
10 | - protocol: TCP
11 | port: 80
12 | targetPort: 8081
13 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/ItemPalette/images/index.js:
--------------------------------------------------------------------------------
1 | export circle from './circle.png'
2 | export dashed from './dashed.png'
3 | export em from './em.png'
4 | export gluon from './gluon.png'
5 | export line from './line.png'
6 | export text from './text.png'
7 |
--------------------------------------------------------------------------------
/client/actions/history/creators/withCommit.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { WITH_COMMIT } from '../types'
3 |
4 | export default (action, message) => ({
5 | type: WITH_COMMIT,
6 | payload: {
7 | action,
8 | message,
9 | },
10 | })
11 |
--------------------------------------------------------------------------------
/client/components/Select/option.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import styles from './styles'
5 |
6 | const Option = ({ ...unusedProps }) =>
7 |
8 | export default Option
9 |
--------------------------------------------------------------------------------
/client/interface/Diagram/Anchor/styles.js:
--------------------------------------------------------------------------------
1 | import { brightBlue, idkGrey } from 'colors'
2 |
3 | export default {
4 | selected: {
5 | fill: brightBlue,
6 | },
7 | notSelected: {},
8 | fixed: {
9 | fill: idkGrey,
10 | },
11 | }
12 |
--------------------------------------------------------------------------------
/client/components/Code/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import styles from './styles'
5 |
6 | const Code = ({ style, ...unused }) =>
7 |
8 | export default Code
9 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/SelectionSummary/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | container: {
3 | display: 'flex',
4 | flexDirection: 'column',
5 | overflowX: 'hidden',
6 | },
7 | deleteButton: {
8 | width: '100%',
9 | },
10 | }
11 |
--------------------------------------------------------------------------------
/client/interface/App/Alert/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import styles from './styles'
5 |
6 | const Alert = ({ style, ...unused }) =>
7 |
8 | export default Alert
9 |
--------------------------------------------------------------------------------
/client/components/Checkbox/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 |
4 | export default ({ checked, onClick, readOnly, ...unused }) => (
5 |
6 | )
7 |
--------------------------------------------------------------------------------
/client/components/Label/styles.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { darkGrey } from 'colors'
3 |
4 | export default {
5 | label: {
6 | textTransform: 'capitalize',
7 | color: darkGrey,
8 | fontWeight: 'bold',
9 | fontSize: 12,
10 | },
11 | }
12 |
--------------------------------------------------------------------------------
/client/interface/index.js:
--------------------------------------------------------------------------------
1 | export Diagram from './Diagram'
2 | export Sidebar from './Sidebar'
3 | export Title from './Title'
4 | export Toolbar from './Toolbar'
5 | export PatternModal from './PatternModal'
6 | export ExportModal from './ExportModal'
7 |
8 | export default from './App'
9 |
--------------------------------------------------------------------------------
/client/actions/elements/creators/mergeElements.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { MERGE_ELEMENTS } from 'actions/elements'
3 |
4 | export default (source, select = false) => ({
5 | type: MERGE_ELEMENTS,
6 | payload: {
7 | source,
8 | select,
9 | },
10 | })
11 |
--------------------------------------------------------------------------------
/client/components/Code/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | container: {
3 | backgroundColor: 'white',
4 | margin: '10px 0',
5 | borderRadius: 6,
6 | fontFamily: 'monaco, Consolas, Lucida Console, monospace',
7 | padding: 10,
8 | },
9 | }
10 |
--------------------------------------------------------------------------------
/client/interface/Diagram/SelectionRectangle/styles.js:
--------------------------------------------------------------------------------
1 | import { brightBlue } from 'colors'
2 |
3 | export default {
4 | rectangle: {
5 | fill: 'transparent',
6 | stroke: brightBlue,
7 | strokeWidth: 1,
8 | strokeDasharray: 10,
9 | },
10 | }
11 |
--------------------------------------------------------------------------------
/client/components/Label/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import styles from './styles'
5 |
6 | const Label = ({ style, children, ...unusedProps }) => {children}
7 |
8 | export default Label
9 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/SelectionSummary/MultiRow/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | container: {
3 | display: 'flex',
4 | flexDirection: 'column',
5 | flexGrow: 1,
6 | width: '100%',
7 | padding: 0,
8 | marginBottom: 10,
9 | },
10 | }
11 |
--------------------------------------------------------------------------------
/client/interface/Diagram/Propagator/Dashed/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 |
4 | // local imports
5 | import Fermion from '../Fermion'
6 |
7 | const Dashed = ({ ...unusedProps }) =>
8 |
9 | export default Dashed
10 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/SelectionSummary/Header/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | container: {
3 | width: '100%',
4 | padding: 15,
5 | color: 'white',
6 | display: 'flex',
7 | justifyContent: 'center',
8 | marginBottom: 5,
9 | },
10 | }
11 |
--------------------------------------------------------------------------------
/client/interface/PatternModal/patterns/blank/index.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import preview from './preview'
3 |
4 | export default {
5 | name: 'Blank',
6 | preview,
7 | elements: {
8 | type: 'pattern',
9 | anchors: [],
10 | propagators: [],
11 | },
12 | }
13 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/SelectionSummary/Label/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | container: {
3 | display: 'flex',
4 | padding: '0px 15px',
5 | fontSize: 14,
6 | color: 'white',
7 | textTransform: 'capitalize',
8 | marginBottom: 5,
9 | },
10 | }
11 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/SelectionSummary/Row/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import styles from './styles'
5 |
6 | const Row = ({ style, ...unusedProps }) =>
7 |
8 | export default Row
9 |
--------------------------------------------------------------------------------
/client/utils/fixPositionToGrid.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import round from './round'
3 |
4 | const fixPositionToGrid = ({ x, y }, gridSize) => ({
5 | x: gridSize === 0 ? x : round(x, gridSize),
6 | y: gridSize === 0 ? y : round(y, gridSize),
7 | })
8 |
9 | export default fixPositionToGrid
10 |
--------------------------------------------------------------------------------
/client/utils/tests/range.test.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import range from '../range'
3 |
4 | describe('Utils', () => {
5 | describe('Range util', () => {
6 | test('returns correct array', () => {
7 | expect(range(5)).toEqual([0, 1, 2, 3, 4])
8 | })
9 | })
10 | })
11 |
--------------------------------------------------------------------------------
/client/actions/info/storage.js:
--------------------------------------------------------------------------------
1 | export default class Storage {
2 | value = {}
3 |
4 | getItem(field) {
5 | return this.value[field]
6 | }
7 |
8 | setItem(field, val) {
9 | this.value[field] = val
10 | }
11 |
12 | clear() {
13 | this.value = {}
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/client/interface/App/Alert/styles.js:
--------------------------------------------------------------------------------
1 | import colors from 'colors'
2 |
3 | export default {
4 | container: {
5 | borderRadius: '50%',
6 | display: 'flex',
7 | justifyContent: 'center',
8 | alignItems: 'center',
9 | width: 100,
10 | height: 100,
11 | },
12 | }
13 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | # system setup
2 | sudo: required
3 | dist: trusty
4 |
5 | # language setup
6 | language: node_js
7 | node_js:
8 | - "6"
9 |
10 | # setup yarn
11 | cache:
12 | yarn: true
13 | directories:
14 | - node_modules
15 |
16 | # command to run tests
17 | script:
18 | - npm run test:coverage:report
19 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/SelectionSummary/Container/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import styles from './styles'
5 |
6 | const SelectionContainer = ({ style, ...unused }) =>
7 |
8 | export default SelectionContainer
9 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/SelectionSummary/Row/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | container: {
3 | display: 'flex',
4 | flexDirection: 'row',
5 | width: '100%',
6 | alignItems: 'center',
7 | padding: '0 15px',
8 | minHeight: 30,
9 | marginBottom: 10,
10 | },
11 | }
12 |
--------------------------------------------------------------------------------
/client/sagas/placeElements/placeShapes.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import { put } from 'redux-saga/effects'
3 | // local imports
4 | import { addElements } from 'actions/elements'
5 |
6 | export default function* createShapes(...shapes) {
7 | yield put(addElements(...shapes.map(shape => ({ type: 'shapes', ...shape }))))
8 | }
9 |
--------------------------------------------------------------------------------
/client/components/Select/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import styles from './styles'
5 |
6 | const Select = ({ style, ...unusedProps }) =>
7 |
8 | export default Select
9 | export Option from './option'
10 |
--------------------------------------------------------------------------------
/client/components/RedButton/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import Button from '../Button'
5 | import styles from './styles'
6 |
7 | const RedButton = ({ style, ...unusedProps }) =>
8 |
9 | export default RedButton
10 |
--------------------------------------------------------------------------------
/client/components/ToggleButton/styles.js:
--------------------------------------------------------------------------------
1 | import { lightBlue, red } from 'colors'
2 |
3 | const button = {
4 | outline: 'none',
5 | }
6 |
7 | export default {
8 | active: {
9 | ...button,
10 | color: lightBlue,
11 | },
12 | inactive: {
13 | ...button,
14 | color: red,
15 | },
16 | }
17 |
--------------------------------------------------------------------------------
/server/templates/base.tex.tmpl:
--------------------------------------------------------------------------------
1 | % this file describes the basic latex file that is used by the interpretter at alec.aivazis
2 | % author: alec aivazis
3 |
4 | \documentclass[{% .ExtraConfig %} border=1pt]{standalone}
5 |
6 | \usepackage{feynman}
7 |
8 | \begin{document}
9 | {% .Content %}
10 | \end{document}
11 |
12 |
13 | % end of file
14 |
--------------------------------------------------------------------------------
/client/components/RedButton/styles.js:
--------------------------------------------------------------------------------
1 | import { otherRed } from 'colors'
2 |
3 | export default {
4 | container: {
5 | background: '#c1403e linear-gradient(180deg, #cd6664, #c1403e) repeat-x',
6 | color: 'white',
7 | border: `1px solid ${otherRed}`,
8 | textShadow: '1px 1px 1px rgba(0,0,0,0.1)',
9 | },
10 | }
11 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/SelectionSummary/ButtonRow/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import Row from '../Row'
5 | import styles from './styles'
6 |
7 | const ButtonRow = ({ style, ...unusedProps }) =>
8 |
9 | export default ButtonRow
10 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/SelectionSummary/MultiRow/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import Row from '../Row'
5 | import styles from './styles'
6 |
7 | const MultiRow = ({ style, ...unusedProps }) =>
8 |
9 | export default MultiRow
10 |
--------------------------------------------------------------------------------
/client/sagas/placeElements/placeAnchor.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import { put } from 'redux-saga/effects'
3 | // local imports
4 | import { addAnchors } from 'actions/elements'
5 |
6 | export default function* createAnchor(...anchors) {
7 | // next we have to create a propagator between the two anchors
8 | yield put(addAnchors(...anchors))
9 | }
10 |
--------------------------------------------------------------------------------
/client/components/Button/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import styles from './styles'
5 |
6 | const Button = ({ style, children, ...unusedProps }) => (
7 |
8 | {children}
9 |
10 | )
11 |
12 | export default Button
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /db/
2 | /node_modules/
3 | /build/
4 | .DS_Store
5 | *.log
6 | src/server/static/client.js
7 | .vscode/
8 | server/server
9 | *~
10 | .tmp/
11 | coverage/
12 | server/test/diagram.aux
13 | server/test/diagram.log
14 | server/test/diagram.pdf
15 | server/test/diagram.fls
16 | server/test/diagram.synctex*
17 | server/test/diagram.fdb_latexmk
18 | server/feynman
--------------------------------------------------------------------------------
/client/interface/ExportModal/styles.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { red } from 'colors'
3 |
4 | export default {
5 | container: {},
6 | buttonRow: {
7 | display: 'flex',
8 | flexDirection: 'row',
9 | justifyContent: 'center',
10 | },
11 | closeButton: {
12 | marginRight: 8,
13 | color: red,
14 | },
15 | }
16 |
--------------------------------------------------------------------------------
/client/interface/Sidebar/TitleControl/styles.js:
--------------------------------------------------------------------------------
1 | import { elementSpacing } from '../styles'
2 |
3 | export default {
4 | container: {
5 | display: 'flex',
6 | flexDirection: 'column',
7 | },
8 | label: {
9 | marginBottom: elementSpacing,
10 | },
11 | input: {
12 | marginBottom: elementSpacing,
13 | },
14 | }
15 |
--------------------------------------------------------------------------------
/client/components/Input/styles.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { grey, lightGrey } from 'colors'
3 |
4 | export default {
5 | input: {
6 | height: 27,
7 | backgroundColor: lightGrey,
8 | border: `1px solid ${grey}`,
9 | boxShadow: 'none',
10 | borderRadius: 3,
11 | textIndent: 10,
12 | fontSize: 12,
13 | },
14 | }
15 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/SelectionSummary/SliderRow/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | value: {
3 | color: 'white',
4 | display: 'flex',
5 | width: '100%',
6 | justifyContent: 'center',
7 | fontSize: 14,
8 | },
9 | sliderRow: {
10 | padding: '0 30px',
11 | minHeight: 'none',
12 | marginBottom: 0,
13 | },
14 | }
15 |
--------------------------------------------------------------------------------
/client/sagas/placeElements/placeText.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import { put } from 'redux-saga/effects'
3 | // local imports
4 | import { addElements } from 'actions/elements'
5 |
6 | export default function* createText(...texts) {
7 | // next we have to create a propagator between the two texts
8 | yield put(addElements(...texts.map(text => ({ type: 'text', ...text }))))
9 | }
10 |
--------------------------------------------------------------------------------
/client/interface/Sidebar/ButtonGrid/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | container: {
3 | display: 'flex',
4 | flexDirection: 'column',
5 | },
6 | centerButton: {
7 | marginLeft: 10,
8 | },
9 | buttonRow: {
10 | display: 'flex',
11 | flexDirection: 'row',
12 | marginBottom: 10,
13 | justifyContent: 'space-between',
14 | },
15 | }
16 |
--------------------------------------------------------------------------------
/client/interface/Sidebar/HotkeySummary/Hotkey/styles.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { mediumGrey } from 'colors'
3 |
4 | export default {
5 | container: {},
6 | action: {
7 | fontSize: 12,
8 | color: mediumGrey,
9 | fontWeight: 'bold',
10 | marginRight: 5,
11 | },
12 | trigger: {
13 | fontSize: 12,
14 | color: mediumGrey,
15 | },
16 | }
17 |
--------------------------------------------------------------------------------
/client/interface/Title/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | import { connect } from 'react-redux'
4 | // local imports
5 | import styles from './styles'
6 |
7 | const DiagramTitle = ({ style, info }) => {info.title}
8 |
9 | const selector = ({ diagram: { info } }) => ({ info })
10 | export default connect(selector)(DiagramTitle)
11 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/SelectionSummary/Label/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import styles from './styles'
5 |
6 | const SelectionSummaryLabel = ({ style, children, ...unusedProps }) => (
7 |
8 | {children}
9 |
10 | )
11 |
12 | export default SelectionSummaryLabel
13 |
--------------------------------------------------------------------------------
/client/sagas/index.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import placeElements from './placeElements'
3 | import loadPattern from './loadPattern'
4 | import withCommit from './withCommit'
5 | import splitElement from './splitElement'
6 | import loadDiagram from './loadDiagram'
7 |
8 | export default function* rootSaga() {
9 | yield [placeElements(), loadPattern(), withCommit(), splitElement(), loadDiagram()]
10 | }
11 |
--------------------------------------------------------------------------------
/client/utils/tests/ceil.test.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import ceil from '../ceil'
3 |
4 | describe('Utils', () => {
5 | describe('Ceil to util', () => {
6 | test('returns the correct value', () => {
7 | expect(ceil(75, 50)).toEqual(100)
8 | })
9 |
10 | test('handles zero', () => {
11 | expect(ceil(75, 0)).toEqual(75)
12 | })
13 | })
14 | })
15 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/SelectionSummary/Header/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import styles from './styles'
5 |
6 | const SelectionSummaryHeader = ({ style, children, ...unusedProps }) => (
7 |
8 | {children}
9 |
10 | )
11 |
12 | export default SelectionSummaryHeader
13 |
--------------------------------------------------------------------------------
/client/store/browser.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import { createResponsiveStateReducer } from 'redux-responsive'
3 |
4 | // create a reducer that is aware of the height and width of the browser
5 | const reducer = createResponsiveStateReducer(null, {
6 | extraFields: () => ({
7 | width: window.innerWidth,
8 | height: window.innerHeight,
9 | }),
10 | })
11 |
12 | export default reducer
13 |
--------------------------------------------------------------------------------
/server/charts/cert.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: certmanager.k8s.io/v1alpha1
2 | kind: Certificate
3 | metadata:
4 | name: latex-certificate
5 | spec:
6 | secretName: latex-certificate
7 | issuerRef:
8 | name: letsencrypt-staging
9 | kind: ClusterIssuer
10 | dnsNames:
11 | - *.aivazis.com
12 | acme:
13 | config:
14 | - http01:
15 | ingressClass: nginx
16 | domains:
17 | - *.aivazis.com
18 |
--------------------------------------------------------------------------------
/client/utils/throttle.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import _ from 'lodash'
3 |
4 | export default throttle => (target, nam, descriptor) => {
5 | // created a throttled version of the
6 | const decorated = _.throttle(descriptor.value, throttle)
7 |
8 | // overwrite the function called by the descriptor
9 | descriptor.value = decorated
10 | // we are done modifying the descriptor
11 | return descriptor
12 | }
13 |
--------------------------------------------------------------------------------
/client/actions/elements/creators/addElements.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { ADD_ELEMENTS } from 'actions/elements'
3 |
4 | export default (...configs) => {
5 | // if there are any configs without a type
6 | if (configs.filter(({ type }) => !type).length > 0) {
7 | throw new Error('Cannot add element without type')
8 | }
9 |
10 | return {
11 | type: ADD_ELEMENTS,
12 | payload: configs,
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/Footer/styles.js:
--------------------------------------------------------------------------------
1 | import { lightestBlue } from 'colors'
2 |
3 | export default {
4 | container: {
5 | color: 'white',
6 | fontSize: 14,
7 | },
8 | line: {
9 | textAlign: 'center',
10 | marginBottom: 15,
11 | lineHeight: 1.4,
12 | },
13 | link: {
14 | color: lightestBlue,
15 | marginLeft: 5,
16 | textDecoration: 'none',
17 | },
18 | }
19 |
--------------------------------------------------------------------------------
/client/interface/Title/styles.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { sidebarWidth } from '../Sidebar/styles'
3 | import { toolbarWidth } from '../Toolbar/styles'
4 |
5 | export default {
6 | container: {
7 | position: 'absolute',
8 | top: 20,
9 | left: sidebarWidth,
10 | right: toolbarWidth,
11 | textAlign: 'center',
12 | fontSize: 25,
13 | fontWeight: 400,
14 | zIndex: 5,
15 | },
16 | }
17 |
--------------------------------------------------------------------------------
/client/interface/Diagram/styles.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { grey } from 'colors'
3 |
4 | const container = {
5 | display: 'flex',
6 | flexGrow: 1,
7 | cursor: 'default',
8 | zIndex: 1,
9 | }
10 |
11 | export default {
12 | containerWithGrid: {
13 | ...container,
14 | backgroundColor: 'rgb(245, 245, 245)',
15 | },
16 | containerWithoutGrid: {
17 | ...container,
18 | },
19 | canvas: {},
20 | }
21 |
--------------------------------------------------------------------------------
/client/interface/Sidebar/HotkeySummary/Hotkey/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | // local imports
3 | import { Collapsible } from 'components'
4 | import styles from './styles'
5 |
6 | const Hotkey = ({ style, action, trigger }) => (
7 |
8 | {action}:
9 | {trigger}
10 |
11 | )
12 |
13 | export default Hotkey
14 |
--------------------------------------------------------------------------------
/server/charts/deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: latex
5 | labels:
6 | app: latex
7 | spec:
8 | replicas: 1
9 | selector:
10 | matchLabels:
11 | app: latex
12 | template:
13 | metadata:
14 | labels:
15 | app: latex
16 | spec:
17 | containers:
18 | - name: latex
19 | image: gcr.io/lucky-essence-160522/latex:3
20 | ports:
21 | - containerPort: 8081
22 |
--------------------------------------------------------------------------------
/client/interface/Diagram/Shape/Parton/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import { brightBlue } from 'colors'
5 |
6 | const Parton = ({ x, y, r, selected, color }) => (
7 |
15 | )
16 |
17 | export default Parton
18 |
--------------------------------------------------------------------------------
/client/interface/Diagram/Propagator/Gluon/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import With from './withEndCaps'
5 | import Without from './withoutEndcaps'
6 |
7 | // which gluon we show depends on wether there are end caps or not
8 | const Gluon = ({ endcaps, ...unusedProps }) => (endcaps ? : )
9 |
10 | Gluon.defaultProps = {
11 | endcaps: true,
12 | direction: 1,
13 | }
14 |
15 | export default Gluon
16 |
--------------------------------------------------------------------------------
/client/interface/Sidebar/HistorySummary/Entry/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import styles from './styles'
5 |
6 | const HistorySummaryEntry = ({ children, index, length, active, ...unused }) => (
7 |
8 |
{length - index} :
9 |
{` ${children.message}`}
10 |
11 | )
12 |
13 | export default HistorySummaryEntry
14 |
--------------------------------------------------------------------------------
/client/utils/range.js:
--------------------------------------------------------------------------------
1 | // an object to hold memoized values
2 | const memo = {}
3 |
4 | const range = n => {
5 | // if there is an entry in the memoization store
6 | if (memo[n]) {
7 | // return it
8 | return memo[n]
9 | }
10 |
11 | // the usual recursive implementation
12 | const value = n > 1 ? range(n - 1).concat([n - 1]) : [0]
13 | // save the memoized value
14 | memo[n] = value
15 |
16 | // return it to the user
17 | return value
18 | }
19 |
20 | export default range
21 |
--------------------------------------------------------------------------------
/client/interface/Sidebar/GridSizeControl/styles.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { mediumGrey } from 'colors'
3 |
4 | export default {
5 | container: {},
6 | header: {
7 | display: 'flex',
8 | flexDirection: 'row',
9 | justifyContent: 'space-between',
10 | alignItems: 'center',
11 | marginBottom: 5,
12 | },
13 | sizeIndicator: {
14 | fontSize: 12,
15 | color: mediumGrey,
16 | },
17 | sliderContainer: {
18 | padding: '0 5px',
19 | marginBottom: 3,
20 | },
21 | }
22 |
--------------------------------------------------------------------------------
/client/interface/Sidebar/ZoomLevelControl/styles.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { mediumGrey } from 'colors'
3 |
4 | export default {
5 | container: {},
6 | header: {
7 | display: 'flex',
8 | flexDirection: 'row',
9 | justifyContent: 'space-between',
10 | alignItems: 'center',
11 | marginBottom: 5,
12 | },
13 | levelIndicator: {
14 | fontSize: 12,
15 | color: mediumGrey,
16 | },
17 | sliderContainer: {
18 | padding: '0 5px',
19 | marginBottom: 3,
20 | },
21 | }
22 |
--------------------------------------------------------------------------------
/client/components/Overlay/Header/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import styles from './styles'
5 |
6 | const OverlayHeader = ({ children, style, addon, hide, ...unusedProps }) => (
7 |
8 |
9 | x
10 |
11 |
{children}
12 |
{addon}
13 |
14 | )
15 |
16 | export default OverlayHeader
17 |
--------------------------------------------------------------------------------
/client/interface/App/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | container: {
3 | position: 'fixed',
4 | top: 0,
5 | left: 0,
6 | right: 0,
7 | bottom: 0,
8 | overflow: 'hidden',
9 |
10 | display: 'flex',
11 | flexDirection: 'row',
12 | flexGrow: 1,
13 |
14 | fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
15 | },
16 | alertContainer: {
17 | position: 'fixed',
18 | zIndex: 50,
19 | right: 20,
20 | width: 400,
21 | bottom: 20,
22 | },
23 | }
24 |
--------------------------------------------------------------------------------
/client/components/Collapsible/styles.js:
--------------------------------------------------------------------------------
1 | import { blue } from 'colors'
2 |
3 | export default {
4 | container: {
5 | display: 'flex',
6 | flexDirection: 'column',
7 | },
8 | header: {
9 | display: 'flex',
10 | flexDirection: 'row',
11 | justifyContent: 'space-between',
12 | fontSize: 12,
13 | fontWeight: 'bold',
14 | },
15 | toggle: {
16 | cursor: 'pointer',
17 | fontSize: 12,
18 | fontWeight: 'bold',
19 | color: blue,
20 | textTransform: 'capitalize',
21 | },
22 | }
23 |
--------------------------------------------------------------------------------
/client/components/ColorPicker/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | container: {
3 | display: 'flex',
4 | width: 50,
5 | cursor: 'pointer',
6 | justifyContent: 'center',
7 | zIndex: 10,
8 | },
9 | thumbnail: {
10 | display: 'flex',
11 | alignSelf: 'flex-start',
12 | width: '60%',
13 | height: 20,
14 | border: '5px solid #ddd',
15 | boxSizing: 'content-box',
16 | },
17 | pickerContainer: {
18 | position: 'absolute',
19 | right: 30,
20 | marginTop: 25,
21 | },
22 | }
23 |
--------------------------------------------------------------------------------
/client/components/index.js:
--------------------------------------------------------------------------------
1 | export Button from './Button'
2 | export Collapsible from './Collapsible'
3 | export ColorPicker from './ColorPicker'
4 | export Label from './Label'
5 | export Input from './Input'
6 | export RedButton from './RedButton'
7 | export Slider from './Slider'
8 | export ToggleButton from './ToggleButton'
9 | export Overlay from './Overlay'
10 | export Splittable from './Splittable'
11 | export Select, { Option } from './Select'
12 | export Checkbox from './Checkbox'
13 | export Code from './Code'
14 | export Text from './Text'
15 | export MouseMove from './MouseMove'
16 |
--------------------------------------------------------------------------------
/client/components/Button/styles.js:
--------------------------------------------------------------------------------
1 | import { grey, lightBlue } from 'colors'
2 |
3 | export default {
4 | container: {
5 | fontSize: 12,
6 | height: 30,
7 | border: `1px solid ${grey}`,
8 | color: lightBlue,
9 | borderRadius: 3,
10 | background: '#d2d2d2 linear-gradient(180deg, #ebebeb, #d2d2d2) repeat-x',
11 | padding: '5px 10px',
12 | textShadow: '1px 1px 1px rgba(255,255,255,0.3)',
13 | cursor: 'pointer',
14 | fontWeight: 'bold',
15 | boxShadow: '0px 1px 5px 0px rgba(50,50,50,0.24)',
16 | },
17 | }
18 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/styles.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { image } from './ItemPalette/styles'
3 |
4 | export const toolbarWidth = 220
5 |
6 | export default {
7 | container: {
8 | width: toolbarWidth,
9 | position: 'absolute',
10 | right: 0,
11 | height: '100%',
12 | display: 'flex',
13 | backgroundColor: 'rgba(50,50,50,0.65)',
14 | flexDirection: 'column',
15 | justifyContent: 'space-between',
16 | zIndex: 5,
17 | },
18 | shadow: {
19 | ...image,
20 | position: 'fixed',
21 | },
22 | }
23 |
--------------------------------------------------------------------------------
/client/colors.js:
--------------------------------------------------------------------------------
1 | export const darkGrey = "#333"
2 | export const lightGrey = "#fafafa"
3 | export const yagLGrey = "#eee"
4 | export const sighGrey = "#e5e5e5"
5 | export const grey = "#bbb"
6 | export const idkGrey = "#b3b3b3"
7 | export const itsGrey = "#959595"
8 | export const anotherGrey = "#ddd"
9 | export const mediumGrey = "#8d8d8d"
10 | export const brightBlue = "#0091ff"
11 | export const blue = "#2d89d3"
12 | export const otherBlue = "#5a768e"
13 | export const lightBlue = "#1c83ce"
14 | export const lightestBlue = "#00b7ff"
15 | export const red = "#bf5352"
16 | export const otherRed = "#c1403e"
17 |
--------------------------------------------------------------------------------
/client/interface/PatternModal/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | overlay: {},
3 | overlayContent: {
4 | flexDirection: 'row',
5 | display: 'flex',
6 | justifyContent: 'flex-start',
7 | flexWrap: 'wrap',
8 | },
9 | addon: {
10 | fontSize: 14,
11 | display: 'flex',
12 | justifyContent: 'center',
13 | alignItems: 'center',
14 | cursor: 'pointer',
15 | marginLeft: -60, // ✨ magic ✨
16 | },
17 | checkbox: {
18 | marginLeft: 10, // ✨ magic ✨
19 | marginBottom: -2, // ✨ magic ✨
20 | },
21 | }
22 |
--------------------------------------------------------------------------------
/client/components/ToggleButton/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import styles from './styles'
5 | import Button from '../Button'
6 |
7 | const ToggleButton = ({ style, active, activeText, inactiveText, ...unusedProps }) => {
8 | // get the appropriate style
9 | const buttonStyle = active ? styles.inactive : styles.active
10 |
11 | return (
12 |
13 | {active ? activeText : inactiveText}
14 |
15 | )
16 | }
17 |
18 | export default ToggleButton
19 |
--------------------------------------------------------------------------------
/client/utils/tests/round.test.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import round from '../round'
3 |
4 | describe('Utils', () => {
5 | describe('Round util', () => {
6 | test('returns correct value', () => {
7 | // the table of test cases
8 | const table = [[5, 10, 10], [2, 10, 0], [15, 10, 20], [7, 0, 7]]
9 |
10 | // loop over every row in the table
11 | for (const [n, to, expected] of table) {
12 | // make sure the result matches expectation
13 | expect(round(n, to)).toEqual(expected)
14 | }
15 | })
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/client/components/Slider/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | import RcSlider from 'rc-slider'
4 | import 'rc-slider/assets/index.css'
5 | // local imports
6 | import styles from './styles'
7 | import './slider.css'
8 |
9 | const Slider = ({ min, max, step, style, ...unusedProps }) => (
10 |
11 | )
12 |
13 | const SliderHandle = ({ offset }) => (
14 |
15 |
16 |
17 | )
18 |
19 | export default Slider
20 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/SelectionSummary/PropagatorSummary/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | container: {
3 | display: 'flex',
4 | },
5 | select: {
6 | width: '100%',
7 | },
8 | button: {
9 | width: '100%',
10 | textTransform: 'capitalize',
11 | },
12 | colorPicker: {
13 | marginLeft: 'auto',
14 | marginRight: 10,
15 | },
16 | labelInput: {
17 | height: 32,
18 | fontSize: 16,
19 | display: 'flex',
20 | flexGrow: 1,
21 | width: 10,
22 | },
23 | labelLabel: {
24 | padding: '0 15px 0 0',
25 | },
26 | }
27 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/utils/anchorsInSpec.js:
--------------------------------------------------------------------------------
1 | export default function anchorsInSpec(doc) {
2 | // if we were passed a null doc
3 | if (doc === null) {
4 | // return an empty list
5 | return []
6 | }
7 |
8 | // grab the used information
9 | const { type, ...spec } = doc.element
10 |
11 | // if the spec is a propagator
12 | if (type === 'propagators') {
13 | // there are two anchors in a propagator labeled anchor1 and anchor2
14 | return [spec.anchor1, spec.anchor2]
15 | } else {
16 | // otherwise its a type we don't recognize
17 | return []
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/client/interface/Sidebar/HistorySummary/Entry/styles.js:
--------------------------------------------------------------------------------
1 | import { lightBlue } from 'colors'
2 |
3 | const container = {
4 | fontSize: 12,
5 | display: 'flex',
6 | flexDirection: 'row',
7 | cursor: 'pointer',
8 | }
9 |
10 | export default {
11 | container,
12 | active: {
13 | ...container,
14 | color: lightBlue,
15 | },
16 | index: {
17 | width: '15%',
18 | display: 'flex',
19 | alignItems: 'flex-end',
20 | flexDirection: 'column',
21 | paddingRight: 5,
22 | },
23 | message: {
24 | width: '85%',
25 | display: 'flex',
26 | },
27 | }
28 |
--------------------------------------------------------------------------------
/client/store/tests/store.test.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import store, { createStore } from '..'
3 |
4 | describe('Application store', () => {
5 | test('is a valid redux store', () => {
6 | const store = createStore()
7 | expect(store.getState()).toBeDefined()
8 | })
9 |
10 | describe('Factory', () => {
11 | test('produces an app store', () => {
12 | // create a mock store
13 | const mockStore = createStore()
14 | // make sure it has the info reducer
15 | expect(mockStore.getState().diagram.elements.propagators).toBeDefined()
16 | })
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/client/actions/info/creators/index.js:
--------------------------------------------------------------------------------
1 | export setDiagramTitle from './setDiagramTitle'
2 | export toggleGrid from './toggleGrid'
3 | export toggleAnchors from './toggleAnchors'
4 | export toggleHotkeys from './toggleHotkeys'
5 | export setGridSize from './setGridSize'
6 | export togglePatternModal from './togglePatternModal'
7 | export togglePatternModalInitialVis from './togglePatternModalInitialVis'
8 | export toggleExportModal from './toggleExportModal'
9 | export toggleHistory from './toggleHistory'
10 | export panDiagram from './panDiagram'
11 | export setZoom from './setZoom'
12 | export zoomIn from './zoomIn'
13 | export zoomOut from './zoomOut'
14 |
--------------------------------------------------------------------------------
/client/utils/relativePosition.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { sidebarWidth } from 'interface/Sidebar/styles'
3 |
4 | // this function takes coordinates in the browser, and returns their location relative
5 | // to the upper left corner of the diagram
6 | const relativePosition = ({ x, y }, info) => {
7 | if (!info) {
8 | info = { pan: { x: 0, y: 0 } }
9 | }
10 |
11 | const zoom = info.zoomLevel || 1
12 |
13 | // for now, just incorporate the sidebar
14 | return {
15 | x: (x - sidebarWidth - info.pan.x) / zoom,
16 | y: (y - info.pan.y) / zoom,
17 | }
18 | }
19 |
20 | export default relativePosition
21 |
--------------------------------------------------------------------------------
/client/components/Overlay/Header/styles.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { otherBlue } from 'colors'
3 |
4 | export default {
5 | container: {
6 | backgroundColor: otherBlue,
7 | padding: '2%',
8 | borderRadius: '6px 6px 0 0',
9 | alignItems: 'center',
10 | justifyContent: 'space-between',
11 | display: 'flex',
12 | flexDirection: 'row',
13 | color: 'white',
14 | fontSize: 18,
15 | },
16 | title: {
17 | fontWeight: 'bold',
18 | },
19 | close: {
20 | cursor: 'pointer',
21 | textTransform: 'capitalize',
22 | fontWeight: 'bold',
23 | },
24 | }
25 |
--------------------------------------------------------------------------------
/client/components/Slider/styles.js:
--------------------------------------------------------------------------------
1 | import { grey, darkGrey } from 'colors'
2 |
3 | export default {
4 | slider: {
5 | borderRadius: '100%',
6 | position: 'absolute',
7 | width: 20,
8 | height: 20,
9 | cursor: 'pointer',
10 | backgroundColor: 'white',
11 | border: `1px solid ${darkGrey}`,
12 | marginTop: -8,
13 | marginLeft: -8,
14 | display: 'flex',
15 | justifyContent: 'center',
16 | alignItems: 'center',
17 | },
18 | innerSlider: {
19 | backgroundColor: '#646464',
20 | borderRadius: '100%',
21 | width: 10,
22 | height: 10,
23 | },
24 | }
25 |
--------------------------------------------------------------------------------
/client/actions/info/creators/togglePatternModalInitialVis.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { TOGGLE_PATTERN_INITIAL_VIS } from 'actions/info/types'
3 |
4 | // the name of the field containing the initial pattern visibility
5 | export const fieldName = '@feynman/show_pattern_modal'
6 |
7 | export default storage => (dispatch, getState) => {
8 | // the current value
9 | const current = getState().diagram.info.patternModalInitalVis
10 |
11 | // invert the value
12 | storage.setItem(fieldName, !current)
13 |
14 | // we're done so let the store catch up (dispatch the appropriate action)
15 | dispatch({
16 | type: TOGGLE_PATTERN_INITIAL_VIS,
17 | })
18 | }
19 |
--------------------------------------------------------------------------------
/client/components/Collapsible/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import styles from './styles'
5 | import { Label } from 'components'
6 |
7 | const Collapsible = ({ active, toggle, children, title, style, ...unusedProps }) => (
8 |
9 |
10 |
{title}
11 |
12 | {active ? 'hide' : 'show'}
13 |
14 |
15 | {active && children}
16 |
17 | )
18 |
19 | export default Collapsible
20 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/SelectionSummary/PropagatorSummary/FermionSummary/styles.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { red } from 'colors'
3 |
4 | const button = {
5 | display: 'flex',
6 | flexGrow: 1,
7 | width: 10,
8 | alignItems: 'center',
9 | justifyContent: 'center',
10 | outline: 'none',
11 | }
12 |
13 | export default {
14 | container: {
15 | display: 'flex',
16 | flexDirection: 'row',
17 | },
18 | hideButton: {
19 | ...button,
20 | width: 45,
21 | marginRight: 10,
22 | color: red,
23 | },
24 | flipButton: {
25 | ...button,
26 | },
27 | showButton: {
28 | ...button,
29 | },
30 | }
31 |
--------------------------------------------------------------------------------
/client/components/Overlay/styles.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { yagLGrey } from 'colors'
3 |
4 | export default {
5 | container: {
6 | position: 'fixed',
7 | top: 0,
8 | left: 0,
9 | right: 0,
10 | bottom: 0,
11 | display: 'flex',
12 | alignItems: 'center',
13 | justifyContent: 'center',
14 | backgroundColor: 'rgba(69, 69, 69, 0.8)',
15 | zIndex: 5,
16 | },
17 | header: {
18 | display: 'flex',
19 | },
20 | content: {
21 | backgroundColor: yagLGrey,
22 | borderRadius: '0 0 6px 6px',
23 | padding: '1.5%',
24 | },
25 | contentWrapper: {
26 | width: '70%',
27 | },
28 | }
29 |
--------------------------------------------------------------------------------
/client/interface/Diagram/Text/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import { Text, Splittable } from 'components'
5 | import styles from './styles'
6 |
7 | const TextElement = ({ x, y, value, id, selected }) => {
8 | // the style to apply to the text (disabled because we render slowly)
9 | // const style = selected ? styles.selected : styles.notSelected
10 | const style = styles.notSelected
11 |
12 | return (
13 |
14 |
15 | {value}
16 |
17 |
18 | )
19 | }
20 |
21 | export default TextElement
22 |
--------------------------------------------------------------------------------
/client/sagas/withCommit/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import { takeEvery } from 'redux-saga'
3 | import { put } from 'redux-saga/effects'
4 | // local imports
5 | import { commit, WITH_COMMIT } from 'actions/history'
6 |
7 | export function* withCommitWorker({ type, payload }) {
8 | // grab the action and message
9 | const { action, message } = payload
10 |
11 | // first apply the action
12 | yield put(action)
13 | // commit the state of the store after the action
14 | yield put(commit(message))
15 | }
16 |
17 | // this saga loads a particular pattern
18 | export default function* withCommit() {
19 | // whenever we want to load a pattern onto the diagram
20 | yield takeEvery(WITH_COMMIT, withCommitWorker)
21 | }
22 |
--------------------------------------------------------------------------------
/server/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang
2 |
3 | # upgrade apt and install deps
4 | RUN apt update && \
5 | apt upgrade -y && \
6 | apt install -y \
7 | imagemagick \
8 | texlive-latex-recommended \
9 | texlive-latex-extra \
10 | texlive-science \
11 | texlive-latex-extra \
12 | texlive-fonts-recommended \
13 | texlive-fonts-extra
14 |
15 | WORKDIR /usr/src/app
16 |
17 | # copy server source over
18 | ADD . .
19 |
20 | # pre-copy/cache go.mod for pre-downloading dependencies and only redownloading them in subsequent builds if they change
21 | COPY go.mod ./
22 | RUN go mod download && go mod verify
23 |
24 | # build the server binary
25 | RUN go build .
26 |
27 | # start the server at the exposed port
28 | CMD ["feynman"]
29 |
--------------------------------------------------------------------------------
/client/sagas/loadPattern/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import { takeEvery } from 'redux-saga'
3 | import { put } from 'redux-saga/effects'
4 | // local imports
5 | import { placeElement, clearElements } from 'actions/elements'
6 | import { LOAD_PATTERN } from 'actions/elements/types'
7 |
8 | export function* loadPatternWorker({ type, payload: { elements } }) {
9 | // first thing to do is clear the current diagram
10 | yield put(clearElements())
11 |
12 | // load the element
13 | yield put(placeElement(elements))
14 | }
15 |
16 | // this saga loads a particular pattern
17 | export default function* loadPattern() {
18 | // whenever we want to load a pattern onto the diagram
19 | yield takeEvery(LOAD_PATTERN, loadPatternWorker)
20 | }
21 |
--------------------------------------------------------------------------------
/client/utils/index.js:
--------------------------------------------------------------------------------
1 | export range from './range'
2 | export ceil from './ceil'
3 | export round from './round'
4 | export generateElementId from './generateElementId'
5 | export relativePosition from './relativePosition'
6 | export fixPositionToGrid from './fixPositionToGrid'
7 | export elementsInRegion from './elementsInRegion'
8 | export throttle from './throttle'
9 | export flatMap from './flatMap'
10 | export fixDeltaToGrid from './fixDeltaToGrid'
11 | export elementsWithLocations from './elementsWithLocations'
12 | export dataUrlToBlob from './dataUrlToBlob'
13 | export diagramBoundingBox from './diagramBoundingBox'
14 | export svgToDataURL from './svgToDataUrl'
15 | export constrainLocationToShape from './constrainLocationToShape'
16 | export * from './latexConfig'
17 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/ItemPalette/paletteItem.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | import PropTypes from 'prop-types'
4 | // local imports
5 | import styles from './styles'
6 |
7 | const PaletteItem = ({ style, image, element, onMouseDown, config, ...unusedProps }) => (
8 | onMouseDown({ image, event, config })}
11 | {...unusedProps}
12 | >
13 | {/* the static image */}
14 |
15 |
16 | )
17 |
18 | PaletteItem.propTypes = {
19 | style: PropTypes.string,
20 | image: PropTypes.string,
21 | config: PropTypes.object.isRequired,
22 | }
23 |
24 | export default PaletteItem
25 |
--------------------------------------------------------------------------------
/client/actions/info/types.js:
--------------------------------------------------------------------------------
1 | export const SET_TITLE = '@feynamn/SET_TITLE'
2 | export const TOGGLE_GRID = '@feynman/TOGGLE_GRID'
3 | export const SET_GRID_SIZE = '@feynamn/SET_GRID_SIZE'
4 | export const TOGGLE_HOTKEYS = '@feynman/TOGGLE_HOTKEYS'
5 | export const TOGGLE_ANCHORS = '@feynman/TOGGLE_ACHORS'
6 | export const TOGGLE_HISTORY = '@feynman/TOGGLE_HISTORY'
7 | export const TOGGLE_PATTERN_MODAL = '@feynman/TOGGLE_PATTERN_MODAL'
8 | export const TOGGLE_PATTERN_INITIAL_VIS = '@feynman/TOGGLE_PATTERN_INITIAL_VIS'
9 | export const TOGGLE_EXPORT_MODAL = '@feynman/TOGGLE_EXPORT_MODAL'
10 | export const PAN_DIAGRAM = '@feynman/PAN_DIAGRAM'
11 | export const SET_ZOOM = '@feynman/SET_ZOOM'
12 | export const ZOOM_IN = '@feynman/ZOOM_IN'
13 | export const ZOOM_OUT = '@feynman/ZOOM_OUT'
14 |
--------------------------------------------------------------------------------
/client/interface/Sidebar/styles.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { yagLGrey } from 'colors'
3 |
4 | // dimentions
5 | export const sidebarWidth = 250
6 | export const elementSpacing = 10
7 |
8 | export default {
9 | container: {
10 | display: 'flex',
11 | width: sidebarWidth,
12 | backgroundColor: 'white',
13 | flexDirection: 'column',
14 | padding: '0 10px',
15 | zIndex: 5,
16 | },
17 | sidebarContainer: {
18 | display: 'flex',
19 | flexDirection: 'column',
20 | },
21 | element: {
22 | padding: elementSpacing,
23 | paddingBottom: 0,
24 | },
25 | elementWithBorder: {
26 | padding: elementSpacing,
27 | borderBottom: `1px solid ${yagLGrey}`,
28 | },
29 | }
30 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/SelectionSummary/PropagatorSummary/ElectroWeakSummary/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import { Button } from 'components'
5 | import { ButtonRow } from '../..'
6 | import ElectroWeak from 'interface/Diagram/Propagator/ElectroWeak'
7 |
8 | const defaults = ElectroWeak.defaultProps
9 |
10 | const PropagatorSummary = ({ direction = defaults.direction, setAttrs, commit, ...unusedProps }) => (
11 |
12 | setAttrs({ direction: -direction }, 'inverted propagator amplitude')}
15 | >
16 | Invert Amplitude
17 |
18 |
19 | )
20 |
21 | export default PropagatorSummary
22 |
--------------------------------------------------------------------------------
/client/store/tests/browser.test.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { createStore } from 'store'
3 |
4 | describe('Reducers', () => {
5 | describe('Browser reducer', () => {
6 | test('has width attribute', () => {
7 | // create a store to test with
8 | const mock = createStore()
9 |
10 | // make sure the width attribute exists
11 | expect(mock.getState().browser.width).toEqual(window.innerWidth)
12 | })
13 |
14 | test('has height attribute', () => {
15 | // create a store to test with
16 | const mock = createStore()
17 |
18 | // make sure the height attribute exists
19 | expect(mock.getState().browser.height).toEqual(window.innerHeight)
20 | })
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/client/interface/Diagram/Anchor/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import { Splittable } from 'components'
5 | import styles from './styles'
6 |
7 | export const Anchor = ({ x, y, selected, r, fill, fixed, id }) => {
8 | // get any required styling
9 | let styling = selected ? styles.selected : styles.notSelected
10 | // if the anchor is fixed, mixin the fixed with
11 | styling = fixed ? { ...styles.fixed, ...styling } : styling
12 |
13 | return (
14 |
15 |
16 |
17 | )
18 | }
19 |
20 | Anchor.defaultProps = {
21 | r: 5,
22 | fill: 'black',
23 | fixed: false,
24 | }
25 |
26 | export default Anchor
27 |
--------------------------------------------------------------------------------
/client/interface/Sidebar/TitleControl/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | import { connect } from 'react-redux'
4 | // local imports
5 | import { Label, Input } from 'components'
6 | import styles from './styles'
7 | import { setDiagramTitle } from 'actions/info'
8 |
9 | const TitleControl = ({ style, info, dispatch, ...unusedProps }) => (
10 |
11 | title
12 | dispatch(setDiagramTitle(target.value))}
16 | />
17 |
18 | )
19 |
20 | const selector = ({ diagram: { info } }) => ({ info })
21 | // export the connected component
22 | export default connect(selector)(TitleControl)
23 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/Footer/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import styles from './styles'
5 |
6 | const ToolbarFooter = ({ style, ...unusedProps }) => (
7 |
22 | )
23 |
24 | export default ToolbarFooter
25 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/SelectionSummary/AnchorSummary/styles.js:
--------------------------------------------------------------------------------
1 | import { lightBlue } from 'colors'
2 |
3 | const button = {
4 | display: 'flex',
5 | alignSelf: 'center',
6 | textTransform: 'capitalize',
7 | width: '100%',
8 | flexDirection: 'row',
9 | justifyContent: 'center',
10 | alignItems: 'center',
11 | }
12 |
13 | export default {
14 | container: {
15 | display: 'flex',
16 | flexDirection: 'column',
17 | alignItems: 'center',
18 | flexGrow: 1,
19 | },
20 | fixButton: {
21 | ...button,
22 | },
23 | deleteButton: {
24 | ...button,
25 | },
26 | alignButton: {
27 | ...button,
28 | color: lightBlue,
29 | width: '100%',
30 | },
31 | colorPicker: {
32 | marginLeft: 'auto',
33 | marginRight: 10,
34 | },
35 | }
36 |
--------------------------------------------------------------------------------
/client/utils/tests/constrainLocactionToShape.test.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import constrainLocactionToShape from '../constrainLocationToShape'
3 |
4 | describe('Utils', () => {
5 | describe('Constrain Location to Shape', () => {
6 | test('constrains locaction to circle', () => {
7 | // the shape to constraint it to
8 | const shape = {
9 | kind: 'parton',
10 | r: 50,
11 | x: 50,
12 | y: 50,
13 | }
14 |
15 | // the location to constrain
16 | const location = {
17 | x: 105,
18 | y: 50,
19 | }
20 |
21 | expect(constrainLocactionToShape({ shape, location })).toEqual({
22 | x: 100,
23 | y: 50,
24 | })
25 | })
26 | })
27 | })
28 |
--------------------------------------------------------------------------------
/client/interface/Diagram/Shape/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | import PropTypes from 'prop-types'
4 | // local imports
5 | import { Splittable } from 'components'
6 | import Parton from './Parton'
7 |
8 | // a mapping of shape type to component to render
9 | const shapeMap = {
10 | parton: Parton,
11 | }
12 |
13 | export const Shape = ({ kind, dispatch, ...element }) => {
14 | const Component = shapeMap[kind]
15 | return (
16 |
17 |
18 |
19 | )
20 | }
21 |
22 | Shape.propTypes = {
23 | kind: PropTypes.string,
24 | color: PropTypes.string,
25 | r: PropTypes.number,
26 | }
27 |
28 | Shape.defaultProps = {
29 | kind: 'parton',
30 | color: 'black',
31 | r: 25,
32 | }
33 |
34 | export default Shape
35 |
--------------------------------------------------------------------------------
/server/test/diagram.tex:
--------------------------------------------------------------------------------
1 | % this file describes the basic latex file that is used by the interpretter at alec.aivazis
2 | % author: alec aivazis
3 |
4 | \documentclass[border=1pt]{standalone}
5 |
6 | \usepackage{feynman}
7 |
8 | \begin{document}
9 | \fontsize{5}{6}
10 | \textcolor{black}{
11 | \begin{feynman}
12 | \fermion[label=$l$]{2.20, 10.20}{0.20, 12.20}
13 | \fermion[label=$\anti{l}$]{0.20, 8.20}{2.20, 10.20}
14 | \electroweak[color=2d89d3, label=$Z$]{2.20, 10.20}{5.20, 10.20}
15 | \fermion[label=$l$]{7.20, 12.20}{5.20, 10.20}
16 | \fermion[label=$\anti{l}$]{5.20, 10.20}{7.20, 8.20}
17 | \fermion[]{3.20, 7.20}{4.20, 0.20}
18 | \fermion[]{4.20, 0.20}{10.20, 4.20}
19 | \fermion[]{2.20, 4.20}{3.20, 7.20}
20 | \fermion[]{0.20, 8.20}{2.20, 4.20}
21 | \fermion[]{3.20, 4.20}{3.20, 4.20}
22 | \end{feynman}
23 | }
24 |
25 | \end{document}
26 |
27 |
28 | % end of file
29 |
--------------------------------------------------------------------------------
/client/utils/tests/throttle.test.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import throttle from '../throttle'
3 |
4 | describe('Utils', () => {
5 | describe('Throttle decorator util', () => {
6 | test('can throttle a class method', () => {
7 | // a spy to count invocations
8 | const spy = jest.fn()
9 |
10 | // a class to test
11 | class Foo {
12 | // create a throttled method
13 | @throttle(1000)
14 | decorated() {
15 | // call the spy
16 | spy()
17 | }
18 | }
19 |
20 | // call the method twice
21 | const foo = new Foo()
22 | foo.decorated()
23 | foo.decorated()
24 |
25 | // make sure the spy was only called one
26 | expect(spy).toHaveBeenCalledTimes(1)
27 | })
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/client/interface/App/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | import { connect } from 'react-redux'
4 | import { AlertContainer } from 'quark-web'
5 | // local imports
6 | import './reset.css'
7 | import styles from './styles'
8 | import { Diagram, Sidebar, Title, Toolbar, PatternModal, ExportModal } from '..'
9 |
10 | // App must be a class-based component because it will recieve a ref
11 | const App = ({ info }) => (
12 |
13 |
14 |
15 |
16 |
17 | {info.showPatternModal && }
18 | {info.showExportModal && }
19 |
20 |
21 | )
22 |
23 | const selector = ({ diagram: { info } }) => ({ info })
24 | export default connect(selector)(App)
25 |
--------------------------------------------------------------------------------
/client/interface/PatternModal/Choice/styles.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { anotherGrey, brightBlue } from 'colors'
3 |
4 | const container = {
5 | display: 'flex',
6 | flexDirection: 'column',
7 | width: '29.5%',
8 | boxShadow: '0px 2px 5px 0px rgba(208,208,208,0.75)',
9 | backgroundColor: 'white',
10 | borderRadius: 6,
11 | margin: '1.5%',
12 | cursor: 'pointer',
13 | }
14 |
15 | export default {
16 | container,
17 | hoverContainer: {
18 | ...container,
19 | border: `2px solid ${brightBlue}`,
20 | },
21 | title: {
22 | display: 'flex',
23 | justifyContent: 'center',
24 | alignItems: 'center',
25 | borderBottom: `1px solid ${anotherGrey}`,
26 | padding: 10,
27 | },
28 | content: {
29 | display: 'flex',
30 | },
31 | preview: {
32 | width: '100%',
33 | padding: 15,
34 | },
35 | }
36 |
--------------------------------------------------------------------------------
/client/utils/tests/fixPositionToGrid.test.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import fixPositionToGrid from '../fixPositionToGrid'
3 |
4 | describe('Utils', () => {
5 | describe('Fix Position to Grid Util', () => {
6 | test('can correct coordinates', () => {
7 | // for now, the only difference between absolute and diagram coordinates
8 | // is the sidebar width
9 | expect(fixPositionToGrid({ x: 48, y: 102 }, 50)).toEqual({
10 | x: 50,
11 | y: 100,
12 | })
13 | })
14 |
15 | it("doesn't round when gridsize is 0", function() {
16 | // for now, the only difference between absolute and diagram coordinates
17 | // is the sidebar width
18 | expect(fixPositionToGrid({ x: 48, y: 102 }, 0)).toEqual({
19 | x: 48,
20 | y: 102,
21 | })
22 | })
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/client/utils/fixDeltaToGrid.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { round as roundTo } from 'utils'
3 |
4 | export default function computeFixedDelta({ origin, next, round = true, info: { zoomLevel, gridSize } }) {
5 | // if there is no grid or we aren't supposed to round
6 | if (gridSize === 0 || !round) {
7 | // just move along
8 | return next
9 | }
10 |
11 | // convert the grid size into viewport coordinates
12 | const spacing = gridSize * zoomLevel
13 |
14 | // compute the difference between the mouse's current location and the previous one
15 | const delta = {
16 | x: next.x - origin.x,
17 | y: next.y - origin.y,
18 | }
19 |
20 | // the location to move to
21 | const fixed = { ...origin }
22 |
23 | // round the delta to match the grid
24 | fixed.x += roundTo(delta.x, spacing)
25 | fixed.y += roundTo(delta.y, spacing)
26 |
27 | return fixed
28 | }
29 |
--------------------------------------------------------------------------------
/client/components/Overlay/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | import PropTypes from 'prop-types'
4 | // local imports
5 | import styles from './styles'
6 | import Header from './Header'
7 |
8 | const preventBubble = event => event && event.stopPropagation()
9 |
10 | const Overlay = ({ addon, title, children, hide, onClick, style, ...unusedProps }) => (
11 |
12 |
13 |
16 | {children}
17 |
18 |
19 | )
20 |
21 | Overlay.propTypes = {
22 | hide: PropTypes.func.isRequired,
23 | addon: PropTypes.element,
24 | style: PropTypes.object,
25 | }
26 |
27 | export default Overlay
28 |
--------------------------------------------------------------------------------
/client/actions/elements/creators/saveDiagram.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import { saveAs } from 'file-saver'
3 | import _ from 'lodash'
4 | // local imports
5 | import { EXPORT_DIAGRAM } from 'actions/elements'
6 |
7 | export default () => (dispatch, getState) => {
8 | // grab the information we want to persist
9 | const {
10 | diagram: {
11 | info: { title },
12 | elements,
13 | },
14 | } = getState()
15 |
16 | const diagramElements = _.cloneDeep(elements)
17 |
18 | // remove the history from the elements
19 | Reflect.deleteProperty(diagramElements, 'history')
20 | Reflect.deleteProperty(diagramElements, 'selection')
21 |
22 | // save the diagram as a json file
23 | saveAs(
24 | new Blob([
25 | JSON.stringify({
26 | title,
27 | elements: diagramElements,
28 | }),
29 | ]),
30 | `${title}.json`
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/client/actions/elements/creators/index.js:
--------------------------------------------------------------------------------
1 | export addPropagators from './addPropagators'
2 | export addAnchors from './addAnchors'
3 | export setAnchorLocations from './setAnchorLocations'
4 | export selectElements from './selectElements'
5 | export clearSelection from './clearSelection'
6 | export mergeElements from './mergeElements'
7 | export setElementAttrs from './setElementAttrs'
8 | export deleteElements from './deleteElements'
9 | export moveSelectedElements from './moveSelectedElements'
10 | export snapSelectedElements from './snapSelectedElements'
11 | export alignSelectedAnchors from './alignSelectedAnchors'
12 | export clearElements from './clearElements'
13 | export deleteSelection from './deleteSelection'
14 | export placeElement from './placeElement'
15 | export loadPattern from './loadPattern'
16 | export addElements from './addElements'
17 | export splitElement from './splitElement'
18 | export saveDiagram from './saveDiagram'
19 | export loadDiagram from './loadDiagram'
20 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/SelectionSummary/SliderRow/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import { Slider } from 'components'
5 | import styles from './styles'
6 | import MultiRow from '../MultiRow'
7 | import Row from '../Row'
8 | import Label from '../Label'
9 |
10 | const SliderRow = ({ label, value, onChange, initial, max, min, step, ...unusedProps }) => (
11 |
12 |
13 | {label}:
14 |
20 |
21 |
22 |
23 |
24 |
25 | )
26 |
27 | export default SliderRow
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # feynman
2 |
3 | [](https://travis-ci.org/AlecAivazis/feynman)
4 | [](https://coveralls.io/github/AlecAivazis/feynman?branch=feat%2Fcoverage)
5 |
6 | A javascript application for creating feynman diagrams. Live at [feynman.aivazis.com](http://feynman.aivazis.com).
7 |
8 | This repo started as a migration of my [original](https://github.com/AlecAivazis/feynman-old) feynman diagram
9 | application to a leaner flask server. It then evolved from a [badly written angular 1.2 application](https://github.com/AlecAivazis/feynman/tree/angular1.x) to a less-badly written react/redux app. During that time,
10 | the backend (responsible for rendering latex) was also rewritten from python to golang.
11 |
12 | ## Running the Tests
13 |
14 | The tests are written using jest:
15 |
16 | ```bash
17 | npm i && npm run test
18 | ```
19 |
--------------------------------------------------------------------------------
/client/interface/Diagram/Propagator/Fermion/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import styles from './styles'
5 | import Arrow from './Arrow'
6 |
7 | const Fermion = ({ x1, y1, x2, y2, stroke, anchor1, anchor2, strokeWidth, selected, arrow, ...unusedProps }) => (
8 |
9 |
16 | {arrow != 0 && (
17 |
27 | )}
28 |
29 | )
30 |
31 | Fermion.defaultProps = {
32 | arrow: 1,
33 | }
34 |
35 | export default Fermion
36 |
--------------------------------------------------------------------------------
/client/interface/Sidebar/index.js:
--------------------------------------------------------------------------------
1 | // external
2 | import React from 'react'
3 | // local imports
4 | import { setDiagramTitle } from 'actions/info'
5 | import { Label, Input } from 'components'
6 | import styles from './styles'
7 | import TitleControl from './TitleControl'
8 | import ZoomLevelControl from './ZoomLevelControl'
9 | import GridSizeControl from './GridSizeControl'
10 | import HotkeySummary from './HotkeySummary'
11 | import HistorySummary from './HistorySummary'
12 | import ButtonGrid from './ButtonGrid'
13 |
14 | const Sidebar = ({ style, dispatch, info }) => (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | )
24 |
25 | export default Sidebar
26 |
--------------------------------------------------------------------------------
/client/sagas/placeElements/tests/placeAnchor.test.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import { put } from 'redux-saga/effects'
3 | // local imports
4 | import { addAnchors } from 'actions/elements'
5 | import placeAnchor from '../placeAnchor'
6 |
7 | describe('Sagas', () => {
8 | describe('Place Elements', () => {
9 | describe('Anchors', () => {
10 | test('can place an anchor', () => {
11 | // the description of the anchor to create
12 | const desc = {
13 | id: 1,
14 | x: 75,
15 | y: 80,
16 | }
17 |
18 | // get the generator
19 | const gen = placeAnchor(desc)
20 | // the only thing we have to do is create the anchor
21 | expect(gen.next().value).toEqual(put(addAnchors(desc)))
22 |
23 | // make sure there isn't anything left
24 | expect(gen.next().done).toBeTruthy()
25 | })
26 | })
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/client/sagas/splitElement/test.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import { select, put } from 'redux-saga/effects'
3 | // local imports
4 | import { constrainLocationToShape } from 'utils'
5 | import { splitElement, commit, ADD_ANCHORS, ADD_PROPAGATOR, UPDATE_ELEMENT } from 'actions/elements'
6 | import { splitElementWorker } from '.'
7 |
8 | describe('Sagas', () => {
9 | describe('Split Element', () => {
10 | test('gracefully handles unrecognized elements', () => {
11 | // the element to split
12 | const element = {
13 | type: 'foo',
14 | }
15 |
16 | // the location to split at
17 | const location = {
18 | x: 50,
19 | y: 105,
20 | }
21 |
22 | // create the generator responding to the action
23 | const gen = splitElementWorker(splitElement({ element, location }))
24 |
25 | // make sure we're done
26 | expect(gen.next().done).toBeTruthy()
27 | })
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/client/interface/Diagram/Propagator/Label/relLocForLabel.js:
--------------------------------------------------------------------------------
1 | export default function relLocForLabel(location, propagator) {
2 | // compute the distance between the label and the first anchor
3 | const dx = location.x - propagator.x1
4 | const dy = location.y - propagator.y1
5 | const r = Math.sqrt(dx * dx + dy * dy)
6 |
7 | // save a reference to the distances between the anchors
8 | const anchorDx = propagator.x2 - propagator.x1
9 | const anchorDy = propagator.y1 - propagator.y2
10 | const m = anchorDy / anchorDx
11 |
12 | // compute the angle formed by the lengths
13 | let θ = Math.atan2(dy, dx) + Math.atan2(anchorDy, anchorDx)
14 | // if a flip is necessary
15 | if (anchorDx < 0 || (anchorDx === 0 && anchorDy < 0)) {
16 | // invert the angle
17 | θ *= -1
18 | }
19 |
20 | // return the resulting location
21 | return {
22 | labelDistance: -r * Math.sin(θ),
23 | labelLocation: r * Math.cos(θ) / Math.sqrt(anchorDx * anchorDx + anchorDy * anchorDy),
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/client/sagas/loadPattern/test.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import { put } from 'redux-saga/effects'
3 | // local imports
4 | import { loadPatternWorker } from '.'
5 | import { loadPattern, placeElement, clearElements } from 'actions/elements'
6 |
7 | describe('Sagas', () => {
8 | describe('Load Pattern', () => {
9 | test('loads a pattern over the current state', () => {
10 | // the description of the anchor to create
11 | const desc = {
12 | elements: 'hello',
13 | }
14 |
15 | // get the generator
16 | const gen = loadPatternWorker(loadPattern(desc))
17 |
18 | // the first thing to do is clear all visible elements
19 | expect(gen.next().value).toEqual(put(clearElements()))
20 |
21 | // the next thing is to place the elements
22 | expect(gen.next().value).toEqual(put(placeElement(desc.elements)))
23 |
24 | // make sure there isn't anything left
25 | expect(gen.next().done).toBeTruthy()
26 | })
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/client/sagas/withCommit/test.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import { put } from 'redux-saga/effects'
3 | // local imports
4 | import { withCommit, commit } from 'actions/history'
5 | import { withCommitWorker } from '.'
6 |
7 | describe('Sagas', () => {
8 | describe('With Commit', () => {
9 | test('dispatches the provided action and commits it with message', () => {
10 | // the action to dispatch
11 | const action = { hello: 'world' }
12 | // the message to commit with
13 | const msg = 'hello'
14 |
15 | // the generator wrapping the action
16 | const gen = withCommitWorker(withCommit(action, msg))
17 |
18 | // make sure the first thing we do is dispatch the action
19 | expect(gen.next().value).toEqual(put(action))
20 | // then make sure we commit the state after the action
21 | expect(gen.next().value).toEqual(put(commit(msg)))
22 | // we should be done
23 | expect(gen.next().done).toBeTruthy()
24 | })
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/client/sagas/placeElements/placePropagator.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import { put } from 'redux-saga/effects'
3 | // local imports
4 | import { flatMap } from 'utils'
5 | import { addAnchors, addPropagators } from 'actions/elements'
6 |
7 | export default function* createPropagator(...propagators) {
8 | // figure out if there are any anchors to create
9 | const anchors = flatMap(
10 | propagators,
11 | // find the non-numeric anchors
12 | ({ anchor1, anchor2 }) => [anchor1, anchor2].filter(anchor => !isFinite(anchor))
13 | )
14 |
15 | // if there are anchors to create
16 | if (anchors.length > 0) {
17 | // create the anchors
18 | yield put(addAnchors(...anchors))
19 | }
20 |
21 | // next we have to create a propagator between the two anchors
22 | yield put(
23 | addPropagators(
24 | ...propagators.map(prop => ({
25 | ...prop,
26 | anchor1: prop.anchor1.id || prop.anchor1,
27 | anchor2: prop.anchor2.id || prop.anchor2,
28 | }))
29 | )
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/client/interface/Diagram/Patterns.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import { brightBlue } from 'colors'
5 |
6 | const DiagramPatterns = () => (
7 |
8 | {/* patterns used in the application */}
9 |
18 |
19 |
20 |
29 |
30 |
31 |
32 | )
33 |
34 | export default DiagramPatterns
35 |
--------------------------------------------------------------------------------
/client/sagas/splitElement/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import { takeEvery } from 'redux-saga'
3 | import { put } from 'redux-saga/effects'
4 | // local imports
5 | import { SPLIT_ELEMENT } from 'actions/elements'
6 | import shape from './shape'
7 | import propagator from './propagator'
8 | import anchor from './anchor'
9 |
10 | // a map of element type to the split function
11 | const splitMap = {
12 | shapes: shape,
13 | propagators: propagator,
14 | anchors: anchor,
15 | }
16 |
17 | export function* splitElementWorker({ type, payload: { type: elementType, element, location } }) {
18 | // try to get the appropriate handler for the element type
19 | const splitFn = splitMap[elementType]
20 |
21 | // if there is a function to call
22 | if (splitFn) {
23 | // pass the element and location to the appropriate function
24 | yield* splitFn({ element, location })
25 | }
26 | }
27 |
28 | // this saga loads a particular pattern
29 | export default function* splitElement() {
30 | // whenever we want to load a pattern onto the diagram
31 | yield takeEvery(SPLIT_ELEMENT, splitElementWorker)
32 | }
33 |
--------------------------------------------------------------------------------
/client/utils/diagramBoundingBox.js:
--------------------------------------------------------------------------------
1 | export default function diagramBoundingBox(elements, padding = 10) {
2 | // the object to return
3 | const bb = {}
4 |
5 | // diagram bounding boxes are primarily defined by the anchors
6 | for (const { x, y } of Object.values(elements.anchors)) {
7 | // if the anchor is less than the lowest value of the bounding boxes
8 | // or we haven't set a value yet
9 | if (!bb.x1 || x < bb.x1) {
10 | // this anchor defines the lower bound of the bounding box
11 | bb.x1 = x
12 | }
13 | // and so on...
14 | if (!bb.x2 || x > bb.x2) {
15 | bb.x2 = x
16 | }
17 | if (!bb.y1 || y < bb.y1) {
18 | bb.y1 = y
19 | }
20 | if (!bb.y2 || y > bb.y2) {
21 | bb.y2 = y
22 | }
23 | }
24 |
25 | // add a padding to the bounding box
26 | bb.x1 -= padding
27 | bb.y1 -= padding
28 | bb.x2 += padding
29 | bb.y2 += padding
30 |
31 | return {
32 | ...bb,
33 | width: Math.abs(bb.x2 - bb.x1),
34 | height: Math.abs(bb.y2 - bb.y1),
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/client/interface/Title/tests/title.test.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | import { Provider } from 'react-redux'
4 | import { mount } from 'enzyme'
5 | // local imports
6 | import Title from '..'
7 | import { createStore } from 'store'
8 | import { setDiagramTitle } from 'actions/info'
9 |
10 | describe('Interface Components', () => {
11 | describe('Title Component', () => {
12 | test('shows the title of the diagram', () => {
13 | // create a store to test with
14 | const store = createStore()
15 | // the title of the diagram
16 | const title = 'test title'
17 |
18 | // set the title of the store
19 | store.dispatch(setDiagramTitle(title))
20 |
21 | // render the component
22 | const wrapper = mount(
23 |
24 |
25 |
26 | )
27 |
28 | // make sure there is a title element with the correct text
29 | expect(wrapper.containsMatchingElement({title} )).toBeTruthy()
30 | })
31 | })
32 | })
33 |
--------------------------------------------------------------------------------
/client/store/index.js:
--------------------------------------------------------------------------------
1 | // externals
2 | import { createStore as createReduxStore, combineReducers, compose, applyMiddleware } from 'redux'
3 | import { responsiveStoreEnhancer, calculateResponsiveState } from 'redux-responsive'
4 | import createSagaMiddleware from 'redux-saga'
5 | import thunk from 'redux-thunk'
6 | // locals
7 | import browser from './browser'
8 | import diagram from './diagram'
9 | import sagas from 'sagas'
10 |
11 | // create the reducer
12 | const reducer = combineReducers({
13 | diagram,
14 | browser,
15 | })
16 |
17 | // the saga middleware
18 | const sagaMiddleware = createSagaMiddleware()
19 |
20 | export const createStore = () =>
21 | createReduxStore(reducer, compose(applyMiddleware(sagaMiddleware, thunk), responsiveStoreEnhancer))
22 | // create the store from the reducer
23 | const store = createStore()
24 |
25 | // attach the saga middleware to the availible sagas
26 | sagaMiddleware.run(sagas)
27 |
28 | // make sure we track the window as it changes size (so the grid can grow/shrink)
29 | window.addEventListener('resize', () =>
30 | // update the redux store
31 | store.dispatch(calculateResponsiveState(window))
32 | )
33 |
34 | export default store
35 |
--------------------------------------------------------------------------------
/client/actions/elements/types.js:
--------------------------------------------------------------------------------
1 | export const ADD_PROPAGATORS = '@feynman/ADD_PROPAGATORS'
2 | export const ADD_ANCHORS = '@feynman/ADD_ANCHORS'
3 | export const ADD_ELEMENTS = '@feynman/ADD_ELEMENTS'
4 | export const SET_ANCHOR_LOCATIONS = '@feynman/SET_ANCHOR_LOCATIONS'
5 | export const SELECT_ELEMENTS = '@feynman/SELECT_ELEMENTS'
6 | export const CLEAR_SELECTION = '@feynman/CLEAR_SELECTION'
7 | export const MERGE_ELEMENTS = '@feynman/MERGE_ELEMENTS'
8 | export const SET_ELEMENT_ATTRS = '@feynman/SET_ELEMENT_ATTRS'
9 | export const DELETE_ELEMENTS = '@feynman/DELETE_ELEMENTS'
10 | export const MOVE_SELECTED_ELEMENTS = '@feynman/MOVE_SELECTED_ELEMENTS'
11 | export const ALIGN_SELECTED_ANCHORS = '@feynman/ALIGN_SELECTED_ANCHORS'
12 | export const CLEAR_ELEMENTS = '@feynman/CLEAR_ELEMENTS'
13 | export const DELETE_SELECTION = '@feynman/DELETE_SELECTION'
14 | export const PLACE_ELEMENTS = '@feynman/PLACE_ELEMENTS'
15 | export const LOAD_PATTERN = '@feyman/LOAD_PATTERN'
16 | export const SPLIT_ELEMENT = '@feynman/SPLIT_ELEMENT'
17 | export const SNAP_SELECTED_ELEMENTS = '@feynman/SNAP_SELECTED_ELEMENTS'
18 | export const EXPORT_DIAGRAM = '@feynman/EXPORT_DIAGRAM'
19 | export const LOAD_DIAGRAM = '@feynman/LOAD_DIAGRAM'
20 |
--------------------------------------------------------------------------------
/client/interface/Diagram/Propagator/Align.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | import SvgMatrix from 'svg-matrix'
4 | // local imports
5 | import Fermion from 'interface/Diagram/Propagator/Fermion'
6 |
7 | const Align = ({ x1, y1, x2, y2, length, element, degrade, ...unusedProps }) => {
8 | const dx = x2 - x1
9 | const dy = y2 - y1
10 | const actual = Math.sqrt(dx * dx + dy * dy)
11 |
12 | // if there is no length
13 | if (actual === 0) {
14 | // dont render anything
15 | return null
16 | }
17 | // if we are told to show something, but round to zero
18 | if (degrade) {
19 | // render a fermion (this is a little misleading)
20 | return
21 | }
22 |
23 | // compute the angle we need to rotate the propagator to line up correctly
24 | const angle = Math.atan2(dy, dx) * 180 / Math.PI
25 |
26 | // the transform matrix to line the propagator up to the anchors
27 | const scaleFactor = actual / length
28 | const transform = SvgMatrix()
29 | .rotate(angle, x1, y1)
30 | .scale(scaleFactor, scaleFactor, x1, y1)
31 | return
32 | }
33 |
34 | export default Align
35 |
--------------------------------------------------------------------------------
/client/sagas/placeElements/tests/placeText.test.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import { put } from 'redux-saga/effects'
3 | // local imports
4 | import { addElements } from 'actions/elements'
5 | import placeText from '../placeText'
6 |
7 | describe('Sagas', () => {
8 | describe('Place Elements', () => {
9 | describe('Text', () => {
10 | test('can place a text element', () => {
11 | // the description of the anchor to create
12 | const desc = {
13 | id: 1,
14 | x: 75,
15 | y: 80,
16 | value: 'hello',
17 | }
18 |
19 | // get the generator
20 | const gen = placeText(desc)
21 | // the only thing we have to do is create the anchor
22 | expect(gen.next().value).toEqual(
23 | put(
24 | addElements({
25 | type: 'text',
26 | ...desc,
27 | })
28 | )
29 | )
30 |
31 | // make sure there isn't anything left
32 | expect(gen.next().done).toBeTruthy()
33 | })
34 | })
35 | })
36 | })
37 |
--------------------------------------------------------------------------------
/client/sagas/placeElements/tests/placeShapes.test.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import { put } from 'redux-saga/effects'
3 | // local imports
4 | import { addElements } from 'actions/elements'
5 | import placeShapes from '../placeShapes'
6 |
7 | describe('Sagas', () => {
8 | describe('Place Elements', () => {
9 | describe('Shape', () => {
10 | test('can place a shape element', () => {
11 | // the description of the anchor to create
12 | const desc = {
13 | id: 1,
14 | x: 75,
15 | y: 80,
16 | kind: 'parton',
17 | }
18 |
19 | // get the generator
20 | const gen = placeShapes(desc)
21 | // the only thing we have to do is create the anchor
22 | expect(gen.next().value).toEqual(
23 | put(
24 | addElements({
25 | type: 'shapes',
26 | ...desc,
27 | })
28 | )
29 | )
30 |
31 | // make sure there isn't anything left
32 | expect(gen.next().done).toBeTruthy()
33 | })
34 | })
35 | })
36 | })
37 |
--------------------------------------------------------------------------------
/client/sagas/loadDiagram/test.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import { put } from 'redux-saga/effects'
3 | // local imports
4 | import { loadDiagramWorker } from '.'
5 | import { loadDiagram, loadPattern } from 'actions/elements'
6 | import { setDiagramTitle } from 'actions/info'
7 |
8 | describe('Sagas', () => {
9 | describe('Load Diagram', () => {
10 | test('loads elements over the current state', () => {
11 | // the description of the anchor to create
12 | const desc = {
13 | elements: {
14 | anchors: {
15 | 1: 'hello',
16 | },
17 | },
18 | title: 'hello',
19 | }
20 |
21 | // get the generator
22 | const gen = loadDiagramWorker(loadDiagram(desc))
23 |
24 | // the first thing to do is clear all visible elements
25 | expect(gen.next().value).toEqual(put(loadPattern({ elements: { type: 'pattern', anchors: ['hello'] } })))
26 |
27 | // then we need to set the title of the diagram
28 | expect(gen.next().value).toEqual(put(setDiagramTitle(desc.title)))
29 |
30 | // we should be finished
31 | expect(gen.next().done).toBeTruthy()
32 | })
33 | })
34 | })
35 |
--------------------------------------------------------------------------------
/client/utils/generateElementId.js:
--------------------------------------------------------------------------------
1 | import range from './range'
2 |
3 | // this function returns a unique id for an anchor from the given store
4 | const generateElementId = anchors => {
5 | // create our first candidate
6 | let candidate = Math.floor(Math.random() * 10000)
7 | // while there is still a conflict
8 | while (anchors[candidate]) {
9 | // generate another random id
10 | candidate = Math.floor(Math.random() * 10000)
11 | }
12 | // the candidate is unique
13 |
14 | // return the candidate
15 | return candidate
16 | }
17 |
18 | const generateMultipleElementIds = (anchors, n = 1) => {
19 | // the list of ids
20 | const ids = []
21 |
22 | // generate the right number of ids
23 | for (const i of range(n)) {
24 | // create a candidate
25 | let candidate = generateElementId(anchors)
26 | // while there is a conflict with ids we've already generated
27 | while (ids.includes(candidate)) {
28 | // generate a new id
29 | candidate = generateElementId(anchors)
30 | }
31 | // the candidate is garunteed unique, add it to the list
32 | ids.push(candidate)
33 | }
34 |
35 | return n === 1 ? ids[0] : ids
36 | }
37 |
38 | export default generateMultipleElementIds
39 |
--------------------------------------------------------------------------------
/client/utils/tests/relativePosition.test.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import relativePosition from '../relativePosition'
3 | import { sidebarWidth } from 'interface/Sidebar/styles'
4 |
5 | describe('Utils', () => {
6 | describe('Relative Position Util', () => {
7 | test('Can correct coordinates', () => {
8 | const info = { pan: { x: 0, y: 0 } }
9 | // for now, the only difference between absolute and diagram coordinates
10 | // is the sidebar width
11 | expect(relativePosition({ x: 50, y: 100 }, info)).toEqual({
12 | x: 50 - sidebarWidth,
13 | y: 100,
14 | })
15 | })
16 |
17 | test('incorporate diagram pan in relative calculation', () => {
18 | const info = {
19 | // the mock pan
20 | pan: {
21 | x: 10,
22 | y: 100,
23 | },
24 | }
25 | // for now, the only difference between absolute and diagram coordinates
26 | // is the sidebar width
27 | expect(relativePosition({ x: 50, y: 100 }, info)).toEqual({
28 | x: 50 - sidebarWidth - info.pan.x,
29 | y: 100 - info.pan.y,
30 | })
31 | })
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/config/projectPaths.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Provides a single, consistent place for js files to get
3 | * relevant paths, globs, etc pertaining to the project structure.
4 | */
5 |
6 | // node imports
7 | var path = require('path')
8 |
9 | var rootDir = path.join(__dirname, '..')
10 | var configDir = path.join(rootDir, 'config')
11 | var buildDir = path.join(rootDir, 'build')
12 | var sourceDir = path.join(rootDir, 'client')
13 | var clientEntry = path.join(sourceDir, 'index.js')
14 | var clientBuild = path.join(buildDir, 'client.js')
15 | var indexTemplate = path.join(sourceDir, 'index.html')
16 |
17 | module.exports = {
18 | // directories
19 | rootDir: rootDir,
20 | sourceDir: sourceDir,
21 | buildDir: buildDir,
22 | // entry points
23 | clientEntry: clientEntry,
24 | // built files
25 | clientBuild: clientBuild,
26 | indexTemplate: indexTemplate,
27 | // globs
28 | clientBuildGlob: path.join(clientBuild, '*'),
29 | testGlob: path.join(sourceDir, "**", "*test.js"),
30 | // configuration files
31 | eslintConfig: path.join(configDir, 'eslint.js'),
32 | karmaConfig: path.join(configDir, 'karma.js'),
33 | webpackConfig: path.join(configDir, 'webpack.js'),
34 | babelConfig: path.join(configDir, 'babel.json'),
35 | tsConfig: path.join(configDir, 'tsconfig.json'),
36 | }
37 |
--------------------------------------------------------------------------------
/client/interface/Sidebar/HistorySummary/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | import { connect } from 'react-redux'
4 | // local imports
5 | import { Collapsible } from 'components'
6 | import { toggleHistory } from 'actions/info'
7 | import { goto } from 'actions/history'
8 | import styles from './styles'
9 | import Entry from './Entry'
10 |
11 | const HistorySummary = ({ info, history, toggle, goTo, style }) => (
12 |
13 |
14 | {history.log.map((entry, index) => (
15 | goTo(index)}
17 | active={index === history.head}
18 | length={history.log.length}
19 | index={index}
20 | key={index}
21 | >
22 | {entry}
23 |
24 | ))}
25 |
26 |
27 | )
28 |
29 | const selector = ({ diagram: { info, elements: { history } } }) => ({ info, history })
30 | const mapDispatchToProps = dispatch => ({
31 | toggle: () => dispatch(toggleHistory()),
32 | goTo: i => dispatch(goto(i)),
33 | })
34 | export default connect(selector, mapDispatchToProps)(HistorySummary)
35 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/SelectionSummary/PropagatorSummary/GluonSummary/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import { Button, ToggleButton } from 'components'
5 | import { ButtonRow, MultiRow } from '../..'
6 | import styles from './styles'
7 | import Gluon from 'interface/Diagram/Propagator/Gluon'
8 |
9 | const defaults = Gluon.defaultProps
10 |
11 | const GluonSummary = ({
12 | setAttrs,
13 | commit,
14 | endcaps = defaults.endcaps,
15 | direction = defaults.direction,
16 | ...unusedProps
17 | }) => (
18 |
19 |
20 | setAttrs({ direction: -direction }, 'inverted loop direction')}
23 | >
24 | Invert Loop Direction
25 |
26 |
27 |
28 | setAttrs({ endcaps: !endcaps }, `made gluon endcaps ${endcaps ? 'visible' : 'hidden'}`)}
31 | active={endcaps}
32 | activeText="Hide End Caps"
33 | inactiveText="Draw End Caps"
34 | />
35 |
36 |
37 | )
38 |
39 | export default GluonSummary
40 |
--------------------------------------------------------------------------------
/client/interface/Diagram/SelectionRectangle/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | import { connect } from 'react-redux'
4 | // local imports
5 | import styles from './styles'
6 |
7 | const SelectionRectangle = ({ point1, point2 }) => {
8 | // compute the dimentions of the rectangle
9 | const width = Math.abs(point2.x - point1.x)
10 | const height = Math.abs(point2.y - point1.y)
11 |
12 | // figure out the transform to align the rectangle with the user's mouse
13 | const transform = [1, 0, 0, 1, 0, 0]
14 |
15 | // if the origin is to the right of the mouse
16 | if (point1.x > point2.x) {
17 | // we need to move the rectangle to the left by its width
18 | transform[4] = -width
19 | }
20 |
21 | // if the origin is above the rectangle
22 | if (point1.y > point2.y) {
23 | // we need to translate the rectangle down by its height
24 | transform[5] = -height
25 | }
26 |
27 | return (
28 |
36 | )
37 | }
38 |
39 | const selector = ({ info }) => ({ info })
40 | export default SelectionRectangle
41 |
--------------------------------------------------------------------------------
/client/sagas/loadDiagram/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import { takeEvery } from 'redux-saga'
3 | import { put } from 'redux-saga/effects'
4 | import _ from 'lodash'
5 | // local imports
6 | import { LOAD_DIAGRAM, loadPattern } from 'actions/elements'
7 | import { setDiagramTitle } from 'actions/info'
8 | import { commit } from 'actions/history'
9 |
10 | const convertObjectToArray = obj =>
11 | Object.keys(obj)
12 | .reduce((prev, key) => {
13 | const newList = [...prev]
14 | newList[key] = obj[key]
15 |
16 | return newList
17 | }, [])
18 | .filter(Boolean)
19 |
20 | export function* loadDiagramWorker({ type, payload: { elements, title } }) {
21 | const diagramElements = Object.keys(elements).reduce(
22 | (prev, key) => ({
23 | ...prev,
24 | [key]: convertObjectToArray(elements[key]),
25 | }),
26 | { type: 'pattern' }
27 | )
28 |
29 | // load the pattern specified in the diagram
30 | yield put(loadPattern({ elements: diagramElements }))
31 |
32 | // set the diagram title to match
33 | yield put(setDiagramTitle(title))
34 | }
35 |
36 | // this saga loads a particular diagram
37 | export default function* loadDiagram() {
38 | // whenever we want to load a Diagram onto the diagram
39 | yield takeEvery(LOAD_DIAGRAM, loadDiagramWorker)
40 | }
41 |
--------------------------------------------------------------------------------
/client/interface/Sidebar/GridSizeControl/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | import { connect } from 'react-redux'
4 | // local imports
5 | import styles from './styles'
6 | import { Label, Slider } from 'components'
7 | import { setGridSize } from 'actions/info'
8 |
9 | // this had to be a class-based component for slider (attaches a ref)
10 | class SliderHandle extends React.Component {
11 | render() {
12 | // pull out the used props
13 | const { offset } = this.props
14 |
15 | // render the component
16 | return (
17 |
18 |
19 |
20 | )
21 | }
22 | }
23 |
24 | const GridSizeControl = ({ info, dispatch, style, ...unusedProps }) => (
25 |
26 |
27 | grid
28 | {info.gridSize}
29 |
30 |
31 | dispatch(setGridSize(value))} />
32 |
33 |
34 | )
35 |
36 | const selector = ({ diagram: { info } }) => ({ info })
37 | export default connect(selector)(GridSizeControl)
38 |
--------------------------------------------------------------------------------
/client/actions/history/test.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { commit, COMMIT, goto, GOTO, undo, UNDO, redo, REDO, withCommit, WITH_COMMIT } from 'actions/history'
3 |
4 | describe('Action Creators', () => {
5 | describe('History', () => {
6 | test('can commit the store with a message', () => {
7 | expect(commit('hello world')).toEqual({
8 | type: COMMIT,
9 | payload: 'hello world',
10 | })
11 | })
12 |
13 | test('can goto a specific commit', () => {
14 | expect(goto(123)).toEqual({
15 | type: GOTO,
16 | payload: 123,
17 | })
18 | })
19 |
20 | test('can undo history', () => {
21 | expect(undo()).toEqual({
22 | type: UNDO,
23 | })
24 | })
25 |
26 | test('can redo history', () => {
27 | expect(redo()).toEqual({
28 | type: REDO,
29 | })
30 | })
31 |
32 | test('with commit', () => {
33 | // the action to dispatch
34 | const action = { hello: 'world' }
35 |
36 | expect(withCommit(action, 'msg')).toEqual({
37 | type: WITH_COMMIT,
38 | payload: {
39 | action,
40 | message: 'msg',
41 | },
42 | })
43 | })
44 | })
45 | })
46 |
--------------------------------------------------------------------------------
/client/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import * as React from "react"
3 | import * as ReactDOM from "react-dom"
4 | import { Provider } from 'react-redux'
5 | import 'babel-polyfill'
6 | // local imports
7 | import App from './interface'
8 | import store from './store'
9 |
10 | import { placeElement } from 'actions/elements'
11 | import { fieldName } from 'actions/info/creators/togglePatternModalInitialVis'
12 | import { togglePatternModal, TOGGLE_PATTERN_INITIAL_VIS } from 'actions/info'
13 |
14 | // check if the user has an opinion on the initial visibility of the pattern modal
15 | // parsing as if json because local storage only stores strings 🤦
16 | const showPatternModal = JSON.parse(window.localStorage.getItem(fieldName))
17 | if (showPatternModal !== null) {
18 | // the current state of the store
19 | const initialVisibility = store.getState().diagram.info.patternModalInitalVis
20 | // if the user wants something different than what the store thinks
21 | if (showPatternModal !== initialVisibility) {
22 | // invert the store's notion of the internal visibility
23 | store.dispatch({
24 | type: TOGGLE_PATTERN_INITIAL_VIS
25 | })
26 | // toggle the actual modal
27 | store.dispatch(togglePatternModal())
28 | }
29 | }
30 |
31 | ReactDOM.render((
32 |
33 |
34 |
35 | ), document.getElementById("app"))
36 |
--------------------------------------------------------------------------------
/client/interface/Sidebar/ZoomLevelControl/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | import { connect } from 'react-redux'
4 | // local imports
5 | import styles from './styles'
6 | import { Label, Slider } from 'components'
7 | import { setZoom } from 'actions/info'
8 |
9 | // this had to be a class-based component for slider (attaches a ref)
10 | class SliderHandle extends React.Component {
11 | render() {
12 | // pull out the used props
13 | const { offset } = this.props
14 |
15 | // render the component
16 | return (
17 |
18 |
19 |
20 | )
21 | }
22 | }
23 |
24 | const ZoomLevelControl = ({ info, dispatch, style, ...unusedProps }) => (
25 |
26 |
27 | zoom
28 | {info.zoomLevel.toFixed(1)}
29 |
30 |
31 | dispatch(setZoom(value))} />
32 |
33 |
34 | )
35 |
36 | const selector = ({ diagram: { info } }) => ({ info })
37 | export default connect(selector)(ZoomLevelControl)
38 |
--------------------------------------------------------------------------------
/client/interface/PatternModal/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | import { connect } from 'react-redux'
4 | // local imports
5 | import Choice from './Choice'
6 | import patterns from './patterns'
7 | import styles from './styles'
8 | import { Overlay, Checkbox } from 'components'
9 | import { togglePatternModal, togglePatternModalInitialVis } from 'actions/info'
10 |
11 | const Addon = ({ toggleInitialState, checked }) => (
12 |
13 | show at startup
14 |
15 |
16 | )
17 |
18 | const PatternModal = ({ info, hideOverlay, toggleInitialState }) => (
19 | }
24 | >
25 | {patterns.map((pattern, i) => )}
26 |
27 | )
28 |
29 | const selector = ({ diagram: { info } }) => ({ info })
30 | const mapDispatchToProps = dispatch => ({
31 | hideOverlay: () => dispatch(togglePatternModal()),
32 | toggleInitialState: () => dispatch(togglePatternModalInitialVis(window.localStorage)),
33 | })
34 | export default connect(selector, mapDispatchToProps)(PatternModal)
35 |
--------------------------------------------------------------------------------
/client/utils/constrainLocationToShape.js:
--------------------------------------------------------------------------------
1 | import { Shape } from 'interface/Diagram/Shape'
2 |
3 | const constrainLocationToShape = ({ location, shape }) => {
4 | // if we are constrained the location to a circular shape
5 | if (['parton'].includes(shape.kind)) {
6 | // pull out the shape attributes that we'll need
7 | const { r = Shape.defaultProps.r, x, y } = shape
8 | // compute the distance between the location and the shape
9 | const dx = location.x - x
10 | const dy = location.y - y
11 | // and the slow of the line joining the two
12 | const m = dy / dx
13 |
14 | // calculate the distance from the center of the shape
15 | const translate = {
16 | x: Math.sqrt(r * r / (1 + m * m)),
17 | y: m * Math.sqrt(r * r / (1 + m * m)),
18 | }
19 |
20 | // apply the right translations in the appropriate quadrant
21 | if (dx > 0) {
22 | return {
23 | x: x + translate.x,
24 | y: y + translate.y,
25 | }
26 | } else if (dx < 0) {
27 | return {
28 | x: x - translate.x,
29 | y: y - translate.y,
30 | }
31 | } else {
32 | return {
33 | x,
34 | y: dy > 0 ? y + r : y - r,
35 | }
36 | }
37 | }
38 | }
39 |
40 | export default constrainLocationToShape
41 |
--------------------------------------------------------------------------------
/client/components/Splittable/test.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | import { mount } from 'enzyme'
4 | import { Provider } from 'react-redux'
5 | // local imports
6 | import { createStore } from 'store'
7 | import Splittable from '../Splittable'
8 |
9 | describe('"Reusable" Components', () => {
10 | describe('Splittable', () => {
11 | test('renders its child', () => {
12 | // render a wrapped div
13 | const wrapper = mount(
14 |
15 |
16 | hello
17 |
18 |
19 | )
20 |
21 | // make sure there is a div in the view
22 | expect(wrapper.find('div')).toHaveLength(1)
23 | })
24 |
25 | test('barfs if there are multiple children', () => {
26 | // render a splittable around two divs
27 | const wrapper = () =>
28 | mount(
29 |
30 |
31 | hello
32 | hello
33 |
34 |
35 | )
36 |
37 | // make sure that fails
38 | expect(wrapper).toThrow(Error)
39 | })
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/SelectionSummary/PropagatorSummary/FermionSummary/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import styles from './styles'
5 | import { ButtonRow } from '../..'
6 | import { Button, ToggleButton } from 'components'
7 | import Fermion from 'interface/Diagram/Propagator/Fermion'
8 |
9 | const FermionSummary = ({
10 | setAttrs,
11 | arrow = Fermion.defaultProps.arrow,
12 | style,
13 | anchor1,
14 | anchor2,
15 | labelDistance,
16 | labelLocation,
17 | commit,
18 | ...unusedProps
19 | }) =>
20 | arrow !== 0 ? (
21 |
22 | setAttrs({ arrow: 0 }, 'made propagator arrow hidden')}>
23 | Hide Arrow
24 |
25 | setAttrs({ arrow: 0 - arrow }, 'flipped propagator arrow direction')}
28 | >
29 | Flip
30 |
31 |
32 | ) : (
33 |
34 | setAttrs({ arrow: 1 }, 'made propagator arrow visible')} style={styles.showButton}>
35 | Show Arrow
36 |
37 |
38 | )
39 |
40 | export default FermionSummary
41 |
--------------------------------------------------------------------------------
/client/interface/Diagram/Propagator/Label/locationForLabel.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { defaultProps } from '..'
3 |
4 | export default function locationForLabel({
5 | x1,
6 | x2,
7 | y1,
8 | y2,
9 | distance: r = defaultProps.labelDistance,
10 | location: s = defaultProps.labelLocation,
11 | label,
12 | }) {
13 | // the midpoint
14 | const midx = x1 + s * (x2 - x1)
15 | const midy = y1 + s * (y2 - y1)
16 |
17 | // the slope of the perpendicular line
18 | // m` = -1 / m
19 | const m = -(x1 - x2) / (y1 - y2)
20 |
21 | // the points that are perpedicular to the line a distance r away satisfy
22 | // the following system of equations:
23 | //
24 | // y = mx
25 | // x^2 + y^2 = r
26 | //
27 | const x = Math.sqrt(r * r / (1 + m * m))
28 | const y = m * x
29 |
30 | // the multiplier for distance away from line
31 | const rSign = r > 0 ? 1 : -1
32 | const mSign = m > 0 ? 1 : -1
33 |
34 | // add these values to the mid points to displace the label
35 | // WARNING: these signs take into account the possible minus sign from midx/y calculation
36 | const labelx = midx - mSign * rSign * x
37 | let labely = midy - mSign * rSign * y
38 |
39 | // check against horizontal lines
40 | if (isNaN(labely)) {
41 | labely = midy - r
42 | }
43 |
44 | // return the label coordinates
45 | return {
46 | x: labelx,
47 | y: labely,
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/client/actions/elements/tests/propagators.test.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { addPropagators, ADD_PROPAGATORS } from '..'
3 |
4 | describe('Action Creators', () => {
5 | describe('Elements', () => {
6 | describe('Propagators', () => {
7 | test('add single propagator', () => {
8 | // the configuration for the propagator
9 | const propagator = {
10 | anchor1: 1,
11 | anchor2: 2,
12 | }
13 |
14 | // make sure the action reflects the expectation
15 | expect(addPropagators(propagator)).toEqual({
16 | type: ADD_PROPAGATORS,
17 | payload: [propagator],
18 | })
19 | })
20 |
21 | test('add multiple propagators', () => {
22 | // the configuration for the propagator
23 | const propagators = [
24 | {
25 | anchor1: 1,
26 | anchor2: 2,
27 | },
28 | {
29 | anchor1: 1,
30 | anchor2: 2,
31 | },
32 | ]
33 |
34 | // make sure the action reflects the expectation
35 | expect(addPropagators(...propagators)).toEqual({
36 | type: ADD_PROPAGATORS,
37 | payload: propagators,
38 | })
39 | })
40 | })
41 | })
42 | })
43 |
--------------------------------------------------------------------------------
/client/interface/PatternModal/Choice/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | import { BooleanState } from 'quark-web'
4 | import { connect } from 'react-redux'
5 | // local imports
6 | import { loadPattern } from 'actions/elements'
7 | import { commit } from 'actions/history'
8 | import styles from './styles'
9 |
10 | const PatternChoice = ({ load, hideOverlay, name, elements, commitWithMsg, preview: Preview }) => (
11 |
12 | {({ state, toggle }) => (
13 | {
18 | // load the associated pattern
19 | load({ name, elements })
20 | // commit the state after loading the pattern
21 | commitWithMsg(`rendered canvas with ${name} pattern`)
22 | // hide the overlay
23 | hideOverlay()
24 | }}
25 | >
26 |
{name}
27 |
28 |
29 | )}
30 |
31 | )
32 |
33 | const mapDispatchToProps = dispatch => ({
34 | load: pattern => dispatch(loadPattern(pattern)),
35 | commitWithMsg: msg => dispatch(commit(msg)),
36 | })
37 |
38 | export default connect(
39 | null,
40 | mapDispatchToProps
41 | )(PatternChoice)
42 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Draw Feynman Diagram Online
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/client/actions/info/storage.test.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import Storage from './storage'
3 |
4 | describe('Action Creators', () => {
5 | describe('Info', () => {
6 | describe('Storage mock', () => {
7 | test('can set field', () => {
8 | // instantiate a storage object to test
9 | const storage = new Storage()
10 | // update a field
11 | storage.setItem('hello', 'world')
12 | // make sure we updated the internal data structure
13 | expect(storage.value.hello).toEqual('world')
14 | })
15 |
16 | test('can retrieve fields', () => {
17 | // instantiate a storage object to test
18 | const storage = new Storage()
19 | // update an internal field
20 | storage.value.hello = 'world'
21 | // make sure we updated the internal data structure
22 | expect(storage.getItem('hello')).toEqual('world')
23 | })
24 |
25 | test('can be cleared', () => {
26 | // instantiate a storage object to test
27 | const storage = new Storage()
28 | // update the internal data structure
29 | storage.value.hello = 'world'
30 | // clear the storage object
31 | storage.clear()
32 | // make sure the field updated is empty
33 | expect(storage.getItem('hello')).not.toBeDefined()
34 | })
35 | })
36 | })
37 | })
38 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/ItemPalette/styles.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { itsGrey } from 'colors'
3 |
4 | export const image = {
5 | userDrag: 'none',
6 | userSelect: 'none',
7 | MozUserSelect: 'none',
8 | WebkitUserDrag: 'none',
9 | WebkitUserSelect: 'none',
10 | MsUserSelect: 'none',
11 | }
12 |
13 | export default {
14 | container: {
15 | display: 'flex',
16 | flexDirection: 'column',
17 | alignItems: 'center',
18 | padding: 25,
19 | },
20 | header: {
21 | textTransform: 'uppercase',
22 | color: 'white',
23 | fontSize: 12,
24 | marginBottom: 10,
25 | },
26 | subHeader: {
27 | color: itsGrey,
28 | fontSize: 12,
29 | },
30 | palette: {
31 | marginTop: 10,
32 | display: 'flex',
33 | flexDirection: 'row',
34 | flexWrap: 'wrap',
35 | justifyContent: 'space-between',
36 | },
37 | paletteItem: {
38 | display: 'flex',
39 | justifyContent: 'center',
40 | alignItems: 'center',
41 | cursor: 'pointer',
42 | height: 75,
43 | width: 75,
44 | background:
45 | 'rgba(137,137,137,0.8) linear-gradient(180deg, rgba(153,153,153,0.8), rgba(137,137,137,0.8)) repeat-x',
46 | boxShadow: 'inset 1px 1px 5px 0px rgba(204,204,204,0.2), 0px 2px 1px 0px rgba(50,50,50,0.3)',
47 | marginBottom: 25,
48 | },
49 | image,
50 | patternButtonContainer: {
51 | width: '100%',
52 | marginBottom: 10,
53 | },
54 | patternButton: {
55 | width: '100%',
56 | },
57 | }
58 |
--------------------------------------------------------------------------------
/client/components/ColorPicker/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | import { TwitterPicker } from 'react-color'
4 | import { BooleanState } from 'quark-web'
5 | // local imports
6 | import styles from './styles'
7 |
8 | // the color options
9 | export const colors = [
10 | '#ff6900',
11 | '#fcb900',
12 | '#7bdcb5',
13 | '#00d084',
14 | '#8ed1fc',
15 | '#0693e3',
16 | '#abb8C3',
17 | '#eb144c',
18 | '#f78da7',
19 | '#9900ef',
20 | ]
21 |
22 | const ColorPicker = ({ style, color, onChange, state: active, toggle, ...unusedProps }) => (
23 |
24 | {({ state: active, toggle }) => (
25 |
26 |
31 | {active && (
32 |
33 | onChange(hex)}
39 | onChangeComplete={toggle}
40 | />
41 |
42 | )}
43 |
44 | )}
45 |
46 | )
47 |
48 | export const Picker = TwitterPicker
49 |
50 | export default ColorPicker
51 |
--------------------------------------------------------------------------------
/client/sagas/splitElement/anchor.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import { select, put } from 'redux-saga/effects'
3 | // local imports
4 | import { addAnchors, addPropagators, selectElements } from 'actions/elements'
5 | import { generateElementId, fixPositionToGrid } from 'utils'
6 |
7 | export default function* splitAnchor({ element, location }) {
8 | // get the current state so we can create some IDs
9 | const state = (yield select(({ diagram: { elements, info } }) => ({ elements, info }))) || {
10 | elements: {
11 | anchors: {},
12 | propagators: {},
13 | },
14 | info: {
15 | gridSize: 50,
16 | },
17 | }
18 |
19 | // we are going to create a new anchor connected to the origin
20 |
21 | // first, we need an id for the anchor
22 | const newAnchorId = generateElementId(state.elements.anchors)
23 |
24 | // figure out the current location for the anchor
25 | const pos = fixPositionToGrid(location, state.info.gridSize)
26 |
27 | // create the new anchor
28 | yield put(
29 | addAnchors({
30 | id: newAnchorId,
31 | ...pos,
32 | })
33 | )
34 |
35 | // create a propagator linking the two anchors
36 | yield put(
37 | addPropagators({
38 | kind: 'fermion',
39 | id: generateElementId(state.elements.propagators),
40 | anchor1: element.id,
41 | anchor2: newAnchorId,
42 | })
43 | )
44 |
45 | // select the anchor we just made
46 | yield put(
47 | selectElements({
48 | type: 'anchors',
49 | id: newAnchorId,
50 | })
51 | )
52 | }
53 |
--------------------------------------------------------------------------------
/client/store/diagram/elements/selection.js:
--------------------------------------------------------------------------------
1 | // exteranl imports
2 | import _ from 'lodash'
3 | // local imports
4 | import { SELECT_ELEMENTS, CLEAR_SELECTION, DELETE_SELECTION } from 'actions/elements'
5 | import { flatMap } from 'utils'
6 |
7 | export const noIdErr = 'cannot set location of anchor without explicit id'
8 |
9 | export const initialState = {
10 | anchors: [],
11 | propagators: [],
12 | text: [],
13 | shapes: [],
14 | }
15 |
16 | // the reducer slice that manages just the selection state but has reference
17 | // to the entire element state reducer (must return just the propagator slice)
18 | export default (state = initialState, { type, payload }) => {
19 | // if the payload represents a new selection
20 | if (type === SELECT_ELEMENTS) {
21 | // the payload is a list of objects with id and type fields
22 | return _.chain(payload)
23 | .cloneDeep()
24 | .map(selected => {
25 | // if there is no element by that type and id
26 | if (!state[selected.type][selected.id]) {
27 | throw new Error(`Could not find ${selected.type} with id ${selected.id}`)
28 | }
29 | // pass this item through
30 | return selected
31 | })
32 | .groupBy('type')
33 | .mapValues(val => val.map(({ id }) => id))
34 | .value()
35 | }
36 |
37 | // if the action indicates we need to clear the selection
38 | if (type === CLEAR_SELECTION) {
39 | // return an empty state
40 | return _.cloneDeep(initialState)
41 | }
42 |
43 | // return the previous selection
44 | return state.selection
45 | }
46 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/utils/tests/anchorsInSpec.test.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { anchorsInSpec } from '..'
3 | import * as specMap from '../../specs'
4 |
5 | describe('Utils', () => {
6 | describe('AnchorsInSpec util', () => {
7 | test('gracefully handles null spec', () => {
8 | // create an invalid spec
9 | const spec = null
10 | // searching through this spec should generate an error
11 | expect(anchorsInSpec(spec)).toEqual([])
12 | })
13 |
14 | test('returns the anchors in a propagator spec', () => {
15 | // create a propagator spec
16 | const spec = specMap.propagators({
17 | x: 50,
18 | y: 100,
19 | info: { gridSize: 50 },
20 | elements: { anchors: {}, propagators: {} },
21 | config: {},
22 | })
23 | // look for the anchors in the spec
24 | const anchors = anchorsInSpec(spec)
25 |
26 | // there should be 2 anchors for any propagator
27 | expect(anchors).toHaveLength(2)
28 | })
29 |
30 | test('handles text types', () => {
31 | // the spec for text
32 | const spec = { element: { type: 'text', value: 'hello', x: 50, y: 1000 } }
33 | // there are no anchors in text
34 | expect(anchorsInSpec(spec)).toHaveLength(0)
35 | })
36 |
37 | test('handles shapes types', () => {
38 | // the spec for text
39 | const spec = { element: { type: 'shapes', kind: 'parton', x: 50, y: 1000 } }
40 | // there are no anchors in text
41 | expect(anchorsInSpec(spec)).toHaveLength(0)
42 | })
43 | })
44 | })
45 |
--------------------------------------------------------------------------------
/client/interface/Diagram/Propagator/Fermion/Arrow.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import styles from './styles'
5 |
6 | const FermionArrow = ({ x1, y1, x2, y2, strokeWidth, stroke, flip = false, selected, ...unusedProps }) => {
7 | // some points in space
8 | const x = (x1 + x2) / 2
9 | const y = (y1 + y2) / 2
10 | let dx = x1 - x2
11 | let dy = y1 - y2
12 |
13 | // make sure there is a line to show
14 | if (dx === 0 && dy === 0) {
15 | return null
16 | }
17 |
18 | // compute some needed lengths
19 | const A = 6 * strokeWidth / 1.4 // a dimensionless quantity to scale the arrow by
20 | const halfHeight = A * Math.sqrt(3) / 2
21 | const halfBase = A * 3 / 4
22 |
23 | // the angle the line forces
24 | let angle = Math.atan(dy / dx) * 180 / Math.PI
25 |
26 | // if we need to flip the arrow for consistency
27 | if (dx >= 0) {
28 | // then do so
29 | angle += 180
30 | }
31 |
32 | // add a little bit to accomodate the triangle's original orientation
33 | angle += 90
34 |
35 | // if we are supposed to flip the original direction
36 | if (flip) {
37 | angle += 180
38 | }
39 |
40 | // the styling we use depends on wether or not the fermion is selected
41 | const styling = selected ? styles.selected : {}
42 |
43 | // render the arrow
44 | return (
45 |
52 | )
53 | }
54 |
55 | export default FermionArrow
56 |
--------------------------------------------------------------------------------
/client/components/ColorPicker/test.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | import { mount } from 'enzyme'
4 | // local imports
5 | import ColorPicker, { Picker, colors } from '.'
6 |
7 | describe('"Reusable" Components', () => {
8 | describe('Color picker', () => {
9 | test('starts off hidden', () => {
10 | // render the color picker
11 | const wrapper = mount( )
12 |
13 | // make sure there is no Picker visible
14 | expect(wrapper.find(Picker)).toHaveLength(0)
15 | })
16 |
17 | test('becomes visible when the user clicks on the thumbnail', () => {
18 | // render the color picker
19 | const wrapper = mount( )
20 |
21 | // click on the thumbnail
22 | wrapper.find('.colorThumbnail').simulate('click')
23 |
24 | // make sure there is no Picker visible
25 | expect(wrapper.find(Picker)).toHaveLength(1)
26 | })
27 |
28 | test('passes the correct value to the onChange handler', () => {
29 | // the color to test against
30 | const color = colors[0]
31 |
32 | // something to inspect what's passed to the onChange handler
33 | const spy = jest.fn()
34 |
35 | // render the color picker
36 | const wrapper = mount( )
37 |
38 | // click on the thumbnail to show the swatches
39 | wrapper.find('.colorThumbnail').simulate('click')
40 | // click on the first swatch to select its color
41 | wrapper
42 | .find('Swatch')
43 | .at(0)
44 | .simulate('click')
45 |
46 | // make sure the spy was correctly called
47 | expect(spy).toHaveBeenCalledWith(color)
48 | })
49 | })
50 | })
51 |
--------------------------------------------------------------------------------
/client/sagas/splitElement/shape.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import { select, put } from 'redux-saga/effects'
3 | // local imports
4 | import { addElements, selectElements } from 'actions/elements'
5 | import { constrainLocationToShape, generateElementId } from 'utils'
6 |
7 | export default function* splitShape({ element, location }) {
8 | // the current state
9 | const state = (yield select(state => state.diagram.elements)) || {
10 | anchors: {},
11 | propagators: {},
12 | }
13 |
14 | // compute the constrained location
15 | const constrained = constrainLocationToShape({ shape: element, location })
16 |
17 | // generate 2 anchor ids
18 | const [anchor1, anchor2] = generateElementId(state.anchors, 2)
19 |
20 | // and a propagator id
21 | const propagator = generateElementId(state.propagators)
22 |
23 | // if there isn't an id of the shape
24 | if (!element.id) {
25 | throw new Error('Could not find id of shape to split')
26 | }
27 |
28 | // the first thing we have to do is create 2 anchors
29 | yield put(
30 | addElements(
31 | {
32 | type: 'anchors',
33 | id: anchor1,
34 | ...constrained,
35 | constraint: element.id,
36 | },
37 | {
38 | type: 'anchors',
39 | id: anchor2,
40 | ...constrained,
41 | }
42 | )
43 | )
44 |
45 | // create a propagator between the two anchors
46 | yield put(
47 | addElements({
48 | type: 'propagators',
49 | kind: 'fermion',
50 | id: propagator,
51 | anchor1,
52 | anchor2,
53 | })
54 | )
55 |
56 | // when we're done, select the anchor we split off of the shape
57 | yield put(selectElements({ type: 'anchors', id: anchor2 }))
58 | }
59 |
--------------------------------------------------------------------------------
/client/interface/Diagram/Propagator/Label/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | import autobind from 'autobind-decorator'
4 | import { connect } from 'react-redux'
5 | // local imports
6 | import { Text, MouseMove } from 'components'
7 | import { relativePosition } from 'utils'
8 | import { setElementAttrs } from 'actions/elements'
9 | import relPosition from './relLocForLabel'
10 | import locationForLabel from './locationForLabel'
11 |
12 | class PropagatorLabel extends React.Component {
13 | render() {
14 | // grab the used props
15 | const { children, id, location, distance, element } = this.props
16 |
17 | // compute the location for the labels
18 | const { x, y } = locationForLabel({ ...element, location, distance })
19 |
20 | return (
21 |
22 |
23 | {children}
24 |
25 |
26 | )
27 | }
28 |
29 | @autobind
30 | _mouseMove({ next }) {
31 | // this is called whenever the label is being dragged
32 | // we need to convert the mouse's location in diagram space
33 | const mouse = relativePosition(next, this.props.info)
34 | // compute the new relative location for the label
35 | const labelLoc = relPosition(mouse, this.props.element)
36 | // update the locations of the label
37 | this.props.setAttrs(labelLoc)
38 | }
39 | }
40 |
41 | const selector = ({ diagram: { info } }) => ({ info })
42 | const mapDispatchToProps = (dispatch, { element }) => ({
43 | setAttrs: attrs =>
44 | dispatch(
45 | setElementAttrs({
46 | type: 'propagators',
47 | id: element.id,
48 | ...attrs,
49 | })
50 | ),
51 | })
52 |
53 | export default connect(selector, mapDispatchToProps)(PropagatorLabel)
54 |
--------------------------------------------------------------------------------
/client/utils/elementsWithLocations.js:
--------------------------------------------------------------------------------
1 | import constrainLocationToShape from './constrainLocationToShape'
2 |
3 | // elementsWithLocations returns the list of elements configuration
4 | // with concrete (dereferenced) values. This means that first anchors are
5 | // constrained to any shapes, and then propagators are provided locations
6 | // between the appropriate anchors
7 | const elementsWithLocations = elements => {
8 | // the anchors in our diagram are potentially constrained
9 | const anchors = Object.values(elements.anchors).reduce((prev, next) => {
10 | // the function to apply to the constraint
11 | let coordinates = { x: next.x, y: next.y }
12 |
13 | // if the anchor is constrained to a shape
14 | if (next.constraint) {
15 | // use the constrained location instead
16 | coordinates = constrainLocationToShape({
17 | location: coordinates,
18 | shape: elements.shapes[next.constraint],
19 | })
20 | }
21 |
22 | return {
23 | ...prev,
24 | [next.id]: {
25 | ...next,
26 | ...coordinates,
27 | },
28 | }
29 | }, {})
30 |
31 | return {
32 | anchors: Object.values(anchors),
33 | propagators: Object.values(elements.propagators).map(({ anchor1, anchor2, ...propagator }) => {
34 | // get the location for the two anchors (guarunteed to exist by the reducer)
35 | const anch1 = anchors[anchor1]
36 | const anch2 = anchors[anchor2]
37 |
38 | // add the anchor positions to the propagator config
39 | return {
40 | ...propagator,
41 | anchor1,
42 | anchor2,
43 | x1: anch1.x,
44 | y1: anch1.y,
45 | x2: anch2.x,
46 | y2: anch2.y,
47 | }
48 | }),
49 | }
50 | }
51 |
52 | export default elementsWithLocations
53 |
--------------------------------------------------------------------------------
/client/components/Input/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import styles from './styles'
5 |
6 | class Input extends React.Component {
7 | state = {
8 | edited: false,
9 | }
10 |
11 | componentWillUnmount() {
12 | if (this.state.edited) {
13 | if (this.props.onFinish) {
14 | this.props.onFinish(this.props.value)
15 | }
16 | this.setState({
17 | edited: false,
18 | })
19 | }
20 | }
21 |
22 | render() {
23 | const { style, onKeyDown, onChange, onFinish, ...unusedProps } = this.props
24 |
25 | return (
26 | {
29 | evt.stopPropagation()
30 | }}
31 | ref={e => (this._input = e)}
32 | onBlur={evt => {
33 | if (this.state.edited) {
34 | // if there is a callback to invoke when we're done
35 | if (onFinish) {
36 | // do so
37 | onFinish(this.props.value)
38 | }
39 |
40 | // we haven't updated the state since we last had focus
41 | this.setState({
42 | edited: false,
43 | })
44 | }
45 | }}
46 | onChange={evt => {
47 | // if there is a change callback
48 | if (this.props.onChange) {
49 | this.props.onChange(evt)
50 | }
51 | this.setState({ edited: true })
52 | }}
53 | {...unusedProps}
54 | />
55 | )
56 | }
57 | }
58 |
59 | Input.defaultProps = {
60 | onKeyDown: () => {},
61 | }
62 |
63 | export default Input
64 |
--------------------------------------------------------------------------------
/client/components/Overlay/test.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | import { shallow, mount } from 'enzyme'
4 | // local imports
5 | import Overlay from '.'
6 | import Header from './Header'
7 |
8 | describe('"Reusable" Components', () => {
9 | describe('Overlay', () => {
10 | test('clicking on the background calls hide', () => {
11 | // a spy to track if hide is called
12 | const spy = jest.fn()
13 |
14 | // mount the overlay
15 | const wrapper = shallow(foo )
16 |
17 | // click the root level element
18 | wrapper.find('aside').simulate('click')
19 |
20 | // make sure the spy was called
21 | expect(spy).toHaveBeenCalled()
22 | })
23 |
24 | test('clicking on the content does not call hide', () => {
25 | // a spy to track if hide is called
26 | const spy = jest.fn()
27 |
28 | // mount the overlay
29 | const wrapper = shallow(foo )
30 |
31 | // click the content
32 | wrapper.find('section').simulate('click')
33 |
34 | // make sure the spy was called
35 | expect(spy).not.toHaveBeenCalled()
36 | })
37 |
38 | test('places addon prop in the dom', () => {
39 | // the addon to the pass to the overlay
40 | const Foo = () =>
41 | // we have to pass an instanstiated component
42 | const addon =
43 |
44 | // a spy to track if hide is called
45 | const spy = jest.fn()
46 |
47 | // mount the overlay
48 | const wrapper = mount(
49 |
50 | foo
51 |
52 | )
53 |
54 | // make sure the addon was rendere
55 | expect(wrapper.find(Foo)).toHaveLength(1)
56 | })
57 | })
58 | })
59 |
--------------------------------------------------------------------------------
/client/interface/Diagram/Propagator/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import styles from './styles'
5 | import Fermion from './Fermion'
6 | import ElectroWeak from './ElectroWeak'
7 | import Gluon from './Gluon'
8 | import Dashed from './Dashed'
9 | import { Splittable } from 'components'
10 | import Label from './Label'
11 |
12 | export const Propagator = ({ kind, selected, id, labelDistance, labelLocation, label, ...element }) => {
13 | // a mapping of element kind to component
14 | const Component = {
15 | fermion: Fermion,
16 | em: ElectroWeak,
17 | dashed: Dashed,
18 | gluon: Gluon,
19 | }[kind]
20 |
21 | if (typeof Component === 'undefined') {
22 | return null
23 | }
24 |
25 | // use the selected styling when appropriate
26 | const styling = selected ? styles.selected : {}
27 |
28 | // compute the distance for the label
29 | const dx = element.x1 - element.x2
30 | const dy = element.y1 - element.y2
31 | const distance = Math.sqrt(dx * dx + dy * dy)
32 | // if the distance is too small to show anything meaninful
33 | if (distance < 5) {
34 | // don't render anything
35 | return null
36 | }
37 |
38 | return (
39 |
40 | {label && (
41 |
42 | {label}
43 |
44 | )}
45 |
46 |
47 |
48 |
49 | )
50 | }
51 |
52 | export const defaultProps = {
53 | strokeWidth: 2,
54 | stroke: 'black',
55 | selected: false,
56 | labelDistance: 30,
57 | labelLocation: 0.5,
58 | label: '',
59 | }
60 |
61 | Propagator.defaultProps = defaultProps
62 |
63 | export default Propagator
64 |
--------------------------------------------------------------------------------
/client/interface/Sidebar/HotkeySummary/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | import { connect } from 'react-redux'
4 | // local imports
5 | import styles from './styles'
6 | import { toggleHotkeys } from 'actions/info'
7 | import { Collapsible } from 'components'
8 | import Hotkey from './Hotkey'
9 |
10 | const HotkeySummary = ({ style, info, dispatch, ...unusedProps }) => (
11 | dispatch(toggleHotkeys())}
15 | style={style}
16 | {...unusedProps}
17 | >
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | )
38 |
39 | const selector = ({ diagram: { info } }) => ({ info })
40 | export default connect(selector)(HotkeySummary)
41 |
--------------------------------------------------------------------------------
/client/interface/PatternModal/patterns/dy/index.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import preview from './preview'
3 | import { blue } from 'colors'
4 |
5 | export default {
6 | name: 'Drell-Yan',
7 | preview,
8 | elements: {
9 | type: 'pattern',
10 | anchors: [
11 | {
12 | id: 1,
13 | x: 50,
14 | y: 100,
15 | },
16 | {
17 | id: 2,
18 | x: 150,
19 | y: 200,
20 | },
21 | {
22 | id: 3,
23 | x: 50,
24 | y: 300,
25 | },
26 | {
27 | id: 4,
28 | x: 400,
29 | y: 100,
30 | },
31 | {
32 | id: 5,
33 | x: 300,
34 | y: 200,
35 | },
36 | {
37 | id: 6,
38 | x: 400,
39 | y: 300,
40 | },
41 | ],
42 | propagators: [
43 | {
44 | id: 1,
45 | kind: 'fermion',
46 | label: 'l',
47 | anchor1: 2,
48 | anchor2: 1,
49 | },
50 | {
51 | id: 2,
52 | kind: 'fermion',
53 | label: '\\anti{l}',
54 | anchor1: 3,
55 | anchor2: 2,
56 | },
57 | {
58 | id: 3,
59 | kind: 'em',
60 | stroke: blue,
61 | label: 'Z',
62 | anchor1: 2,
63 | anchor2: 5,
64 | },
65 | {
66 | id: 4,
67 | kind: 'fermion',
68 | label: 'l',
69 | anchor1: 4,
70 | anchor2: 5,
71 | },
72 | {
73 | id: 5,
74 | kind: 'fermion',
75 | label: '\\anti{l}',
76 | anchor1: 5,
77 | anchor2: 6,
78 | },
79 | ],
80 | },
81 | }
82 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/ItemPalette/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | import { connect } from 'react-redux'
4 | import { FilePicker } from 'quark-web'
5 | // local imports
6 | import styles from './styles'
7 | import PaletteItem from './paletteItem'
8 | import { Button } from 'components'
9 | import { circle, gluon, line, dashed, em, text } from './images'
10 | import { togglePatternModal } from 'actions/info'
11 | import { saveDiagram as save, loadDiagram as load } from 'actions/elements'
12 | import { commit as commitWithMsg } from 'actions/history'
13 |
14 | const ItemPalette = ({ togglePatterns, style, commit, saveDiagram, loadDiagram, onMouseDown, ...unusedProps }) => {
15 | // a local component to dry up code
16 | const Item = ({ ...props }) =>
17 |
18 | return (
19 |
20 |
item palette
21 |
Drag and drop items to add them to a canvas
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | Show Patterns
33 |
34 |
35 |
36 | )
37 | }
38 |
39 | const mapDispatchToProps = dispatch => ({
40 | togglePatterns: () => dispatch(togglePatternModal()),
41 | })
42 |
43 | export default connect(
44 | null,
45 | mapDispatchToProps
46 | )(ItemPalette)
47 |
--------------------------------------------------------------------------------
/config/webpack.js:
--------------------------------------------------------------------------------
1 | // webpack imports
2 | var webpack = require('webpack')
3 | var process = require('process')
4 | var HtmlWebpackPlugin = require('html-webpack-plugin')
5 | // local imports
6 | var projectPaths = require('./projectPaths')
7 |
8 | var entry = [projectPaths.clientEntry]
9 |
10 | // the initial set of plugins
11 | var plugins = [
12 | new HtmlWebpackPlugin({
13 | template: 'client/index.html',
14 | hash: true,
15 | }),
16 | new webpack.NamedModulesPlugin(),
17 | new webpack.EnvironmentPlugin({
18 | NODE_ENV: process.env.NODE_ENV || 'dev',
19 | }),
20 | ]
21 | // if we are building for production
22 | if (process.env.NODE_ENV !== 'production') {
23 | // add the webpack dev server config
24 | entry = ['webpack-dev-server/client?http://0.0.0.0:8080', 'webpack/hot/only-dev-server'].concat(entry)
25 | }
26 |
27 | // export webpack configuration object
28 | module.exports = {
29 | entry: entry,
30 | output: {
31 | path: projectPaths.buildDir,
32 | filename: 'client.js',
33 | },
34 | module: {
35 | rules: [
36 | {
37 | test: /\.jsx?$/,
38 | loader: 'babel-loader',
39 | include: projectPaths.sourceDir,
40 | },
41 | {
42 | test: /\.css$/,
43 | use: ['style-loader', 'css-loader'],
44 | },
45 | {
46 | test: /\.(png|jpg|ttf)$/,
47 | loader: 'url-loader',
48 | query: { limit: 10000000 },
49 | },
50 | ],
51 | },
52 | resolve: {
53 | extensions: ['.jsx', '.js'],
54 | modules: [projectPaths.sourceDir, projectPaths.rootDir, 'node_modules'],
55 | },
56 | plugins: plugins,
57 | mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
58 | devServer: {
59 | proxy: {
60 | '/latex': {
61 | target: 'http://localhost:8081',
62 | pathRewrite: {
63 | '^/latex': '',
64 | },
65 | },
66 | },
67 | },
68 | }
69 |
70 | // end of file
71 |
--------------------------------------------------------------------------------
/client/utils/tests/generateElementId.test.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import { createStore } from 'store'
3 | import { addAnchors } from 'actions/elements'
4 | import { generateElementId } from '..'
5 | import range from '../range'
6 |
7 | describe('Utils', () => {
8 | describe('Generate Element Id Util', () => {
9 | test('can generate a unique id from an empty state', () => {
10 | // create a store to test with
11 | const { anchors } = createStore().getState().diagram.elements
12 | // generate an id for the anchor
13 | const id = generateElementId(anchors)
14 |
15 | // make sure the id is not taken
16 | expect(anchors[id]).not.toBeDefined()
17 | })
18 |
19 | test('can generate a non-conflicting id with a non-empty state', () => {
20 | // create a state to test with
21 | const store = createStore()
22 |
23 | // generate a set of 1000 anchors
24 | const testAnchors = range(1000).map(() => ({
25 | // with a random id
26 | id: Math.random() * 10000,
27 | x: 50,
28 | y: 50,
29 | }))
30 |
31 | // add an anchor to the state
32 | store.dispatch(addAnchors(...testAnchors))
33 | const { anchors } = store.getState().diagram.elements
34 |
35 | // generate an id for the anchor
36 | const id = generateElementId(anchors)
37 |
38 | // make sure the id is not taken
39 | expect(anchors[id]).not.toBeDefined()
40 | })
41 |
42 | test('can generate multiple unique ids', () => {
43 | // create a state to test with
44 | const { anchors } = createStore().getState().diagram.elements
45 |
46 | // the numer of unique ids to create
47 | const nIds = 5
48 | // create a set out of the result of calling the utility
49 | const ids = new Set(generateElementId(anchors, nIds))
50 |
51 | // make sure we have the correct number of unique ids
52 | expect(ids.size).toEqual(nIds)
53 | })
54 | })
55 | })
56 |
--------------------------------------------------------------------------------
/client/components/Text/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | import PropTypes from 'prop-types'
4 | import queryString from 'query-string'
5 |
6 | class Text extends React.Component {
7 | componentDidMount = () => this._updateImage()
8 |
9 | componentWillReceiveProps = () => this._updateImage()
10 |
11 | state = {
12 | width: 0,
13 | height: 0,
14 | }
15 |
16 | _updateImage = () => {
17 | // create a browser image we can use to find the height and width
18 | var img = new Image()
19 | img.src = this._imgUrl
20 | img.onload = () => {
21 | this.setState({
22 | width: img.width,
23 | height: img.height,
24 | })
25 | }
26 | }
27 |
28 | get _imgUrl() {
29 | return `/latex/string?${queryString.stringify({
30 | string: this.props.children.trim(),
31 | mathMode: JSON.stringify(this.props.math),
32 | color: this.props.color,
33 | })}`
34 | }
35 |
36 | render() {
37 | const { math, children, x, y, color, ...unused } = this.props
38 |
39 | // the text for the label
40 | const string = children.trim()
41 |
42 | // if there is an empty string to render
43 | if (!string) {
44 | // dont render anything
45 | return null
46 | }
47 |
48 | // return the image primitive that will be embedded an an svg
49 | return (
50 |
59 | )
60 | }
61 | }
62 |
63 | Text.defaultProps = {
64 | math: false,
65 | color: 'black',
66 | }
67 |
68 | Text.propTypes = {
69 | math: PropTypes.bool,
70 | children: PropTypes.string,
71 | color: PropTypes.string,
72 | x: PropTypes.number,
73 | y: PropTypes.number,
74 | }
75 |
76 | export default Text
77 |
--------------------------------------------------------------------------------
/client/interface/Diagram/Propagator/ElectroWeak/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import range from 'utils/range'
5 | import Align from '../Align'
6 |
7 | const ElectroWeak = ({
8 | x1,
9 | y1,
10 | x2,
11 | y2,
12 | direction,
13 | // pulled out to prevent cascade
14 | arrow,
15 | anchor1,
16 | anchor2,
17 | ...unusedProps
18 | }) => {
19 | // the width of the pattern (half-period)
20 | const scale = 20
21 | // the height of the pattern
22 | const amplitude = 3 * scale / 2
23 | // compute the length of the line
24 | const dx = x2 - x1
25 | const dy = y2 - y1
26 | const length = Math.sqrt(dx * dx + dy * dy)
27 |
28 | // find the closest whole number of full periods
29 | const nPeriods = Math.round(length / scale)
30 |
31 | // running totals to track the current location
32 | let cx = x1
33 | const cy = y1
34 |
35 | // the upper and lower y coorindates for the anchors
36 | const ymin = cy - direction * amplitude
37 | const ymax = cy + direction * amplitude
38 |
39 | // start the path at the current (x, y) location
40 | let pathString = `M ${cx} ${cy} `
41 | pathString += `C ${cx + scale / 2} ${ymin} ${cx + scale / 2} ${ymax} ${cx + scale} ${cy} `
42 |
43 | // for each period we have to render (correct for zero indexing)
44 | for (const _ of range(nPeriods - 1)) {
45 | // increment the running counter
46 | cx += scale
47 | // add the period
48 | pathString += `S ${cx + scale / 2} ${ymax} ${cx + scale} ${cy} `
49 | }
50 |
51 | // render the actual path
52 | return (
53 |
62 |
63 |
64 | )
65 | }
66 |
67 | ElectroWeak.defaultProps = {
68 | direction: 1,
69 | }
70 |
71 | export default ElectroWeak
72 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/SelectionSummary/TextSummary/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | import { connect } from 'react-redux'
4 | // local imports
5 | import styles from './styles'
6 | import { setElementAttrs, deleteSelection } from 'actions/elements'
7 | import { withCommit, commit } from 'actions/history'
8 | import { ButtonRow, Row, Label, Header, Container } from '..'
9 | import { Input, RedButton } from 'components'
10 |
11 | const TextSummary = ({ text, deleteElement, elements, setValue, showDelete, commit, ...unused }) => {
12 | // a potentially pluralized version of the type
13 | const textPlural = text.length > 1 ? 'texts' : 'text'
14 |
15 | return (
16 |
17 | {`${text.length} ${textPlural} selected`}
18 | {text.map(id => {
19 | // grab the value from the appropriate element
20 | const { value } = elements.text[id]
21 |
22 | return (
23 |
24 | {
28 | setValue(id, evt.target.value)
29 | }}
30 | onFinish={val => commit(`changed text value to ${val}`)}
31 | />
32 |
33 | )
34 | })}
35 | {showDelete && (
36 |
37 | {`Delete ${textPlural}`}
38 |
39 | )}
40 |
41 | )
42 | }
43 |
44 | const selector = ({ diagram: { elements } }) => ({ elements })
45 | const mapDispatchToProps = dispatch => ({
46 | setValue: (id, value) => dispatch(setElementAttrs({ type: 'text', id, value })),
47 | deleteElement: () => dispatch(withCommit(deleteSelection(), 'removed text from diagram')),
48 | commit: msg => dispatch(commit(msg)),
49 | })
50 |
51 | export default connect(selector, mapDispatchToProps)(TextSummary)
52 |
--------------------------------------------------------------------------------
/client/store/diagram/elements/propagators.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import _ from 'lodash'
3 | // local imports
4 | import { ADD_PROPAGATORS } from 'actions/elements'
5 |
6 | // the reducer that manages just the propagator state but has reference
7 | // to the entire element state reducer (must return just the propagator slice)
8 | export default (state, { type, payload }) => {
9 | // if the payload corresponds to a new propagator
10 | if (type === ADD_PROPAGATORS) {
11 | // make a local copy to mess with
12 | const local = _.cloneDeep(state.propagators)
13 |
14 | // get the current list of anchors
15 | const { anchors } = state
16 |
17 | // loop over every propagator we are supposed to add
18 | for (const propagator of payload) {
19 | // if there is no type
20 | if (!propagator.kind) {
21 | throw new Error('Could not add propagator without type')
22 | }
23 |
24 | // if there is no anchor matching anchor1
25 | if (!anchors[propagator.anchor1]) {
26 | throw new Error(`Could not attach propagator to anchor1 with id ${propagator.anchor1}`)
27 | }
28 |
29 | // if there is no anchor matching anchor2
30 | if (!anchors[propagator.anchor2]) {
31 | throw new Error(`Could not attach propagator to anchor2 with id ${propagator.anchor2}`)
32 | }
33 |
34 | // there is no id
35 | if (!propagator.id) {
36 | throw new Error('Cannot add propagator to store withour an id')
37 | }
38 |
39 | // if there is already a propagator with that id
40 | if (local[propagator.id]) {
41 | throw new Error(`Cannot create propagator with id - ${propagator.id} that is already taken.`)
42 | }
43 |
44 | // save a refernce to the propagator locally
45 | local[propagator.id] = propagator
46 | }
47 |
48 | // the propagators are safe to add to the state
49 |
50 | // add the payload to the list of propagators
51 | return local
52 | }
53 |
54 | return state.propagators
55 | }
56 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/SelectionSummary/ShapeSummary/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | import { connect } from 'react-redux'
4 | // local imports
5 | import { Shape } from 'interface/Diagram/Shape'
6 | import { ColorPicker } from 'components'
7 | import { setElementAttrs } from 'actions/elements'
8 | import { commit } from 'actions/history'
9 | import { Container, Label, Header, Row, SliderRow } from '..'
10 | import styles from './styles'
11 |
12 | const ShapeSummary = ({ shapes, elements, setAttrs, commit }) => {
13 | // a potentially pluralized version of the type
14 | const shapePlural = shapes.length > 1 ? 'shapes' : 'shape'
15 |
16 | // get the head of the selection and use that as values for the controls
17 | const head = elements.shapes[shapes[0]]
18 |
19 | return (
20 |
21 | {`${shapes.length} ${shapePlural} selected`}
22 |
23 | color:
24 | setAttrs({ color }, `changed shape color to ${color}`)}
28 | />
29 |
30 | setAttrs({ r })}
37 | onAfterChange={r => commit(`changed shape radius to ${r}`)}
38 | />
39 |
40 | )
41 | }
42 |
43 | const selector = ({ diagram: { elements } }) => ({ elements })
44 | const mapDispatchToProps = (dispatch, { shapes }) => ({
45 | // to update the attributes of each anchor that is selected
46 | setAttrs: (attrs, msg) => {
47 | dispatch(setElementAttrs(...shapes.map(id => ({ type: 'shapes', id, ...attrs }))))
48 | if (msg) {
49 | dispatch(commit(msg))
50 | }
51 | },
52 | commit: msg => dispatch(commit(msg)),
53 | })
54 | export default connect(selector, mapDispatchToProps)(ShapeSummary)
55 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/specs.js:
--------------------------------------------------------------------------------
1 | // this file contains the functions that map the mouse's location to elements specs
2 | // to be interpretted by the placeElement saga
3 |
4 | // local imports
5 | import { generateElementId, ceil, round } from 'utils'
6 |
7 | export const propagators = ({ x, y, info, elements, config }) => {
8 | // compute two anchor ids to use
9 | const [anchor1, anchor2] = generateElementId(elements.anchors, 2)
10 | // compute a propagator id to use
11 | const propagatorId = generateElementId(elements.propagators)
12 |
13 | // the lower right corner of the bounding box
14 | const upper = {
15 | x: ceil(x, info.gridSize),
16 | y: ceil(y, info.gridSize),
17 | }
18 |
19 | const lower = {
20 | x: upper.x - info.gridSize,
21 | y: upper.y - info.gridSize,
22 | }
23 |
24 | // return the element spec
25 | return {
26 | // the element saga to create the propagator
27 | element: {
28 | type: 'propagators',
29 | id: propagatorId,
30 | ...config,
31 | anchor1: {
32 | id: anchor1,
33 | x: lower.x,
34 | y: upper.y,
35 | },
36 | anchor2: {
37 | id: anchor2,
38 | x: upper.x,
39 | y: lower.y,
40 | },
41 | },
42 | // select the propagator we created when we're done
43 | select: {
44 | type: 'propagators',
45 | id: propagatorId,
46 | },
47 | // remove the anchors to undo the action
48 | remove: [
49 | {
50 | type: 'anchors',
51 | id: anchor1,
52 | },
53 | {
54 | type: 'anchors',
55 | id: anchor2,
56 | },
57 | ],
58 | }
59 | }
60 |
61 | // the default spec used for single elements that are positioned at one location
62 | export const element = ({ x, y, info, elements, config, type }) => ({
63 | element: {
64 | id: generateElementId(elements[type]),
65 | type,
66 | ...config,
67 | x: round(x, info.gridSize),
68 | y: round(y, info.gridSize),
69 | },
70 | })
71 |
--------------------------------------------------------------------------------
/client/interface/ExportModal/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | import { connect } from 'react-redux'
4 | // local imports
5 | import { elementsWithLocations, diagramBoundingBox, propagatorConfig, shapeConfig, textConfig } from 'utils'
6 | import { Overlay, Code, Button } from 'components'
7 | import { toggleExportModal } from 'actions/info'
8 | import styles from './styles'
9 |
10 | // the link for the corresponding latex package
11 | const latexPackageLocation = 'https://storage.googleapis.com/aivazis-static-assets/feynman/feynman.sty'
12 |
13 | const ExportModal = ({ elements, hideModal }) => {
14 | const bb = diagramBoundingBox(elements, 200)
15 | return (
16 |
17 | Add this to your preamble
18 | \usepackage{'{feynman}'}
19 | Here's your diagram:
20 |
21 | \begin{'{feynman}'}
22 | {elementsWithLocations(elements).propagators.map((propagator, i) => (
23 | {propagatorConfig(propagator, bb)}
24 | ))}
25 | {Object.values(elements.shapes).map((shape, i) => (
26 | {shapeConfig(shape, bb)}
27 | ))}
28 | {Object.values(elements.text).map((text, i) => (
29 | {textConfig(text, bb)}
30 | ))}
31 | \end{'{feynman}'}
32 |
33 |
34 |
42 |
43 | )
44 | }
45 |
46 | const mapDispatchToProps = dispatch => ({
47 | hideModal: () => dispatch(toggleExportModal()),
48 | })
49 | const selector = ({ diagram: { elements } }) => ({ elements })
50 | export default connect(
51 | selector,
52 | mapDispatchToProps
53 | )(ExportModal)
54 |
--------------------------------------------------------------------------------
/client/interface/Sidebar/HistorySummary/test.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | import { Provider } from 'react-redux'
4 | import { mount } from 'enzyme'
5 | // local imports
6 | import { addAnchors } from 'actions/elements'
7 | import { commit } from 'actions/history'
8 | import { createStore } from 'store'
9 | import HistorySummary from '.'
10 | import Entry from './Entry'
11 |
12 | const Test = props => (
13 |
14 |
15 |
16 | )
17 |
18 | describe('Interface Components', () => {
19 | describe('Sidebar', () => {
20 | describe('History Summary', () => {
21 | test('shows a history entry in the summary for each commit', () => {
22 | // a store to test with
23 | const store = createStore()
24 |
25 | // add some anchors
26 | store.dispatch(
27 | addAnchors({
28 | id: 1,
29 | x: 50,
30 | y: 100,
31 | })
32 | )
33 | // commit the current state
34 | store.dispatch(commit('initial state'))
35 |
36 | // add some anchors
37 | store.dispatch(
38 | addAnchors({
39 | id: 2,
40 | x: 100,
41 | y: 200,
42 | })
43 | )
44 | // commit the current state
45 | store.dispatch(commit('second state'))
46 |
47 | // mount the test
48 | const wrapper = mount( )
49 |
50 | // make sure there is enough entry
51 | expect(wrapper.find(Entry)).toHaveLength(2)
52 |
53 | // make sure the find one wraps an entry with the right message
54 | expect(
55 | wrapper
56 | .find(Entry)
57 | .at(0)
58 | .props().children.message
59 | ).toEqual('second state')
60 | expect(
61 | wrapper
62 | .find(Entry)
63 | .at(1)
64 | .props().children.message
65 | ).toEqual('initial state')
66 | })
67 | })
68 | })
69 | })
70 |
--------------------------------------------------------------------------------
/client/interface/Diagram/Grid/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | import { connect } from 'react-redux'
4 | // local imports
5 | import { range, round } from 'utils'
6 | import styles from './styles'
7 | import { sidebarWidth } from 'interface/Sidebar/styles'
8 |
9 | const Grid = ({ style, browser, info }) => {
10 | // the number of vertical grid lines (add one to cover the remainder)
11 | const nVertical = (Math.floor((browser.width - sidebarWidth) / info.gridSize) + 1) / info.zoomLevel
12 | // the number of horizontal lines (add one to cover the remainder)
13 | const nHorizontal = (Math.floor(browser.height / info.gridSize) + 1) / info.zoomLevel
14 |
15 | // the 4 edges of the grid
16 | const topEdge = (browser.height + info.gridSize - info.pan.y) / info.zoomLevel
17 | const bottomEdge = (-info.gridSize - info.pan.y) / info.zoomLevel
18 | const leftEdge = (-info.gridSize - info.pan.x) / info.zoomLevel
19 | const rightEdge = (browser.width + info.gridSize - info.pan.x) / info.zoomLevel
20 |
21 | // className is used to remove it during png export
22 | return (
23 |
24 | {range(nVertical + 1).map(i => {
25 | // the shared x coordinate of the vertical lines
26 | const x = round(leftEdge + i * info.gridSize, info.gridSize)
27 | // render the line
28 | return (
29 |
34 | )
35 | })}
36 | {range(nHorizontal + 1).map(i => {
37 | // the shared y coordinate of the horizontal lines
38 | const y = round(bottomEdge + i * info.gridSize, info.gridSize)
39 | // render the line
40 | return (
41 |
46 | )
47 | })}
48 |
49 | )
50 | }
51 |
52 | const selector = ({ diagram: { info }, browser }) => ({ info, browser })
53 | export default connect(selector)(Grid)
54 |
--------------------------------------------------------------------------------
/client/interface/Toolbar/SelectionSummary/index.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | import { connect } from 'react-redux'
4 | import _ from 'lodash'
5 | // local imports
6 | import styles from './styles'
7 | import AnchorSummary from './AnchorSummary'
8 | import PropagatorSummary from './PropagatorSummary'
9 | import TextSummary from './TextSummary'
10 | import ShapeSummary from './ShapeSummary'
11 | import { flatMap } from 'utils'
12 | import { deleteSelection } from 'actions/elements'
13 | import { withCommit } from 'actions/history'
14 | import ButtonRow from './ButtonRow'
15 | import { RedButton } from 'components'
16 |
17 | const SelectionSummary = ({ style, selection, deleteElements, ...unusedProps }) => {
18 | // hide the individual delete buttons if there is a heteogenous selection
19 | const hideDelete = Object.values(selection).filter(ids => ids.length > 0).length > 1
20 |
21 | return (
22 |
23 | {(selection.anchors || []).length > 0 && (
24 |
25 | )}
26 | {(selection.propagators || []).length > 0 && (
27 |
28 | )}
29 | {(selection.text || []).length > 0 &&
}
30 | {(selection.shapes || []).length > 0 &&
}
31 | {hideDelete && (
32 |
33 |
34 | Delete Elements
35 |
36 |
37 | )}
38 |
39 | )
40 | }
41 |
42 | const mapDispatchToProps = (dispatch, { selection }) => ({
43 | deleteElements: () => dispatch(withCommit(deleteSelection(), 'removed selected elements from diagram')),
44 | })
45 |
46 | export default connect(null, mapDispatchToProps)(SelectionSummary)
47 |
48 | // local exports for convinience
49 | export MultiRow from './MultiRow'
50 | export SliderRow from './SliderRow'
51 | export Row from './Row'
52 | export Label from './Label'
53 | export Header from './Header'
54 | export ButtonRow from './ButtonRow'
55 | export Container from './Container'
56 |
--------------------------------------------------------------------------------
/client/sagas/splitElement/propagator.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import { select, put } from 'redux-saga/effects'
3 | // local imports
4 | import { addAnchors, addPropagators, selectElements, setElementAttrs } from 'actions/elements'
5 | import { generateElementId } from 'utils'
6 |
7 | export default function* splitPropagator({ element, location }) {
8 | // the current state
9 | const elements = (yield select(state => state.diagram.elements)) || {
10 | anchors: {},
11 | propagators: {
12 | [element.id]: {
13 | anchor1: 1,
14 | anchor2: 2,
15 | },
16 | },
17 | }
18 |
19 | // we need two unique ids for the split and the branch
20 | const [splitAnchorId, branchAnchorId] = generateElementId(elements.anchors, 2)
21 |
22 | // get the anchors assocaited with the propagator we need to snap
23 | const { anchor1, anchor2 } = elements.propagators[element.id]
24 |
25 | // create both anchors on the mouses current location
26 | yield put(
27 | addAnchors(
28 | {
29 | id: splitAnchorId,
30 | ...location,
31 | },
32 | {
33 | id: branchAnchorId,
34 | ...location,
35 | }
36 | )
37 | )
38 |
39 | // the old line will go between the split and anchor1
40 | yield put(
41 | setElementAttrs({
42 | id: element.id,
43 | type: 'propagators',
44 | anchor1,
45 | anchor2: splitAnchorId,
46 | })
47 | )
48 |
49 | // similarly, we need two ids of propagators
50 | const [newPropagatorId, branchPropagatorId] = generateElementId(elements.propagators, 2)
51 |
52 | // create a new propagator between the split and anchor2, and the split and the branching one
53 | yield put(
54 | addPropagators(
55 | {
56 | id: newPropagatorId,
57 | kind: 'fermion',
58 | anchor1: splitAnchorId,
59 | anchor2,
60 | },
61 | {
62 | id: branchPropagatorId,
63 | kind: 'fermion',
64 | anchor1: splitAnchorId,
65 | anchor2: branchAnchorId,
66 | }
67 | )
68 | )
69 |
70 | // when we're done, select the anchor we split off of the shape
71 | yield put(selectElements({ type: 'anchors', id: branchAnchorId }))
72 | }
73 |
--------------------------------------------------------------------------------
/client/interface/Diagram/Propagator/Gluon/withoutEndcaps.js:
--------------------------------------------------------------------------------
1 | // external imports
2 | import React from 'react'
3 | // local imports
4 | import Align from '../Align'
5 |
6 | const GluonWithoutEndcaps = ({
7 | x1,
8 | y1,
9 | x2,
10 | y2,
11 | direction,
12 | // pulled out to prevent cascade
13 | arrow,
14 | anchor1,
15 | anchor2,
16 | ...unusedProps
17 | }) => {
18 | // compute the length of the line
19 | const dx = x2 - x1
20 | const dy = y2 - y1
21 | const length = Math.sqrt(dx * dx + dy * dy)
22 |
23 | // the width of the pattern
24 | const scale = 10
25 | // the height of the pattern
26 | const amplitude = dx < 0 ? scale : -scale
27 |
28 | // find the closest number of full loops
29 | const nLoops = Math.round(length / scale / 2)
30 |
31 | // the running counters
32 | let cx = x1
33 | let cy = y1
34 | // the top and bottom limts for the pattern
35 | const ymin = cy
36 | const ymax = cy + 2 * direction * amplitude
37 |
38 | // the step size
39 | const delta = scale
40 |
41 | // start the path off
42 | let pathString = `M ${cx} ${cy} `
43 |
44 | // the first loop has to be drawn separately since it starts with a C
45 | pathString += `C ${cx + 2 * scale} ${ymin} ${cx + 2 * scale} ${ymax} ${cx + delta} ${ymax} `
46 | // update the running total
47 | cx += delta
48 | // the second half
49 | pathString += `S ${cx - delta} ${ymin} ${cx + delta} ${ymin} `
50 | // update the running total
51 | cx += delta
52 |
53 | // draw the rest of the loops
54 | for (let cycle = 1; cycle < nLoops; cycle++) {
55 | // add the first half of the loop
56 | pathString += `S ${cx + 2 * scale} ${ymax} ${cx + delta} ${ymax} `
57 | // update the running counter
58 | cx += delta
59 | // second half of the loop
60 | pathString += `S ${cx - delta} ${ymin} ${cx + delta} ${ymin} `
61 | // update the running counter
62 | cx += delta
63 | }
64 |
65 | return (
66 |
75 |
76 |
77 | )
78 | }
79 |
80 | export default GluonWithoutEndcaps
81 |
--------------------------------------------------------------------------------
/client/utils/tests/fixDeltaToGrid.test.js:
--------------------------------------------------------------------------------
1 | // local imports
2 | import fixDeltaToGrid from '../fixDeltaToGrid'
3 |
4 | describe('Utils', () => {
5 | describe('Fix Delta to Grid Util', () => {
6 | test("doesn't round the target with grid size === 0", () => {
7 | // where we are
8 | const origin = { x: 0, y: 0 }
9 | // the place to move to
10 | const next = { x: 52, y: 103 }
11 | // the current info / gridSize
12 | const info = { gridSize: 0, zoomLevel: 1 }
13 |
14 | expect(fixDeltaToGrid({ origin, next, info })).toEqual(next)
15 | })
16 |
17 | test("doesn't round the target with grid size === 0", () => {
18 | // where we are
19 | const origin = { x: 0, y: 0 }
20 | // the place to move to
21 | const next = { x: 52, y: 103 }
22 | // the current info / gridSize
23 | const info = { gridSize: 0, zoomLevel: 1 }
24 |
25 | expect(fixDeltaToGrid({ origin, next, info })).toEqual(next)
26 | })
27 |
28 | test('rounds the location snapped to grid when appropriate', () => {
29 | // where we are
30 | const origin = { x: 0, y: 0 }
31 | // the place to move to
32 | const next = { x: 52, y: 103 }
33 | // the current info / gridSize
34 | const info = { gridSize: 50, zoomLevel: 1 }
35 |
36 | expect(fixDeltaToGrid({ origin, next, info })).toEqual({
37 | x: 50,
38 | y: 100,
39 | })
40 | })
41 |
42 | test('incorporates the zoomLevel when there is no grid', () => {
43 | // where we are
44 | const origin = { x: 0, y: 0 }
45 | // the place to move to
46 | const next = { x: 100, y: 100 }
47 | // the current info / gridSize
48 | const info = { gridSize: 0, zoomLevel: 2 }
49 |
50 | expect(fixDeltaToGrid({ origin, next, info })).toEqual({
51 | x: 100,
52 | y: 100,
53 | })
54 | })
55 |
56 | test('incorporates the zoomLevel when there is a grid', () => {
57 | // where we are
58 | const origin = { x: 0, y: 0 }
59 | // the place to move to
60 | const next = { x: 220, y: 100 }
61 | // the current info / gridSize
62 | const info = { gridSize: 50, zoomLevel: 2 }
63 |
64 | expect(fixDeltaToGrid({ origin, next, info })).toEqual({
65 | x: 200,
66 | y: 100,
67 | })
68 | })
69 | })
70 | })
71 |
--------------------------------------------------------------------------------
/server/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "os"
7 | "strconv"
8 | )
9 |
10 | func renderLatexHandler(w http.ResponseWriter, r *http.Request) {
11 | // look for a math mode query parameter
12 | mathQuery := r.URL.Query().Get("mathMode")
13 | // try to turn the query parameter into a boolean
14 | mathMode, _ := strconv.ParseBool(mathQuery)
15 |
16 | // look for a font size query parameter
17 | fontSizeQuery := r.URL.Query().Get("fontSize")
18 | // try to turn the query parameter into a float
19 | fontSize, _ := strconv.ParseFloat(fontSizeQuery, 64)
20 |
21 | // look for a font size query parameter
22 | baseLineQuery := r.URL.Query().Get("baseLine")
23 | // try to turn the query parameter into a float
24 | baseLine, _ := strconv.ParseFloat(baseLineQuery, 64)
25 |
26 | // the config provided by the user
27 | config := &RenderConfig{
28 | String: r.URL.Query().Get("string"),
29 | FontSize: fontSize,
30 | BaseLine: baseLine,
31 | Color: r.URL.Query().Get("color"),
32 | MathMode: mathMode,
33 | }
34 |
35 | // log our intentions
36 | fmt.Println("Rendering string:", config.String)
37 |
38 | // respond with the latex document
39 | writeLatex(w, stringTemplate, config, &BaseTemplateConfig{
40 | ExtraConfig: "varwidth=true,",
41 | })
42 | }
43 |
44 | func renderDiagramHandler(w http.ResponseWriter, r *http.Request) {
45 | fmt.Println("rendering diagram")
46 |
47 | // the config provided by the user
48 | config := &RenderConfig{
49 | String: r.URL.Query().Get("string"),
50 | }
51 |
52 | // respond with the latex document
53 | writeLatex(w, diagramTemplate, config, &BaseTemplateConfig{})
54 | }
55 |
56 | func renderPDFDiagramHandler(w http.ResponseWriter, r *http.Request) {
57 | fmt.Println("rendering diagram")
58 |
59 | // the config provided by the user
60 | config := &RenderConfig{
61 | String: r.URL.Query().Get("string"),
62 | Format: "pdf",
63 | }
64 |
65 | // respond with the latex document
66 | writeLatex(w, diagramTemplate, config, &BaseTemplateConfig{})
67 | }
68 |
69 | func main() {
70 | // where the server is listening on local host
71 | port, ok := os.LookupEnv("PORT")
72 | if !ok {
73 | port = "8081"
74 | }
75 |
76 | // set up the mux
77 | http.HandleFunc("/string", renderLatexHandler)
78 | http.HandleFunc("/diagram", renderDiagramHandler)
79 | http.HandleFunc("/pdf", renderPDFDiagramHandler)
80 |
81 | // notify the user we're going to start the server
82 | fmt.Println("🚀 listening on " + port)
83 |
84 | // start the server
85 | err := http.ListenAndServe(":" + port, nil)
86 | // if something went wrong
87 | if err != nil {
88 | // yell loudly
89 | panic(err)
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/client/interface/App/reset.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * ress.css • v1.1.2
3 | * MIT License
4 | * github.com/filipelinhares/ress
5 | */html{box-sizing:border-box;-webkit-text-size-adjust:100%}*,:after,:before{box-sizing:inherit}:after,:before{text-decoration:inherit;vertical-align:inherit}*{background-repeat:no-repeat;padding:0;margin:0}audio:not([controls]){display:none;height:0}hr{overflow:visible}article,aside,details,figcaption,figure,footer,header,main,menu,nav,section,summary{display:block}summary{display:list-item}small{font-size:80%}[hidden],template{display:none}abbr[title]{border-bottom:1px dotted;text-decoration:none}a{background-color:transparent;-webkit-text-decoration-skip:objects}a:active,a:hover{outline-width:0}code,kbd,pre,samp{font-family:monospace,monospace}b,strong{font-weight:bolder}dfn{font-style:italic}mark{background-color:#ff0;color:#000}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}input{border-radius:0}[role=button],[type=button],[type=reset],[type=submit],button{cursor:pointer}[disabled]{cursor:default}[type=number]{width:auto}[type=search]{-webkit-appearance:textfield}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}textarea{overflow:auto;resize:vertical}button,input,optgroup,select,textarea{font:inherit}optgroup{font-weight:700}button{overflow:visible}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:0;padding:0}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button:-moz-focusring{outline:1px dotted ButtonText}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}button,select{text-transform:none}button,input,select,textarea{background-color:transparent;border-style:none;color:inherit}select::-ms-expand{display:none}select::-ms-value{color:currentColor}legend{border:0;color:inherit;display:table;max-width:100%;white-space:normal}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}img{border-style:none}progress{vertical-align:baseline}svg:not(:root){overflow:hidden}audio,canvas,progress,video{display:inline-block}@media screen{[hidden~=screen]{display:inherit}[hidden~=screen]:not(:active):not(:focus):not(:target){position:absolute!important;clip:rect(0 0 0 0)!important}}[aria-busy=true]{cursor:progress}[aria-controls]{cursor:pointer}[aria-disabled]{cursor:default}::-moz-selection{background-color:#b3d4fc;color:#000;text-shadow:none}::selection{background-color:#b3d4fc;color:#000;text-shadow:none}
6 |
7 | #app {
8 | height: 100%;
9 | display: flex;
10 | }
11 |
12 | * {
13 | flex-shrink: 0;
14 | }
--------------------------------------------------------------------------------