├── .github └── workflows │ └── release.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── app.json ├── client ├── .env ├── .eslintrc.json ├── .flowconfig ├── README.md ├── flow-typed │ └── npm │ │ ├── antd_v3.x.x.js │ │ ├── babel-core_vx.x.x.js │ │ ├── classnames_v2.x.x.js │ │ ├── color_vx.x.x.js │ │ ├── css-time_vx.x.x.js │ │ ├── enzyme-adapter-react-16_vx.x.x.js │ │ ├── enzyme_v3.x.x.js │ │ ├── eslint-config-standard_vx.x.x.js │ │ ├── eslint-plugin-import_vx.x.x.js │ │ ├── eslint-plugin-jest_vx.x.x.js │ │ ├── eslint-plugin-node_vx.x.x.js │ │ ├── eslint-plugin-promise_vx.x.x.js │ │ ├── eslint-plugin-standard_vx.x.x.js │ │ ├── flow-bin_v0.x.x.js │ │ ├── flow_vx.x.x.js │ │ ├── immutability-helper_vx.x.x.js │ │ ├── jest-diff_vx.x.x.js │ │ ├── jest-get-type_vx.x.x.js │ │ ├── jest-matcher-deep-close-to_vx.x.x.js │ │ ├── jest_v22.x.x.js │ │ ├── jquery_v3.x.x.js │ │ ├── lodash_v4.x.x.js │ │ ├── mathjs_vx.x.x.js │ │ ├── mathquill_vx.x.x.js │ │ ├── re-reselect_vx.x.x.js │ │ ├── react-collapsible_v2.0.x.js │ │ ├── react-color_v2.x.x.js │ │ ├── react-redux_v5.x.x.js │ │ ├── react-scripts_vx.x.x.js │ │ ├── react-textarea-autosize_vx.x.x.js │ │ ├── redux-batched-actions_vx.x.x.js │ │ ├── redux-thunk_vx.x.x.js │ │ ├── redux_v3.x.x.js │ │ ├── shallow-diff_vx.x.x.js │ │ ├── styled-components_v4.x.x.js │ │ ├── tarjan-graph_vx.x.x.js │ │ ├── toposort_vx.x.x.js │ │ └── why-did-you-update_v0.x.x.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── fonts │ │ ├── Symbola-basic.eot │ │ ├── Symbola-basic.ttf │ │ ├── Symbola-basic.woff │ │ ├── Symbola-basic.woff2 │ │ ├── Symbola.eot │ │ ├── Symbola.svg │ │ ├── Symbola.ttf │ │ ├── Symbola.woff │ │ └── Symbola.woff2 │ ├── index.html │ ├── jquery.js │ ├── manifest.json │ ├── mathbox.css │ ├── mathquill.css │ └── mathquill.min.js └── src │ ├── App.js │ ├── components │ ├── EditableDescription │ │ ├── getTextWidth.js │ │ └── index.js │ ├── FlexContainer │ │ └── index.js │ ├── LongPressable │ │ ├── index.js │ │ └── index.test.js │ ├── MathBox │ │ ├── MathBox.js │ │ ├── MathBoxComponents.js │ │ ├── helpers.js │ │ └── index.js │ ├── MathQuill │ │ └── index.js │ ├── PopModal │ │ └── index.js │ ├── ScrollWithOverflow │ │ └── index.js │ ├── SortableList │ │ ├── PartlyDraggable.js │ │ └── index.js │ └── SubtleButton │ │ └── index.js │ ├── constants │ ├── colors.js │ ├── idGenerator.js │ ├── index.js │ ├── parsing.js │ └── theme.js │ ├── containers │ ├── ControlledTabs │ │ ├── actions.js │ │ ├── components │ │ │ └── ControlledTabs.js │ │ ├── index.js │ │ └── reducer.js │ ├── ControllerHeader │ │ ├── components │ │ │ └── ControllerHeader.js │ │ ├── index.js │ │ ├── selectors.js │ │ └── selectors.test.js │ ├── Drawer │ │ ├── ScreenSizeDrawerManager.js │ │ ├── actions.js │ │ ├── components │ │ │ ├── Drawer.js │ │ │ └── DrawerToggleButton.js │ │ ├── index.js │ │ └── reducer.js │ ├── MathBoxContainer │ │ ├── components │ │ │ └── MathBoxContainer.js │ │ └── index.js │ ├── MathBoxScene │ │ ├── components │ │ │ └── MathBoxScene.js │ │ └── index.js │ ├── MathObjects │ │ ├── Folder │ │ │ ├── actions.js │ │ │ ├── components │ │ │ │ ├── CollapsedIndicator.js │ │ │ │ └── Folder.js │ │ │ ├── index.js │ │ │ ├── metadata.js │ │ │ └── selectors.js │ │ ├── MathGraphics │ │ │ ├── Axis │ │ │ │ └── index.js │ │ │ ├── Camera │ │ │ │ ├── UseComputedToggle.js │ │ │ │ └── index.js │ │ │ ├── Grid │ │ │ │ └── index.js │ │ │ ├── ImplicitSurface │ │ │ │ └── index.js │ │ │ ├── Line │ │ │ │ └── index.js │ │ │ ├── MathGraphic.js │ │ │ ├── ParametricCurve │ │ │ │ └── index.js │ │ │ ├── ParametricSurface │ │ │ │ ├── containers │ │ │ │ │ ├── ParametricSurfaceStatus.js │ │ │ │ │ └── components │ │ │ │ │ │ └── ColorScale.js │ │ │ │ └── index.js │ │ │ ├── Point │ │ │ │ └── index.js │ │ │ ├── Vector │ │ │ │ └── index.js │ │ │ ├── VectorField │ │ │ │ └── index.js │ │ │ ├── containers │ │ │ │ ├── EvaluatedStatusSymbol │ │ │ │ │ ├── components │ │ │ │ │ │ ├── EvaluatedStatusSymbol.js │ │ │ │ │ │ └── StatusSymbol │ │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ ├── ColorPicker.js │ │ │ │ │ │ │ ├── ColorPickerPopover.js │ │ │ │ │ │ │ └── ColorSquare.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ └── index.js │ │ │ │ └── MathGraphicUI │ │ │ │ │ ├── components │ │ │ │ │ └── MathGraphicUI.js │ │ │ │ │ └── index.js │ │ │ ├── index.js │ │ │ ├── metadata.js │ │ │ └── types.js │ │ ├── MathObject.js │ │ ├── MathObjectUI.js │ │ ├── MathSymbols │ │ │ ├── BooleanVariable │ │ │ │ ├── components │ │ │ │ │ └── BooleanVariable.js │ │ │ │ ├── index.js │ │ │ │ └── metadata.js │ │ │ ├── Variable │ │ │ │ ├── components │ │ │ │ │ └── Variable.js │ │ │ │ ├── index.js │ │ │ │ └── metadata.js │ │ │ ├── VariableSlider │ │ │ │ ├── actions.js │ │ │ │ ├── components │ │ │ │ │ ├── AnimationControls.js │ │ │ │ │ ├── EvaluatedSlider.js │ │ │ │ │ ├── SliderValueDisplay.js │ │ │ │ │ └── VariableSlider.js │ │ │ │ ├── index.js │ │ │ │ ├── metadata.js │ │ │ │ └── reducer.js │ │ │ └── index.js │ │ ├── actions.js │ │ ├── components │ │ │ ├── DeleteButton.js │ │ │ ├── MainRow.js │ │ │ ├── MathObjectUI.js │ │ │ ├── ObjectIcon.js │ │ │ └── index.js │ │ ├── containers │ │ │ ├── MathInput │ │ │ │ ├── ConnectedStaticMath.js │ │ │ │ ├── MathInputLHS.js │ │ │ │ ├── MathInputRHS.js │ │ │ │ ├── MathTextOutput.js │ │ │ │ ├── components │ │ │ │ │ ├── MathInput.js │ │ │ │ │ ├── MathInput.test.js │ │ │ │ │ ├── MathQuillStyled.js │ │ │ │ │ ├── validators.js │ │ │ │ │ └── validators.test.js │ │ │ │ ├── index.js │ │ │ │ ├── selectors.js │ │ │ │ └── selectors.test.js │ │ │ ├── Settings │ │ │ │ ├── components │ │ │ │ │ ├── Settings.js │ │ │ │ │ ├── SettingsButton.js │ │ │ │ │ ├── SettingsContainer.js │ │ │ │ │ └── SettingsContent.js │ │ │ │ └── index.js │ │ │ └── ToggleSwitch │ │ │ │ └── index.js │ │ ├── index.js │ │ ├── reducer.js │ │ ├── reducer.test.js │ │ ├── selectors.js │ │ ├── selectors.test.js │ │ └── services │ │ │ └── activeObject │ │ │ ├── actions.js │ │ │ └── reducer.js │ ├── MathScopeContext │ │ ├── components │ │ │ └── MathScopeContext.js │ │ ├── index.js │ │ └── selectors.js │ └── SortableTree │ │ ├── actions.js │ │ ├── components │ │ └── MathTree.js │ │ ├── index.js │ │ ├── reducer.js │ │ └── reducer.test.js │ ├── index.css │ ├── index.js │ ├── registerServiceWorker.js │ ├── services │ ├── LoopManager.js │ ├── api │ │ └── index.js │ ├── errors │ │ ├── ErrorData.js │ │ ├── actions.js │ │ ├── index.js │ │ ├── reducer.js │ │ └── selectors.js │ ├── evalData.js │ ├── getCameraData.js │ ├── lastSavedState │ │ ├── actions.js │ │ ├── index.js │ │ └── reducer.js │ └── metadata │ │ ├── actions.js │ │ └── reducer.js │ ├── setupTests.js │ ├── store │ ├── actions.js │ ├── hydration.js │ ├── index.js │ ├── initialState.js │ ├── mockState.js │ └── reducer.js │ ├── utils │ ├── colors.js │ ├── flow │ │ ├── index.js │ │ └── index.test.js │ ├── functions │ │ └── index.js │ ├── helpers.js │ ├── helpers.test.js │ ├── marchingCubes │ │ ├── index.js │ │ └── triangleTable.js │ ├── mathParsing │ │ ├── MathExpression.js │ │ ├── MathExpression.test.js │ │ ├── Parser.js │ │ ├── Parser.test.js │ │ ├── index.js │ │ ├── mathscope.js │ │ ├── mathscope.test.js │ │ ├── postprocessors │ │ │ ├── index.js │ │ │ ├── index.test.js │ │ │ ├── reassignOperators.js │ │ │ └── reassignOperators.test.js │ │ └── preprocessors │ │ │ ├── index.js │ │ │ ├── index.test.js │ │ │ ├── preprocessHOFs.js │ │ │ ├── preprocessHOFs.test.js │ │ │ ├── preprocessMathQuill.js │ │ │ └── preprocessMathQuill.test.js │ ├── mathjs │ │ ├── custom.js │ │ ├── derivatives.js │ │ ├── derivatives.test.js │ │ ├── index.js │ │ ├── legendre.js │ │ └── types.js │ ├── sets │ │ ├── index.js │ │ └── index.test.js │ ├── shallowDiffWithSets.js │ └── testing │ │ ├── matchers.js │ │ ├── matchers.test.js │ │ └── mathquill_for_tests_only.js │ └── views │ └── MainView │ ├── Examples │ ├── ExamplesList.js │ ├── data.js │ └── index.js │ ├── Header │ ├── components │ │ ├── HeaderButton.js │ │ └── HelpButton.js │ ├── containers │ │ ├── ExamplesButton │ │ │ └── index.js │ │ ├── HeaderMenu │ │ │ └── index.js │ │ ├── ShareButton │ │ │ ├── components │ │ │ │ ├── ShareButton.js │ │ │ │ └── share.svg │ │ │ └── index.js │ │ └── TitleInput │ │ │ └── index.js │ └── index.js │ ├── Scene.js │ ├── UserControls │ ├── components │ │ └── ControlsDrawer.js │ └── index.js │ ├── actions.js │ └── index.js ├── data └── db │ ├── .gitignore │ └── README.md ├── package-lock.json ├── package.json └── server ├── .eslintrc.json ├── dotenv_template ├── examples ├── animate_camera.json ├── calculated_visibility.json ├── color_and_visibility.json ├── derivatives.json ├── functions.json ├── horizontal_revolution_washer.json ├── labels.json ├── motion.json ├── osculating_circle.json ├── ruled_hyperboloid.json ├── sliders_intro.json ├── sliders_plotrange.json ├── sphere_colormap.json ├── tnb.json ├── vectors.json ├── vertical_revolution_shell_method.json └── z_bias.json ├── migrations ├── create_database.sql └── database_setup.sql ├── package-lock.json ├── package.json ├── src ├── database │ ├── attachDb.js │ ├── getDb.js │ ├── index.js │ ├── mongoose.config.js │ └── seedDb.js ├── graph │ ├── controller.js │ ├── index.js │ └── queries │ │ ├── get_graph.sql │ │ ├── index.js │ │ ├── insert_graph.sql │ │ └── upsert_graph.sql ├── index.js ├── script.js ├── scripts │ ├── fix_prefix.js │ └── to_postgres.js ├── server.js └── utils │ └── index.js └── webpack.config.server.js /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | concurrency: production 4 | 5 | on: workflow_dispatch 6 | 7 | jobs: 8 | deployment: 9 | runs-on: ubuntu-latest 10 | defaults: 11 | run: 12 | working-directory: client 13 | environment: production 14 | env: 15 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 16 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 17 | AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }} 18 | AWS_S3_BUCKET: ${{ vars.AWS_S3_BUCKET }} 19 | SKIP_YARN_COREPACK_CHECK: true 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: actions/setup-node@v4 23 | with: 24 | node-version: "16" 25 | - run: npm ci 26 | - run: npm run build 27 | env: 28 | REACT_APP_NEXT_API_CREATE_URL: ${{ vars.REACT_APP_NEXT_API_CREATE_URL }} 29 | REACT_APP_NEXT_API_RETRIEVE_URL: ${{ vars.REACT_APP_NEXT_API_RETRIEVE_URL }} 30 | REACT_APP_DISABLE_LEGACY_SAVE: ${{ vars.REACT_APP_DISABLE_LEGACY_SAVE }} 31 | - run: aws s3 cp --recursive build s3://${{ vars.AWS_S3_BUCKET }}/ 32 | - run: aws cloudfront create-invalidation --distribution-id ${{vars.CLOUDFRONT_DISTRIBUTION_ID}} --paths "/*" 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # sensitive stuff 4 | server/.env 5 | 6 | # dependencies 7 | *node_modules 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | server/build 14 | client/build 15 | 16 | # misc 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "16.3" 4 | cache: 5 | directories: 6 | - node_modules 7 | before_install: 8 | - "cd client" 9 | script: 10 | - npm test 11 | - npm run build 12 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "math3d-react", 3 | "description": "A webapp for creating and sharing interactive 3d mathematical visualizations", 4 | "scripts": { 5 | }, 6 | "env": { 7 | }, 8 | "formation": { 9 | "web": { 10 | "quantity": 1 11 | } 12 | }, 13 | "addons": [ 14 | 15 | ], 16 | "buildpacks": [ 17 | { 18 | "url": "heroku/nodejs" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /client/.env: -------------------------------------------------------------------------------- 1 | # This is to enable absolute imports, see 2 | # https://medium.com/@ktruong008/absolute-imports-with-create-react-app-4338fbca7e3d 3 | # There are better solutions (e.g., babel-plugin-root-import) but they 4 | # seem to require ejecting from create-react-app 5 | NODE_PATH=src 6 | REACT_APP_VERSION=$npm_package_version 7 | REACT_APP_NAME=$npm_package_name 8 | # REACT_APP_NEXT_API_CREATE_URL=https://api.math3d.org/v0/legacy_scenes/ 9 | # REACT_APP_NEXT_API_RETRIEVE_URL=https://api.math3d.org/v0/legacy_scenes/ 10 | # REACT_APP_DISABLE_LEGACY_SAVE=false 11 | 12 | # REACT_APP_NEXT_API_CREATE_URL=http://localhost:8000/v0/legacy_scenes/ 13 | # REACT_APP_NEXT_API_RETRIEVE_URL=http://localhost:8000/v0/legacy_scenes/ 14 | # REACT_APP_DISABLE_LEGACY_SAVE=true -------------------------------------------------------------------------------- /client/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "plugins": ["jest", "flowtype"], 8 | "extends": [ 9 | "standard", 10 | "plugin:jest/recommended", 11 | "plugin:react/recommended", 12 | "plugin:flowtype/recommended" 13 | ], 14 | "parser": "babel-eslint", 15 | "rules": { 16 | "no-unused-expressions": "off", 17 | "flowtype/no-unused-expressions": "error", 18 | "space-infix-ops": "off", 19 | "multiline-ternary": ["error", "always-multiline"], 20 | "space-in-parens": ["error", "never", { "exceptions": ["{}", "[]"] }], 21 | "object-curly-spacing": ["error", "always"], 22 | "no-use-before-define": ["error", { "functions": false }], 23 | "complexity": [1, 10], 24 | "space-before-function-paren": ["error", { 25 | "anonymous": "never", 26 | "named": "never", 27 | "asyncArrow": "always" 28 | }], 29 | "brace-style": ["error", "stroustrup", { "allowSingleLine": true }], 30 | "padded-blocks": ["error", {"classes": "always", "switches": "always"}] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /client/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | node_modules/flow-inlinestyle/index.js 7 | 8 | [lints] 9 | 10 | [options] 11 | suppress_comment= \\(.\\|\n\\)*\\$FlowFixMe 12 | suppress_comment=\\(.\\|\n\\)*\\$ExpectError 13 | include_warnings=true 14 | module.name_mapper='^utils/\([-a-zA-Z0-9$_/]+\)$' -> '/src/utils/\1' 15 | module.name_mapper='^services/\([-a-zA-Z0-9$_/]+\)$' -> '/src/services/\1' 16 | module.name_mapper='^containers/\([-a-zA-Z0-9$_/]+\)$' -> '/src/containers/\1' 17 | module.name_mapper='^components/\([-a-zA-Z0-9$_/]+\)$' -> '/src/components/\1' 18 | module.name_mapper='^constants/\([-a-zA-Z0-9$_/]+\)$' -> '/src/constants/\1' 19 | module.name_mapper='^store/\([-a-zA-Z0-9$_/]+\)$' -> '/src/store/\1' 20 | 21 | [strict] 22 | -------------------------------------------------------------------------------- /client/flow-typed/npm/babel-core_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 3e77b8b54940542b525d0b91bc2027df 2 | // flow-typed version: <>/babel-core_v^7.0.0-beta.41/flow_v0.76.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-core' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-core' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | 26 | 27 | // Filename aliases 28 | declare module 'babel-core/index' { 29 | declare module.exports: $Exports<'babel-core'>; 30 | } 31 | declare module 'babel-core/index.js' { 32 | declare module.exports: $Exports<'babel-core'>; 33 | } 34 | -------------------------------------------------------------------------------- /client/flow-typed/npm/classnames_v2.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: cf86673cc32d185bdab1d2ea90578d37 2 | // flow-typed version: 614bf49aa8/classnames_v2.x.x/flow_>=v0.25.x 3 | 4 | type $npm$classnames$Classes = 5 | | string 6 | | { [className: string]: * } 7 | | false 8 | | void 9 | | null; 10 | 11 | declare module "classnames" { 12 | declare module.exports: ( 13 | ...classes: Array<$npm$classnames$Classes | $npm$classnames$Classes[]> 14 | ) => string; 15 | } 16 | 17 | declare module "classnames/bind" { 18 | declare module.exports: $Exports<"classnames">; 19 | } 20 | 21 | declare module "classnames/dedupe" { 22 | declare module.exports: $Exports<"classnames">; 23 | } 24 | -------------------------------------------------------------------------------- /client/flow-typed/npm/color_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 111c326066ac7ba6ce7ca6a0b780376c 2 | // flow-typed version: <>/color_v^3.0.0/flow_v0.76.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'color' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'color' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | 26 | 27 | // Filename aliases 28 | declare module 'color/index' { 29 | declare module.exports: $Exports<'color'>; 30 | } 31 | declare module 'color/index.js' { 32 | declare module.exports: $Exports<'color'>; 33 | } 34 | -------------------------------------------------------------------------------- /client/flow-typed/npm/css-time_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 50555f2a57dfdad8d64c1f92b0d9922d 2 | // flow-typed version: <>/css-time_v^0.1.12/flow_v0.76.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'css-time' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'css-time' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'css-time/src/css-time' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'css-time/src/css-time.min' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'css-time/test/css-time' { 34 | declare module.exports: any; 35 | } 36 | 37 | // Filename aliases 38 | declare module 'css-time/src/css-time.js' { 39 | declare module.exports: $Exports<'css-time/src/css-time'>; 40 | } 41 | declare module 'css-time/src/css-time.min.js' { 42 | declare module.exports: $Exports<'css-time/src/css-time.min'>; 43 | } 44 | declare module 'css-time/test/css-time.js' { 45 | declare module.exports: $Exports<'css-time/test/css-time'>; 46 | } 47 | -------------------------------------------------------------------------------- /client/flow-typed/npm/enzyme-adapter-react-16_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: aa047ee4cbd20d5435b49f07c2a125b1 2 | // flow-typed version: <>/enzyme-adapter-react-16_v^1.1.1/flow_v0.76.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'enzyme-adapter-react-16' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'enzyme-adapter-react-16' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'enzyme-adapter-react-16/build/index' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'enzyme-adapter-react-16/build/ReactSixteenAdapter' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'enzyme-adapter-react-16/src/index' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'enzyme-adapter-react-16/src/ReactSixteenAdapter' { 38 | declare module.exports: any; 39 | } 40 | 41 | // Filename aliases 42 | declare module 'enzyme-adapter-react-16/build/index.js' { 43 | declare module.exports: $Exports<'enzyme-adapter-react-16/build/index'>; 44 | } 45 | declare module 'enzyme-adapter-react-16/build/ReactSixteenAdapter.js' { 46 | declare module.exports: $Exports<'enzyme-adapter-react-16/build/ReactSixteenAdapter'>; 47 | } 48 | declare module 'enzyme-adapter-react-16/src/index.js' { 49 | declare module.exports: $Exports<'enzyme-adapter-react-16/src/index'>; 50 | } 51 | declare module 'enzyme-adapter-react-16/src/ReactSixteenAdapter.js' { 52 | declare module.exports: $Exports<'enzyme-adapter-react-16/src/ReactSixteenAdapter'>; 53 | } 54 | -------------------------------------------------------------------------------- /client/flow-typed/npm/eslint-config-standard_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: cdc12f26810d160622f4366b41ccabaf 2 | // flow-typed version: <>/eslint-config-standard_v^11.0.0-beta.0/flow_v0.76.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'eslint-config-standard' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'eslint-config-standard' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'eslint-config-standard/test/basic' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'eslint-config-standard/test/validate-config' { 30 | declare module.exports: any; 31 | } 32 | 33 | // Filename aliases 34 | declare module 'eslint-config-standard/index' { 35 | declare module.exports: $Exports<'eslint-config-standard'>; 36 | } 37 | declare module 'eslint-config-standard/index.js' { 38 | declare module.exports: $Exports<'eslint-config-standard'>; 39 | } 40 | declare module 'eslint-config-standard/test/basic.js' { 41 | declare module.exports: $Exports<'eslint-config-standard/test/basic'>; 42 | } 43 | declare module 'eslint-config-standard/test/validate-config.js' { 44 | declare module.exports: $Exports<'eslint-config-standard/test/validate-config'>; 45 | } 46 | -------------------------------------------------------------------------------- /client/flow-typed/npm/flow-bin_v0.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 6a5610678d4b01e13bbfbbc62bdaf583 2 | // flow-typed version: 3817bc6980/flow-bin_v0.x.x/flow_>=v0.25.x 3 | 4 | declare module "flow-bin" { 5 | declare module.exports: string; 6 | } 7 | -------------------------------------------------------------------------------- /client/flow-typed/npm/flow_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 1769876ef8dae45943207a10ae939dcf 2 | // flow-typed version: <>/flow_v^0.2.3/flow_v0.76.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'flow' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'flow' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'flow/examples/keystore' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'flow/examples/multi' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'flow/examples/serialForEach' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'flow/examples/simple' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'flow/flow' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'flow/tests' { 46 | declare module.exports: any; 47 | } 48 | 49 | // Filename aliases 50 | declare module 'flow/examples/keystore.js' { 51 | declare module.exports: $Exports<'flow/examples/keystore'>; 52 | } 53 | declare module 'flow/examples/multi.js' { 54 | declare module.exports: $Exports<'flow/examples/multi'>; 55 | } 56 | declare module 'flow/examples/serialForEach.js' { 57 | declare module.exports: $Exports<'flow/examples/serialForEach'>; 58 | } 59 | declare module 'flow/examples/simple.js' { 60 | declare module.exports: $Exports<'flow/examples/simple'>; 61 | } 62 | declare module 'flow/flow.js' { 63 | declare module.exports: $Exports<'flow/flow'>; 64 | } 65 | declare module 'flow/tests.js' { 66 | declare module.exports: $Exports<'flow/tests'>; 67 | } 68 | -------------------------------------------------------------------------------- /client/flow-typed/npm/immutability-helper_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: df24306d9e75ec734ce2ad6ea5a5b89b 2 | // flow-typed version: <>/immutability-helper_v^2.6.6/flow_v0.76.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'immutability-helper' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'immutability-helper' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'immutability-helper/test' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'immutability-helper/index' { 31 | declare module.exports: $Exports<'immutability-helper'>; 32 | } 33 | declare module 'immutability-helper/index.js' { 34 | declare module.exports: $Exports<'immutability-helper'>; 35 | } 36 | declare module 'immutability-helper/test.js' { 37 | declare module.exports: $Exports<'immutability-helper/test'>; 38 | } 39 | -------------------------------------------------------------------------------- /client/flow-typed/npm/jest-diff_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 41c1f3f35a845b91c6032797017e80c6 2 | // flow-typed version: <>/jest-diff_v^23.0.0/flow_v0.76.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'jest-diff' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'jest-diff' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'jest-diff/build/constants' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'jest-diff/build/diff_strings' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'jest-diff/build/index' { 34 | declare module.exports: any; 35 | } 36 | 37 | // Filename aliases 38 | declare module 'jest-diff/build/constants.js' { 39 | declare module.exports: $Exports<'jest-diff/build/constants'>; 40 | } 41 | declare module 'jest-diff/build/diff_strings.js' { 42 | declare module.exports: $Exports<'jest-diff/build/diff_strings'>; 43 | } 44 | declare module 'jest-diff/build/index.js' { 45 | declare module.exports: $Exports<'jest-diff/build/index'>; 46 | } 47 | -------------------------------------------------------------------------------- /client/flow-typed/npm/jest-get-type_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 50ddc9f8837792b00402639501d3f73e 2 | // flow-typed version: <>/jest-get-type_v^22.4.3/flow_v0.76.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'jest-get-type' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'jest-get-type' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'jest-get-type/build/index' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'jest-get-type/build/index.js' { 31 | declare module.exports: $Exports<'jest-get-type/build/index'>; 32 | } 33 | -------------------------------------------------------------------------------- /client/flow-typed/npm/jest-matcher-deep-close-to_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: e1ac2c4e693aa05a39efa833edbb27eb 2 | // flow-typed version: <>/jest-matcher-deep-close-to_v^1.0.2/flow_v0.76.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'jest-matcher-deep-close-to' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'jest-matcher-deep-close-to' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'jest-matcher-deep-close-to/lib/index' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'jest-matcher-deep-close-to/src/__tests__/test' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'jest-matcher-deep-close-to/src/index' { 34 | declare module.exports: any; 35 | } 36 | 37 | // Filename aliases 38 | declare module 'jest-matcher-deep-close-to/lib/index.js' { 39 | declare module.exports: $Exports<'jest-matcher-deep-close-to/lib/index'>; 40 | } 41 | declare module 'jest-matcher-deep-close-to/src/__tests__/test.js' { 42 | declare module.exports: $Exports<'jest-matcher-deep-close-to/src/__tests__/test'>; 43 | } 44 | declare module 'jest-matcher-deep-close-to/src/index.js' { 45 | declare module.exports: $Exports<'jest-matcher-deep-close-to/src/index'>; 46 | } 47 | -------------------------------------------------------------------------------- /client/flow-typed/npm/mathquill_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 16bc39d5b47f70acdc0254b15923c97a 2 | // flow-typed version: <>/mathquill_v^0.10.1/flow_v0.76.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'mathquill' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'mathquill' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'mathquill/build/mathquill' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'mathquill/build/mathquill.min' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'mathquill/formula/formula' { 34 | declare module.exports: any; 35 | } 36 | 37 | // Filename aliases 38 | declare module 'mathquill/build/mathquill.js' { 39 | declare module.exports: $Exports<'mathquill/build/mathquill'>; 40 | } 41 | declare module 'mathquill/build/mathquill.min.js' { 42 | declare module.exports: $Exports<'mathquill/build/mathquill.min'>; 43 | } 44 | declare module 'mathquill/formula/formula.js' { 45 | declare module.exports: $Exports<'mathquill/formula/formula'>; 46 | } 47 | -------------------------------------------------------------------------------- /client/flow-typed/npm/react-collapsible_v2.0.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 4d0e3beb9327176f2e78be75bf618efc 2 | // flow-typed version: 41cec91915/react-collapsible_v2.0.x/flow_>=v0.54.x 3 | 4 | declare module "react-collapsible" { 5 | declare type Props = { 6 | trigger: string | React$Node, 7 | triggerWhenOpen?: string | React$Node, 8 | triggerDisabled?: false, 9 | transitionTime?: number, 10 | easing?: string, 11 | open?: boolean, 12 | accordionPosition?: string, 13 | handleTriggerClick?: (accordionPosition?: string | number) => void, 14 | onOpen?: () => void, 15 | onClose?: () => void, 16 | onOpening?: () => void, 17 | onClosing?: () => void, 18 | lazyRender?: boolean, 19 | overflowWhenOpen?: 20 | | "hidden" 21 | | "visible" 22 | | "auto" 23 | | "scroll" 24 | | "inherit" 25 | | "initial" 26 | | "unset", 27 | triggerSibling?: React$Node, 28 | classParentString?: string, 29 | className?: string, 30 | openedClassName?: string, 31 | triggerClassName?: string, 32 | triggerOpenedClassName?: string, 33 | contentOuterClassName?: string, 34 | contentInnerClassName?: string 35 | }; 36 | 37 | declare class Collapsible extends React$Component {} 38 | declare module.exports: typeof Collapsible; 39 | } 40 | -------------------------------------------------------------------------------- /client/flow-typed/npm/react-textarea-autosize_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 6ebe8fd7364e885e67879f75cb71eda3 2 | // flow-typed version: <>/react-textarea-autosize_v^6.0.0/flow_v0.76.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'react-textarea-autosize' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'react-textarea-autosize' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'react-textarea-autosize/dist/react-textarea-autosize.cjs' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'react-textarea-autosize/dist/react-textarea-autosize.es' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'react-textarea-autosize/dist/react-textarea-autosize' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'react-textarea-autosize/dist/react-textarea-autosize.min' { 38 | declare module.exports: any; 39 | } 40 | 41 | // Filename aliases 42 | declare module 'react-textarea-autosize/dist/react-textarea-autosize.cjs.js' { 43 | declare module.exports: $Exports<'react-textarea-autosize/dist/react-textarea-autosize.cjs'>; 44 | } 45 | declare module 'react-textarea-autosize/dist/react-textarea-autosize.es.js' { 46 | declare module.exports: $Exports<'react-textarea-autosize/dist/react-textarea-autosize.es'>; 47 | } 48 | declare module 'react-textarea-autosize/dist/react-textarea-autosize.js' { 49 | declare module.exports: $Exports<'react-textarea-autosize/dist/react-textarea-autosize'>; 50 | } 51 | declare module 'react-textarea-autosize/dist/react-textarea-autosize.min.js' { 52 | declare module.exports: $Exports<'react-textarea-autosize/dist/react-textarea-autosize.min'>; 53 | } 54 | -------------------------------------------------------------------------------- /client/flow-typed/npm/redux-batched-actions_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: b102ae517184f0239bf2264bbc706dd4 2 | // flow-typed version: <>/redux-batched-actions_v^0.3.0/flow_v0.76.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'redux-batched-actions' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'redux-batched-actions' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'redux-batched-actions/lib/index' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'redux-batched-actions/test/index-test' { 30 | declare module.exports: any; 31 | } 32 | 33 | // Filename aliases 34 | declare module 'redux-batched-actions/lib/index.js' { 35 | declare module.exports: $Exports<'redux-batched-actions/lib/index'>; 36 | } 37 | declare module 'redux-batched-actions/test/index-test.js' { 38 | declare module.exports: $Exports<'redux-batched-actions/test/index-test'>; 39 | } 40 | -------------------------------------------------------------------------------- /client/flow-typed/npm/redux-thunk_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: bc11cc2d0959c3c563b9ce36d65ef9f4 2 | // flow-typed version: <>/redux-thunk_v^2.2.0/flow_v0.76.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'redux-thunk' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'redux-thunk' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'redux-thunk/dist/redux-thunk' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'redux-thunk/dist/redux-thunk.min' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'redux-thunk/es/index' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'redux-thunk/lib/index' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'redux-thunk/src/index' { 42 | declare module.exports: any; 43 | } 44 | 45 | // Filename aliases 46 | declare module 'redux-thunk/dist/redux-thunk.js' { 47 | declare module.exports: $Exports<'redux-thunk/dist/redux-thunk'>; 48 | } 49 | declare module 'redux-thunk/dist/redux-thunk.min.js' { 50 | declare module.exports: $Exports<'redux-thunk/dist/redux-thunk.min'>; 51 | } 52 | declare module 'redux-thunk/es/index.js' { 53 | declare module.exports: $Exports<'redux-thunk/es/index'>; 54 | } 55 | declare module 'redux-thunk/lib/index.js' { 56 | declare module.exports: $Exports<'redux-thunk/lib/index'>; 57 | } 58 | declare module 'redux-thunk/src/index.js' { 59 | declare module.exports: $Exports<'redux-thunk/src/index'>; 60 | } 61 | -------------------------------------------------------------------------------- /client/flow-typed/npm/shallow-diff_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 8ce803d4a9203def76aed4aba74d954e 2 | // flow-typed version: <>/shallow-diff_v0.0.5/flow_v0.76.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'shallow-diff' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'shallow-diff' { 17 | declare module.exports: (base: {}, compared: {} ) => { 18 | unchanged: Array, 19 | updated: Array, 20 | deleted: Array, 21 | added: Array 22 | }; 23 | } 24 | 25 | /** 26 | * We include stubs for each file inside this npm package in case you need to 27 | * require those files directly. Feel free to delete any files that aren't 28 | * needed. 29 | */ 30 | 31 | 32 | // Filename aliases 33 | declare module 'shallow-diff/index' { 34 | declare module.exports: $Exports<'shallow-diff'>; 35 | } 36 | declare module 'shallow-diff/index.js' { 37 | declare module.exports: $Exports<'shallow-diff'>; 38 | } 39 | -------------------------------------------------------------------------------- /client/flow-typed/npm/tarjan-graph_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 6eb907beb5b688ae85f36e98791c25b5 2 | // flow-typed version: <>/tarjan-graph_v^1.0.1/flow_v0.76.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'tarjan-graph' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'tarjan-graph' { 17 | declare class Vertex { 18 | constructor(name: string, successors: Array): Vertex, 19 | name: string, 20 | successors: Array, 21 | index: number, 22 | lowLink: number, 23 | onStack: bool, 24 | visited: bool, 25 | reset(): void 26 | } 27 | 28 | declare class Graph { 29 | constructor(): Graph, 30 | vertices: Array, 31 | add(key: string, descendants: Array | string): Graph, 32 | reset(): void, 33 | addAndVerify(key: string, descendants: Array | string): Graph, 34 | dfs(visitor: (Vertex) => void): void, 35 | getDescendants(string): Array, 36 | hasCycles(): bool, 37 | getStronglyConnectedComponents(): Array>, 38 | getCycles(): Array>, 39 | clone(): Graph, 40 | toDot(): string 41 | } 42 | 43 | declare export default typeof Graph; 44 | } 45 | 46 | /** 47 | * We include stubs for each file inside this npm package in case you need to 48 | * require those files directly. Feel free to delete any files that aren't 49 | * needed. 50 | */ 51 | 52 | 53 | // Filename aliases 54 | declare module 'tarjan-graph/index' { 55 | declare module.exports: $Exports<'tarjan-graph'>; 56 | } 57 | declare module 'tarjan-graph/index.js' { 58 | declare module.exports: $Exports<'tarjan-graph'>; 59 | } 60 | -------------------------------------------------------------------------------- /client/flow-typed/npm/toposort_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 581f32279bd34c9af0fad5cb87148b46 2 | // flow-typed version: <>/toposort_v^1.0.6/flow_v0.76.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'toposort' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'toposort' { 17 | declare module.exports: (Array<[string, string]>) => Array 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'toposort/test' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'toposort/index' { 31 | declare module.exports: $Exports<'toposort'>; 32 | } 33 | declare module 'toposort/index.js' { 34 | declare module.exports: $Exports<'toposort'>; 35 | } 36 | declare module 'toposort/test.js' { 37 | declare module.exports: $Exports<'toposort/test'>; 38 | } 39 | -------------------------------------------------------------------------------- /client/flow-typed/npm/why-did-you-update_v0.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 2991c1bc398f4e5a314e188fa39d27f2 2 | // flow-typed version: 1ad2a03c70/why-did-you-update_v0.x.x/flow_>=v0.41.x 3 | 4 | import * as React from "react"; 5 | 6 | declare module "why-did-you-update" { 7 | declare export function whyDidYouUpdate( 8 | React: { 9 | Component: typeof React$Component 10 | }, 11 | options?: Object 12 | ): void; 13 | } 14 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherChudzicki/math3d-react/a49520cfe783e8557641fa795a39ebfd11f05ee8/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/fonts/Symbola-basic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherChudzicki/math3d-react/a49520cfe783e8557641fa795a39ebfd11f05ee8/client/public/fonts/Symbola-basic.eot -------------------------------------------------------------------------------- /client/public/fonts/Symbola-basic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherChudzicki/math3d-react/a49520cfe783e8557641fa795a39ebfd11f05ee8/client/public/fonts/Symbola-basic.ttf -------------------------------------------------------------------------------- /client/public/fonts/Symbola-basic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherChudzicki/math3d-react/a49520cfe783e8557641fa795a39ebfd11f05ee8/client/public/fonts/Symbola-basic.woff -------------------------------------------------------------------------------- /client/public/fonts/Symbola-basic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherChudzicki/math3d-react/a49520cfe783e8557641fa795a39ebfd11f05ee8/client/public/fonts/Symbola-basic.woff2 -------------------------------------------------------------------------------- /client/public/fonts/Symbola.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherChudzicki/math3d-react/a49520cfe783e8557641fa795a39ebfd11f05ee8/client/public/fonts/Symbola.eot -------------------------------------------------------------------------------- /client/public/fonts/Symbola.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherChudzicki/math3d-react/a49520cfe783e8557641fa795a39ebfd11f05ee8/client/public/fonts/Symbola.ttf -------------------------------------------------------------------------------- /client/public/fonts/Symbola.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherChudzicki/math3d-react/a49520cfe783e8557641fa795a39ebfd11f05ee8/client/public/fonts/Symbola.woff -------------------------------------------------------------------------------- /client/public/fonts/Symbola.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristopherChudzicki/math3d-react/a49520cfe783e8557641fa795a39ebfd11f05ee8/client/public/fonts/Symbola.woff2 -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import MainView from './views/MainView' 3 | import { Switch, Route } from 'react-router-dom' 4 | 5 | class App extends Component { 6 | 7 | legacyReRoute( { match } ) { 8 | // Redirect users to the old site 9 | window.location = `https://math3d.herokuapp.com/graph/${match.params.id}` 10 | } 11 | 12 | renderGraph( { match } ) { 13 | return ( 14 | 15 | ) 16 | } 17 | 18 | render() { 19 | // NOTE: I tried path='/load/:graph' for the routes, but got strange errors 20 | // that are possibly related to hacky mathbox imports. 21 | // This seems to work... 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | ) 29 | } 30 | 31 | } 32 | 33 | export default App 34 | -------------------------------------------------------------------------------- /client/src/components/EditableDescription/getTextWidth.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | type GetTextWidth = { 3 | (text: string, font: string): number, 4 | canvas?: HTMLCanvasElement 5 | } 6 | 7 | export const getTextWidth: GetTextWidth = function getTextWidth(text, font) { 8 | // from https://stackoverflow.com/a/21015393/2747370 9 | // re-use canvas object for better performance 10 | const canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement('canvas')) 11 | const context = canvas.getContext('2d') 12 | context.font = font 13 | const metrics = context.measureText(text) 14 | const roundedWidth = Math.floor(metrics.width) + 1 15 | return roundedWidth 16 | } 17 | 18 | window.getTextWidth = getTextWidth 19 | -------------------------------------------------------------------------------- /client/src/components/FlexContainer/index.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export default styled.div` 4 | display:flex; 5 | height:100%; 6 | ` 7 | -------------------------------------------------------------------------------- /client/src/components/MathBox/MathBox.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react' 3 | import { timeout } from 'utils/functions' 4 | import LoopManager from 'services/LoopManager' 5 | 6 | type Props = { 7 | mathbox: any, 8 | children: React.Node 9 | } 10 | 11 | export class MathBox extends React.PureComponent { 12 | 13 | mathboxNode = this.props.mathbox 14 | loopManager: LoopManager 15 | updateSymbol = Symbol('update marker') 16 | 17 | componentDidMount() { 18 | this.loopManager = new LoopManager(this.mathboxNode.three) 19 | } 20 | 21 | componentWillUnmount() { 22 | this.loopManager.unbindEventListeners() 23 | } 24 | 25 | // handles entering/exiting slow mode 26 | async componentDidUpdate() { 27 | const updateSymbol = Symbol('update marker') 28 | this.updateSymbol = updateSymbol 29 | this.loopManager.exitSlowMode() 30 | 31 | await timeout(100) 32 | if (this.updateSymbol === updateSymbol) { 33 | this.loopManager.enterSlowMode() 34 | } 35 | 36 | } 37 | 38 | render() { 39 | if (!this.props.children) { 40 | return null 41 | } 42 | return React.Children.map( 43 | this.props.children, 44 | child => React.cloneElement(child, { 45 | mathboxParent: this.mathboxNode, 46 | mathbox: this.mathboxNode 47 | } ) 48 | ) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /client/src/components/MathBox/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { MathBox } from './MathBox' 3 | export * from './MathBoxComponents' 4 | -------------------------------------------------------------------------------- /client/src/constants/colors.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Color } from "three/src/math/Color.js"; 4 | 5 | export const colors = [ 6 | '#33FF00', 7 | '#2ecc71', 8 | '#3498db', 9 | '#9b59b6', 10 | '#8e44ad', 11 | '#2c3e50', 12 | '#f1c40f', 13 | '#e67e22', 14 | '#e74c3c', 15 | '#808080' 16 | ] 17 | 18 | export const colorMaps = { 19 | rainbow: { 20 | css: `background: linear-gradient( 21 | to right, 22 | hsl(360, 100%, 50%), 23 | hsl(300, 100%, 50%), 24 | hsl(240, 100%, 50%), 25 | hsl(180, 100%, 50%), 26 | hsl(120, 100%, 50%), 27 | hsl(60, 100%, 50%), 28 | hsl(0, 100%, 50%) 29 | ) 30 | `, 31 | func: (frac: number) => { 32 | const color = new Color(0xffffff) 33 | color.setHSL(1 - frac, 1, 0.5) 34 | return [color.r, color.g, color.b, 1.0] 35 | } 36 | }, 37 | bluered: { 38 | css: 'background: linear-gradient(to right, blue, red)', 39 | func: (frac: number) => { 40 | return [frac, 0, 1 - frac, 1] 41 | } 42 | }, 43 | temperature: { 44 | css: `background: linear-gradient( 45 | to right, 46 | hsl(240, 100%, 50%), 47 | hsl(180, 100%, 50%), 48 | hsl(120, 100%, 50%), 49 | hsl(60, 100%, 50%), 50 | hsl(0, 100%, 50%) 51 | ) 52 | `, 53 | func: (frac: number) => { 54 | const color = new Color(0xffffff) 55 | color.setHSL(0.666*(1 - frac), 1, 0.5) 56 | return [color.r, color.g, color.b, 1.0] 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /client/src/constants/idGenerator.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | class SequentialIdGenerator { 3 | 4 | _lastId = 0 5 | 6 | next() { 7 | this._lastId += 1 8 | return String(this._lastId) 9 | } 10 | 11 | setNextId(id: number) { 12 | this._lastId = id - 1 13 | } 14 | 15 | } 16 | 17 | const idGenerator = new SequentialIdGenerator() 18 | 19 | export default idGenerator 20 | -------------------------------------------------------------------------------- /client/src/constants/index.js: -------------------------------------------------------------------------------- 1 | export const HEADER_HEIGHT_PX = 48; -------------------------------------------------------------------------------- /client/src/constants/parsing.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { 3 | Parser, 4 | ScopeEvaluator 5 | } from 'utils/mathParsing' 6 | 7 | export const parser = new Parser() 8 | export const scopeEvaluator = new ScopeEvaluator(parser) 9 | 10 | window.parser=parser 11 | -------------------------------------------------------------------------------- /client/src/constants/theme.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export const theme = { 4 | borderRadius: '4px', 5 | transitionDuration: '300ms', 6 | transitionDurationMS: 300, 7 | transitionTimingFunction: 'cubic-bezier(0.645, 0.045, 0.355, 1);', 8 | // From ant design 9 | gray: [ 10 | '#ffffff', 11 | '#fafafa', 12 | '#f5f5f5', 13 | '#e8e8e8', 14 | '#d9d9d9', 15 | '#bfbfbf', 16 | '#8c8c8c', 17 | '#595959', 18 | '#262626', 19 | '#000000' 20 | ], 21 | // from ant design, daybreak blue 22 | primary: [ 23 | '#e6f7ff', 24 | '#bae7ff', 25 | '#91d5ff', 26 | '#69c0ff', 27 | '#40a9ff', 28 | '#1890ff', 29 | '#096dd9', 30 | '#0050b3', 31 | '#003a8c', 32 | '#002766' 33 | ] 34 | } 35 | 36 | export const MOBILE_BREAKPOINT = 480 37 | 38 | export default theme 39 | -------------------------------------------------------------------------------- /client/src/containers/ControlledTabs/actions.js: -------------------------------------------------------------------------------- 1 | export const SET_ACTIVE_TAB = 'SET_ACTIVE_TAB' 2 | 3 | export function setActiveTab(id, activeTab) { 4 | return { 5 | type: SET_ACTIVE_TAB, 6 | payload: { activeTab, id } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /client/src/containers/ControlledTabs/components/ControlledTabs.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react' 3 | import { Tabs } from 'antd' 4 | 5 | type Props = { 6 | id: string, 7 | setActiveTab: (id: string, activeKey: string) => void 8 | } 9 | 10 | export default class ControlledTabs extends React.PureComponent { 11 | 12 | onChange = (activeKey: string) => { 13 | this.props.setActiveTab(this.props.id, activeKey) 14 | } 15 | 16 | render() { 17 | const { id, ...otherProps } = this.props 18 | return ( 19 | 20 | ) 21 | } 22 | 23 | } 24 | 25 | export const TabPane = Tabs.TabPane 26 | -------------------------------------------------------------------------------- /client/src/containers/ControlledTabs/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react' 3 | import { connect } from 'react-redux' 4 | import { setActiveTab } from './actions' 5 | import { Tabs } from 'antd' 6 | 7 | // TabProps is missing some props 8 | type TabProps = {| 9 | activeKey: string 10 | |} 11 | type OwnProps = {| 12 | id: string, 13 | ...TabProps 14 | |} 15 | type DispatchProps = {| 16 | setActiveTab: (id: string, activeKey: string) => void 17 | |} 18 | type Props = { 19 | ...OwnProps, 20 | ...DispatchProps 21 | } 22 | 23 | class _ControlledTabs extends React.PureComponent { 24 | 25 | onChange = (activeKey: string) => { 26 | this.props.setActiveTab(this.props.id, activeKey) 27 | } 28 | 29 | render() { 30 | const { id, ...otherProps } = this.props 31 | return ( 32 | 33 | ) 34 | } 35 | 36 | } 37 | 38 | const mapStateToProps = ( { tabs }, ownProps) => ( { 39 | activeKey: tabs[ownProps.id].activeTab 40 | } ) 41 | 42 | const mapDispatchToProps = { setActiveTab } 43 | 44 | export default connect(mapStateToProps, mapDispatchToProps)(_ControlledTabs) 45 | export const TabPane = Tabs.TabPane 46 | -------------------------------------------------------------------------------- /client/src/containers/ControlledTabs/reducer.js: -------------------------------------------------------------------------------- 1 | import { SET_ACTIVE_TAB } from './actions' 2 | import { CREATE_MATH_OBJECT } from 'containers/MathObjects/actions' 3 | const initialState = { 4 | controls: { 5 | activeTab: '1' 6 | }, 7 | examples: { 8 | activeTab: '1' 9 | } 10 | } 11 | 12 | export default function tabs(state = initialState, action) { 13 | const { type, payload } = action 14 | 15 | switch (type) { 16 | 17 | case SET_ACTIVE_TAB: { 18 | const { id, activeTab } = payload 19 | return { 20 | ...state, 21 | [id]: { activeTab } 22 | } 23 | } 24 | case CREATE_MATH_OBJECT: { 25 | 26 | return state.controls.activeTab === '1' 27 | ? state 28 | : { ...state, controls: { activeTab: '1' } } 29 | } 30 | default: { 31 | return state 32 | } 33 | 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /client/src/containers/ControllerHeader/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import ControllerHeader from './components/ControllerHeader' 3 | import type { Props, OwnProps } from './components/ControllerHeader' 4 | import { connect } from 'react-redux' 5 | import { getActiveFolder } from './selectors' 6 | import { createMathObject } from 'containers/MathObjects/actions' 7 | import { setActiveObject } from 'containers/MathObjects/services/activeObject/actions' 8 | import { setContentCollapsed } from 'containers/MathObjects/Folder/actions' 9 | 10 | const mapStateToProps = ( { activeObject, sortableTree } ) => { 11 | const treeRoot = sortableTree.root 12 | const activeFolder = getActiveFolder(sortableTree, activeObject) 13 | 14 | if (Object.keys(treeRoot).length === 0) { 15 | throw Error('root folder requires at least 1 child') 16 | } 17 | 18 | // If no active folder, insert new items into last folder 19 | const targetFolder = activeFolder || treeRoot[treeRoot.length - 1] 20 | const newFolderIndex = treeRoot.indexOf(targetFolder) + 1 21 | 22 | const newItemIndex = sortableTree[targetFolder].includes(activeObject) 23 | ? sortableTree[targetFolder].indexOf(activeObject) + 1 24 | : 0 25 | 26 | return { 27 | targetFolder, 28 | newFolderIndex, 29 | newItemIndex 30 | } 31 | } 32 | 33 | const mapDispatchToProps = ( { 34 | createMathObject, 35 | setActiveObject, 36 | setContentCollapsed 37 | } ) 38 | 39 | export default connect( 40 | mapStateToProps, mapDispatchToProps 41 | )(ControllerHeader) 42 | -------------------------------------------------------------------------------- /client/src/containers/ControllerHeader/selectors.js: -------------------------------------------------------------------------------- 1 | import { getParent } from 'containers/MathObjects/selectors' 2 | 3 | const FORBIDDEN_INSERT_FOLDERS = new Set( [ 4 | 'axes', 5 | 'cameraFolder' 6 | ] ) 7 | 8 | /** 9 | * gets the folder corresponding to activeObject, which is: 10 | * - the id of the folder containing activeObject if activeObject 11 | * - null if activeObject is null 12 | * 13 | * @param {Object} tree maps folderIds to objectIds 14 | * @param {?string} activeObject id of activeObject 15 | * @return {?string} string of folder corresponding to activeObject 16 | */ 17 | export function getActiveFolder(tree, activeObject) { 18 | if (activeObject === null) { 19 | return null 20 | } 21 | if (tree[activeObject] ) { 22 | return FORBIDDEN_INSERT_FOLDERS.has(activeObject) ? null : activeObject 23 | } 24 | // getParent will throw an error if no parent or multiple parents 25 | const parent = getParent(tree, activeObject) 26 | 27 | return FORBIDDEN_INSERT_FOLDERS.has(parent) ? null : parent 28 | } 29 | -------------------------------------------------------------------------------- /client/src/containers/ControllerHeader/selectors.test.js: -------------------------------------------------------------------------------- 1 | import { getContainingArrayKeys, getActiveFolder } from './selectors' 2 | 3 | describe('getActiveFolder', () => { 4 | const folders = { 5 | root: ['folder0', 'folder1', 'folder2', 'folder3'], 6 | folder0: ['item0', 'item1', 'item2'], 7 | folder1: ['item3', 'item4', 'item5', 'item6'], 8 | folder2: ['item7', 'item8'], 9 | folder3: ['item9', 'item10', 'item11', 'item12', 'item13', 'item15'] 10 | } 11 | 12 | test('returns null if activeObject is null', () => { 13 | const result = getActiveFolder(folders, null) 14 | const expected = null 15 | expect(result).toEqual(expected) 16 | } ) 17 | 18 | test('returns correct folder if activeObject is folder', () => { 19 | const result = getActiveFolder(folders, 'folder1') 20 | const expected = 'folder1' 21 | expect(result).toEqual(expected) 22 | } ) 23 | 24 | test('returns correct folder if activeObject is item in folder', () => { 25 | const result = getActiveFolder(folders, 'item7') 26 | const expected = 'folder2' 27 | expect(result).toEqual(expected) 28 | } ) 29 | } ) 30 | -------------------------------------------------------------------------------- /client/src/containers/Drawer/ScreenSizeDrawerManager.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react' 3 | import { connect } from 'react-redux' 4 | import withSizes from 'react-sizes' 5 | import { closeDrawer, openDrawer, setWidth } from './actions' 6 | import { DEFAULT_WIDTH } from './reducer' 7 | import { MOBILE_BREAKPOINT } from 'constants/theme' 8 | 9 | type DispatchProps = {| 10 | openDrawer: string => void, 11 | closeDrawer: string => void, 12 | setWidth: (id: string, width: string) => void, 13 | |} 14 | type OwnProps = {| 15 | isSmall: boolean, 16 | id: string 17 | |} 18 | type Props = {| 19 | ...DispatchProps, 20 | ...OwnProps 21 | |} 22 | 23 | class ScreenSizeDrawerManager extends React.PureComponent { 24 | 25 | resize() { 26 | const { id, isSmall, closeDrawer, openDrawer, setWidth } = this.props 27 | if (isSmall) { 28 | closeDrawer(id) 29 | setWidth(id, '290px') 30 | } 31 | else { 32 | openDrawer(id) 33 | setWidth(id, DEFAULT_WIDTH) 34 | } 35 | } 36 | 37 | componentDidUpdate() { 38 | this.resize() 39 | } 40 | 41 | componentDidMount() { 42 | this.resize() 43 | } 44 | 45 | render() { 46 | return null 47 | } 48 | 49 | } 50 | 51 | const mapSizesToProps = ( { width } ) => ( { isSmall: width < MOBILE_BREAKPOINT } ) 52 | const mapDispatchToProps = { openDrawer, closeDrawer, setWidth } 53 | 54 | export default connect(null, mapDispatchToProps)( 55 | withSizes(mapSizesToProps)(ScreenSizeDrawerManager) 56 | ) 57 | -------------------------------------------------------------------------------- /client/src/containers/Drawer/actions.js: -------------------------------------------------------------------------------- 1 | import cssTime from 'css-time' 2 | import Drawer from './components/Drawer' 3 | 4 | const DEFAULT_ANIMATION_SPEED = Drawer.defaultProps.animationSpeed 5 | 6 | export const SET_VISIBILITY = 'SET_VISIBILITY' 7 | export const SET_ANIMATION_STATUS = 'SET_ANIMATION_STATUS' 8 | export const OPEN_DRAWER = 'OPEN_DRAWER' 9 | export const CLOSE_DRAWER = 'CLOSE_DRAWER' 10 | export const SET_WIDTH = 'SET_WIDTH' 11 | 12 | export const setVisibility = (id, isVisible) => ( { 13 | type: SET_VISIBILITY, 14 | payload: { id, isVisible } 15 | } ) 16 | 17 | export const setWidth = (id, width) => ( { 18 | type: SET_WIDTH, 19 | payload: { id, width } 20 | } ) 21 | 22 | export const setAnimationStatus = (id, isAnimating) => ( { 23 | type: SET_ANIMATION_STATUS, 24 | payload: { id, isAnimating } 25 | } ) 26 | 27 | export const closeDrawer = (id, animationSpeed = DEFAULT_ANIMATION_SPEED) => { 28 | const msDuration = cssTime.from(animationSpeed) 29 | return dispatch => { 30 | dispatch(setVisibility(id, false)) 31 | dispatch(setAnimationStatus(id, true)) 32 | setTimeout(() => dispatch(setAnimationStatus(id, false)), msDuration) 33 | } 34 | } 35 | 36 | export const openDrawer = (id, animationSpeed = DEFAULT_ANIMATION_SPEED) => { 37 | const msDuration = cssTime.from(animationSpeed) 38 | return dispatch => { 39 | dispatch(setVisibility(id, true)) 40 | dispatch(setAnimationStatus(id, true)) 41 | setTimeout(() => dispatch(setAnimationStatus(id, false)), msDuration) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /client/src/containers/Drawer/components/DrawerToggleButton.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import PropTypes from 'prop-types' 3 | import styled, { css } from 'styled-components' 4 | import { Icon } from 'antd' 5 | import SubtleButton from 'components/SubtleButton' 6 | 7 | const StyledButton = styled(SubtleButton)` 8 | z-index:200; 9 | width:30px; 10 | height:30px; 11 | ${props => props.side === 'right' && css` 12 | margin-right: -30px; 13 | `}; 14 | ${props => props.side === 'left' && css` 15 | margin-left: -30px; 16 | padding-left: 30px; 17 | & > i { 18 | margin-left: -30px; 19 | } 20 | `}; 21 | } 22 | ` 23 | 24 | // slideTo, isDrawerOpen 25 | const buttonIcons = { 26 | left: { 27 | true: 'left', 28 | false: 'right' 29 | }, 30 | right: { 31 | true: 'right', 32 | false: 'left' 33 | } 34 | } 35 | 36 | export default class DrawerToggleButton extends PureComponent { 37 | 38 | static propTypes = { 39 | onClose: PropTypes.func.isRequired, 40 | onOpen: PropTypes.func.isRequired, 41 | isDrawerOpen: PropTypes.bool.isRequired, 42 | slideTo: PropTypes.oneOf( ['left', 'right'] ), 43 | side: PropTypes.oneOf( ['left', 'right'] ) 44 | } 45 | 46 | render() { 47 | const props = this.props 48 | const onClick = props.isDrawerOpen ? props.onClose : props.onOpen 49 | const iconType = buttonIcons[props.slideTo][props.isDrawerOpen] 50 | return ( 51 | 52 | 53 | 54 | ) 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /client/src/containers/Drawer/index.js: -------------------------------------------------------------------------------- 1 | import Drawer from './components/Drawer' 2 | import { connect } from 'react-redux' 3 | import { openDrawer, closeDrawer } from './actions' 4 | 5 | const mapStateToProps = ( { drawers }, ownProps) => ( { 6 | isOpen: drawers[ownProps.id].isVisible, 7 | isAnimating: drawers[ownProps.id].isAnimating, 8 | width: drawers[ownProps.id].width 9 | } ) 10 | 11 | const mapDispatchToProps = { 12 | onOpen: openDrawer, 13 | onClose: closeDrawer 14 | } 15 | 16 | export default connect(mapStateToProps, mapDispatchToProps)(Drawer) 17 | -------------------------------------------------------------------------------- /client/src/containers/Drawer/reducer.js: -------------------------------------------------------------------------------- 1 | import { SET_VISIBILITY, SET_ANIMATION_STATUS, SET_WIDTH } from './actions' 2 | 3 | export const DEFAULT_WIDTH = '400px' 4 | 5 | export const initialState = { 6 | main: { 7 | isVisible: true, 8 | isAnimating: false, 9 | width: DEFAULT_WIDTH 10 | }, 11 | examples: { 12 | isVisible: false, 13 | isAnimating: false, 14 | width: DEFAULT_WIDTH 15 | } 16 | } 17 | 18 | export default (state = initialState, { type, payload } ) => { 19 | switch (type) { 20 | 21 | case SET_VISIBILITY: 22 | return { 23 | ...state, 24 | [payload.id]: { ...state[payload.id], isVisible: payload.isVisible } 25 | } 26 | case SET_ANIMATION_STATUS: 27 | return { 28 | ...state, 29 | [payload.id]: { ...state[payload.id], isAnimating: payload.isAnimating } 30 | } 31 | case SET_WIDTH: { 32 | const { id, width } = payload 33 | return { 34 | ...state, 35 | [id]: { ...state[id], width } 36 | } 37 | } 38 | default: 39 | return state 40 | 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /client/src/containers/MathBoxContainer/components/MathBoxContainer.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import styled, { css } from 'styled-components' 3 | import PropTypes from 'prop-types' 4 | 5 | const MathBoxOuterDiv = styled.div` 6 | width: 100%; 7 | transform: translateX(${props => props.leftOffset}); 8 | transition-duration: 1s; 9 | & ${props => !props.isAnimating && css` 10 | transition-duration: 0s; 11 | `} 12 | ` 13 | 14 | /** 15 | * This is a wrapper around the MathBox div with a few props to influence 16 | * positioning. 17 | * 18 | * @type {PureComponent} 19 | */ 20 | 21 | export default class MathBoxContainer extends PureComponent { 22 | 23 | static propTypes = { 24 | leftOffset: PropTypes.string.isRequired, 25 | mathboxElement: PropTypes.instanceOf(Element), 26 | children: PropTypes.oneOfType( [ 27 | PropTypes.arrayOf(PropTypes.node), 28 | PropTypes.node 29 | ] ) 30 | } 31 | 32 | assignRef = ref => { 33 | this.ref = ref 34 | } 35 | 36 | componentDidMount() { 37 | const mathboxElement = this.props.mathboxElement 38 | mathboxElement.parentNode.removeChild(mathboxElement) 39 | this.ref.appendChild(mathboxElement) 40 | } 41 | 42 | render() { 43 | return ( 44 | 47 | {this.props.children} 48 | 49 | ) 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /client/src/containers/MathBoxContainer/index.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import MathBoxContainer from './components/MathBoxContainer' 3 | 4 | function getLeftAnimationStatus(state) { 5 | return state.drawers.main.isAnimating 6 | } 7 | 8 | function getLeftOffset(state) { 9 | const width = 400 10 | const { isVisible } = state.drawers.main 11 | return isVisible ? `${-width / 2}px` : '0px' 12 | } 13 | 14 | const mapStateToProps = (state, ownProps) => ( { 15 | leftOffset: getLeftOffset(state), 16 | isAnimating: getLeftAnimationStatus(state) 17 | } ) 18 | 19 | export default connect(mapStateToProps)(MathBoxContainer) 20 | -------------------------------------------------------------------------------- /client/src/containers/MathBoxScene/index.js: -------------------------------------------------------------------------------- 1 | import MathBoxScene from './components/MathBoxScene' 2 | import { connect } from 'react-redux' 3 | import { setError } from 'services/errors' 4 | 5 | const mapStateToProps = ( { mathGraphics, evalErrors, renderErrors } ) => ( { 6 | mathGraphics, 7 | evalErrors, 8 | renderErrors, 9 | order: Object.keys(mathGraphics) 10 | } ) 11 | 12 | const mapDispatchToProps = { 13 | setError 14 | } 15 | 16 | export default connect(mapStateToProps, mapDispatchToProps)(MathBoxScene) 17 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/Folder/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | toggleProperty, 3 | setProperty 4 | } from 'containers/MathObjects/actions' 5 | import { FOLDER } from './metadata' 6 | 7 | export const setContentCollapsed = (id, value) => { 8 | return setProperty(id, FOLDER, 'isCollapsed', value) 9 | } 10 | 11 | export const toggleContentCollapsed = (id) => { 12 | return toggleProperty(id, FOLDER, 'isCollapsed') 13 | } 14 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/Folder/components/CollapsedIndicator.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Icon } from 'antd' 3 | import styled from 'styled-components' 4 | import PropTypes from 'prop-types' 5 | import SubtleButton from 'components/SubtleButton' 6 | import { theme } from 'constants/theme' 7 | 8 | const CollapseIndicatorButton = styled(SubtleButton)` 9 | padding-left: 2px; 10 | padding-right: 2px; 11 | width:28px; 12 | height:28px; 13 | ` 14 | 15 | const RotatingSpan = styled.span` 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | transition-duration: ${props => `${props.animationSpeed}ms`}; 20 | &.collapsed { 21 | transform: rotate(-90deg); 22 | } 23 | ` 24 | 25 | CollapsedIndicator.propTypes = { 26 | onToggleContentCollapsed: PropTypes.func.isRequired, 27 | isCollapsed: PropTypes.bool.isRequired, 28 | animationSpeed: PropTypes.number.isRequired, 29 | lightenOnHover: PropTypes.bool.isRequired, 30 | backgroundColor: PropTypes.string.isRequired 31 | } 32 | 33 | export default function CollapsedIndicator(props) { 34 | const className = props.isCollapsed ? 'collapsed' : '' 35 | return ( 36 | 42 | 46 | 49 | 50 | 51 | 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/Folder/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { MathFolder } from '../MathObject' 3 | import Folder from './components/Folder' 4 | import { connect } from 'react-redux' 5 | import { toggleContentCollapsed } from './actions' 6 | import { getItems } from './selectors' 7 | import { FOLDER, defaultSettings } from './metadata' 8 | 9 | const mapStateToProps = (state, ownProps) => ( { 10 | items: getItems(state, ownProps.id), 11 | isCollapsed: state.folders[ownProps.id].isCollapsed, 12 | isActive: state.activeObject === ownProps.id, 13 | isDropDisabled: state.folders[ownProps.id].isDropDisabled 14 | } ) 15 | 16 | const mapDispatchToProps = { 17 | onToggleContentCollapsed: toggleContentCollapsed 18 | } 19 | 20 | export default new MathFolder( { 21 | type: FOLDER, 22 | defaultSettings: defaultSettings, 23 | uiComponent: connect(mapStateToProps, mapDispatchToProps)(Folder) 24 | } ) 25 | export { FOLDER } 26 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/Folder/metadata.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export const FOLDER = 'FOLDER' 3 | export const defaultSettings = { 4 | isCollapsed: false, 5 | isDropDisabled: false, 6 | isDragDisabled: false, // for the folder, not its items 7 | description: 'Folder' 8 | } 9 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/Folder/selectors.js: -------------------------------------------------------------------------------- 1 | import { 2 | AXIS, 3 | GRID 4 | } from 'containers/MathObjects/MathGraphics' 5 | import createCachedSelector from 're-reselect' 6 | function getType(mathGraphics, mathSymbols, id) { 7 | if (mathGraphics[id] ) { 8 | return mathGraphics[id].type 9 | } 10 | else if (mathSymbols[id] ) { 11 | return mathSymbols[id].type 12 | } 13 | else { 14 | throw Error(`Folder child item ${id} is not a mathGraphic or mathSymbols variable`) 15 | } 16 | } 17 | 18 | const DRAG_DISABLED = new Set( [AXIS, GRID] ) 19 | 20 | export const getItems = createCachedSelector( 21 | (state, folderId) => state.mathGraphics, 22 | (state, folderId) => state.mathSymbols, 23 | (state, folderId) => state.sortableTree[folderId], // itemIds 24 | (mathGraphics, mathSymbols, itemIds) => itemIds.map( 25 | id => { 26 | const type = getType(mathGraphics, mathSymbols, id) 27 | return { id, type, isDragDisabled: DRAG_DISABLED.has(type) } 28 | } 29 | ) 30 | )( 31 | (state, folderId) => folderId 32 | ) 33 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathGraphics/Axis/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { PureComponent } from 'react' 3 | import { Axis as AxisGraphic } from 'components/MathBox' 4 | import MathGraphic from '../MathGraphic' 5 | import MathGraphicUI from '../containers/MathGraphicUI' 6 | import { axisMeta } from '../metadata' 7 | import { 8 | MathInputRHS, 9 | ConnectedStaticMath 10 | } from 'containers/MathObjects/containers/MathInput' 11 | import { MainRow, Cell, Label } from 'containers/MathObjects/components' 12 | 13 | export const AXIS = 'AXIS' 14 | 15 | type Props = { 16 | id: string 17 | } 18 | 19 | const noFlex = { 20 | flex: 0 21 | } 22 | 23 | export class AxisUI extends PureComponent { 24 | 25 | render() { 26 | return ( 27 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | ) 53 | } 54 | 55 | } 56 | 57 | export default new MathGraphic( { 58 | type: AXIS, 59 | metadata: axisMeta, 60 | uiComponent: AxisUI, 61 | mathboxComponent: AxisGraphic 62 | } ) 63 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathGraphics/Camera/UseComputedToggle.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react' 3 | import { Switch } from 'antd' 4 | import { connect } from 'react-redux' 5 | import { setProperty } from 'containers/MathObjects/actions' 6 | 7 | type OwnProps = {| 8 | parentId: string, 9 | |} 10 | type StateProps = {| 11 | checked: boolean, 12 | type: string 13 | |} 14 | 15 | type DispatchProps = {| 16 | setProperty: typeof setProperty 17 | |} 18 | 19 | type Props = { 20 | ...OwnProps, 21 | ...StateProps, 22 | ...DispatchProps 23 | } 24 | 25 | class _UseComputedToggle extends React.PureComponent { 26 | 27 | onChange = (value: boolean) => { 28 | const { parentId, type } = this.props 29 | this.props.setProperty(parentId, type, 'isZoomEnabled', !value) 30 | this.props.setProperty(parentId, type, 'isRotateEnabled', !value) 31 | this.props.setProperty(parentId, type, 'isPanEnabled', false) 32 | this.props.setProperty(parentId, type, 'useComputed', value) 33 | } 34 | 35 | render() { 36 | const { parentId, type, setProperty, ...otherProps } = this.props 37 | return ( 38 | 42 | ) 43 | } 44 | 45 | } 46 | 47 | const mapStateToProps = ( { mathGraphics }: any, ownProps: OwnProps) => { 48 | const { parentId, ...otherProps } = ownProps 49 | return ( { 50 | checked: mathGraphics[parentId].useComputed, 51 | type: mathGraphics[parentId].type, 52 | ...otherProps 53 | } ) 54 | } 55 | 56 | const mapDispatchToProps = { setProperty } 57 | 58 | export default connect(mapStateToProps, mapDispatchToProps)(_UseComputedToggle) 59 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathGraphics/Grid/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { PureComponent } from 'react' 3 | import { Grid as GridGraphic } from 'components/MathBox' 4 | import MathGraphic from '../MathGraphic' 5 | import MathGraphicUI from '../containers/MathGraphicUI' 6 | import { gridMeta } from '../metadata' 7 | import { 8 | ConnectedStaticMath 9 | } from 'containers/MathObjects/containers/MathInput' 10 | import styled from 'styled-components' 11 | import { MainRow } from 'containers/MathObjects/components' 12 | 13 | export const GRID = 'GRID' 14 | 15 | type Props = { 16 | id: string 17 | } 18 | 19 | const Cell = styled.div` 20 | display: flex; 21 | flex: 1; 22 | align-items:center; 23 | ` 24 | const Label = styled.span` 25 | padding-left:5px; 26 | padding-right:5px; 27 | ` 28 | 29 | export class GridUI extends PureComponent { 30 | 31 | render() { 32 | return ( 33 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ) 47 | } 48 | 49 | } 50 | 51 | export default new MathGraphic( { 52 | type: GRID, 53 | metadata: gridMeta, 54 | uiComponent: GridUI, 55 | mathboxComponent: GridGraphic 56 | } ) 57 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathGraphics/Line/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { PureComponent } from 'react' 3 | import { Line as LineGraphic } from 'components/MathBox' 4 | import MathGraphic from '../MathGraphic' 5 | import MathGraphicUI from '../containers/MathGraphicUI' 6 | import { lineMeta } from '../metadata' 7 | import { MainRow } from 'containers/MathObjects/components' 8 | import { MathInputRHS } from 'containers/MathObjects/containers/MathInput' 9 | 10 | export const LINE = 'LINE' 11 | 12 | type Props = { 13 | id: string 14 | } 15 | 16 | export class LineUI extends PureComponent { 17 | 18 | render() { 19 | return ( 20 | 24 | 25 | 26 | 27 | 28 | ) 29 | } 30 | 31 | } 32 | 33 | export default new MathGraphic( { 34 | type: LINE, 35 | metadata: lineMeta, 36 | uiComponent: LineUI, 37 | mathboxComponent: LineGraphic 38 | } ) 39 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathGraphics/MathGraphic.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { MetaData } from './types' 3 | import { capitalize } from 'utils/helpers' 4 | import type { MathObjectWrapper, Settings, Support } from '../MathObject' 5 | 6 | /** 7 | * Contains metadata about MathGraphic and coerces it into a form consumable 8 | * by other sources 9 | */ 10 | export default class MathGraphic implements MathObjectWrapper { 11 | 12 | type: string 13 | defaultSettings: Settings 14 | uiComponent: Function 15 | mathboxComponent: Function 16 | computedProps: Array 17 | support: Support 18 | reducer = 'mathGraphics' 19 | 20 | constructor( { type, description, metadata, uiComponent, mathboxComponent, support = 'full' }: { 21 | type: string, 22 | description?: string, 23 | metadata: MetaData, 24 | uiComponent: Function, 25 | mathboxComponent: Function, 26 | support?: Support 27 | } ) { 28 | this.type = type 29 | this.support = support 30 | this.uiComponent = uiComponent 31 | this.mathboxComponent = mathboxComponent 32 | this.defaultSettings = MathGraphic.getDefaultSettings(type, metadata, description) 33 | this.computedProps = Object.keys(metadata) 34 | .filter(key => metadata[key].inputType === 'math') 35 | } 36 | 37 | static getDefaultSettings(type: string, metadata: MetaData, description: ?string) { 38 | const initial = { 39 | type, 40 | description: description || capitalize(type), 41 | useCalculatedVisibility: false 42 | } 43 | return Object.keys(metadata).reduce((acc, property) => { 44 | acc[property] = metadata[property].defaultValue 45 | return acc 46 | }, initial) 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathGraphics/ParametricCurve/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { PureComponent } from 'react' 3 | import { ParametricCurve as ParametricCurveGraphic } from 'components/MathBox' 4 | import MathGraphic from '../MathGraphic' 5 | import MathGraphicUI from '../containers/MathGraphicUI' 6 | import { parametricCurveMeta } from '../metadata' 7 | import { MainRow } from 'containers/MathObjects/components' 8 | import { 9 | MathInputRHS, 10 | StaticMathStyled 11 | } from 'containers/MathObjects/containers/MathInput' 12 | 13 | export const PARAMETRIC_CURVE = 'PARAMETRIC_CURVE' 14 | 15 | type Props = { 16 | id: string 17 | } 18 | 19 | const justifyRight = { 20 | justifyContent: 'flex-end' 21 | } 22 | const rangeStyle = { 23 | flex: 0 24 | } 25 | 26 | export class ParemetricCurveUI extends PureComponent { 27 | 28 | render() { 29 | return ( 30 | 34 | 35 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | ) 52 | } 53 | 54 | } 55 | 56 | export default new MathGraphic( { 57 | type: PARAMETRIC_CURVE, 58 | description: 'Parametric Curve', 59 | metadata: parametricCurveMeta, 60 | uiComponent: ParemetricCurveUI, 61 | mathboxComponent: ParametricCurveGraphic 62 | } ) 63 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathGraphics/ParametricSurface/containers/ParametricSurfaceStatus.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react' 3 | import { connect } from 'react-redux' 4 | import EvaluatedStatusSymbol from 'containers/MathObjects/MathGraphics/containers/EvaluatedStatusSymbol' 5 | import { colors, colorMaps } from 'constants/colors' 6 | import { Tabs } from 'antd' 7 | import { 8 | StaticMathStyled, 9 | MathInputRHS 10 | } from 'containers/MathObjects/containers/MathInput' 11 | import { ColorScale } from './components/ColorScale' 12 | 13 | const TabPane = Tabs.TabPane 14 | 15 | const extendedColors = [...colors, ...Object.keys(colorMaps)] 16 | 17 | type OwnProps = {| 18 | id: string, 19 | labelU: string, 20 | labelV: string 21 | |} 22 | type StateProps = {| 23 | color: string 24 | |} 25 | type Props = { 26 | ...OwnProps, 27 | ...StateProps 28 | } 29 | 30 | function _ParametricSurfaceStatus(props: Props) { 31 | const { id, color, labelU, labelV } = props 32 | return ( 33 | 38 | 39 |

40 | 41 |

42 | 47 |

Note: The values capital X, Y, Z represent scaled axis coordinates, and range from 0 to 1.

48 | 49 |

Color maps may look better with shaded=false.

50 | 51 | } 52 | /> 53 | ) 54 | } 55 | 56 | const mapStateToProps = ( { mathGraphics }, ownProps) => ( { 57 | color: mathGraphics[ownProps.id].color 58 | } ) 59 | 60 | export default connect(mapStateToProps)(_ParametricSurfaceStatus) 61 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathGraphics/ParametricSurface/containers/components/ColorScale.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react' 3 | import { StaticMathStyled } from 'containers/MathObjects/containers/MathInput' 4 | import styled from 'styled-components' 5 | import { colorMaps } from 'constants/colors' 6 | 7 | const ScaleContainer = styled.div` 8 | display:flex; 9 | align-items:center; 10 | margin:4px; 11 | ` 12 | const ColorBar = styled.div` 13 | flex:1; 14 | ${props => props.gradient}; 15 | background-color: ${props => props.color}; 16 | height:10px; 17 | ` 18 | 19 | type Props = { 20 | color: string 21 | } 22 | 23 | export function ColorScale(props: Props) { 24 | const { color } = props 25 | const gradient = colorMaps[color] && colorMaps[color].css 26 | return ( 27 | 28 | 29 | 30 | 31 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathGraphics/Point/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { PureComponent } from 'react' 3 | import { Point as PointGraphic } from 'components/MathBox' 4 | import MathGraphic from '../MathGraphic' 5 | import MathGraphicUI from '../containers/MathGraphicUI' 6 | import { pointMeta } from '../metadata' 7 | import { MainRow } from 'containers/MathObjects/components' 8 | import { MathInputRHS } from 'containers/MathObjects/containers/MathInput' 9 | 10 | export const POINT = 'POINT' 11 | 12 | type Props = { 13 | id: string 14 | } 15 | 16 | export class PointUI extends PureComponent { 17 | 18 | render() { 19 | return ( 20 | 24 | 25 | 26 | 27 | 28 | ) 29 | } 30 | 31 | } 32 | 33 | export default new MathGraphic( { 34 | type: POINT, 35 | metadata: pointMeta, 36 | uiComponent: PointUI, 37 | mathboxComponent: PointGraphic 38 | } ) 39 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathGraphics/Vector/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { PureComponent } from 'react' 3 | import { Vector as VectorGraphic } from 'components/MathBox' 4 | import MathGraphic from '../MathGraphic' 5 | import MathGraphicUI from '../containers/MathGraphicUI' 6 | import { vectorMeta } from '../metadata' 7 | import { MainRow } from 'containers/MathObjects/components' 8 | import { MathInputRHS } from 'containers/MathObjects/containers/MathInput' 9 | 10 | export const VECTOR = 'VECTOR' 11 | 12 | type Props = { 13 | id: string 14 | } 15 | 16 | export class VectorUI extends PureComponent { 17 | 18 | render() { 19 | return ( 20 | 24 | 25 | 26 | 27 | 28 | ) 29 | } 30 | 31 | } 32 | 33 | export default new MathGraphic( { 34 | type: VECTOR, 35 | metadata: vectorMeta, 36 | uiComponent: VectorUI, 37 | mathboxComponent: VectorGraphic 38 | } ) 39 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathGraphics/containers/EvaluatedStatusSymbol/components/StatusSymbol/components/ColorPicker.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react' 3 | import styled from 'styled-components' 4 | import ColorSquare from './ColorSquare' 5 | import { colors } from 'constants/colors' 6 | 7 | const Container = styled.div` 8 | display: grid; 9 | grid-template-columns: repeat(5, 30px); 10 | grid-auto-rows: 30px; 11 | grid-gap: 5px; 12 | ` 13 | 14 | type Props = { 15 | colors: Array, 16 | onPickColor: (color: string) => void 17 | } 18 | 19 | export default class ColorPicker extends React.Component { 20 | 21 | static defaultProps = { 22 | colors: colors 23 | } 24 | 25 | onPickColor = (color: string) => { 26 | this.props.onPickColor(color) 27 | } 28 | 29 | render() { 30 | return ( 31 | 32 | { 33 | this.props.colors.map(color => 34 | 39 | )} 40 | 41 | ) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathGraphics/containers/EvaluatedStatusSymbol/components/StatusSymbol/components/ColorSquare.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react' 3 | import styled from 'styled-components' 4 | import { colorMaps } from 'constants/colors' 5 | 6 | const Color = styled.div` 7 | ${props => colorMaps[props.color] 8 | ? colorMaps[props.color].css 9 | : `background-color: ${props.color}` 10 | }; 11 | background-color: ${props => props.color}; 12 | border-radius: 5px; 13 | width: 30px; 14 | height: 30px; 15 | cursor: pointer; 16 | &:hover { 17 | border: 1px solid black; 18 | } 19 | ` 20 | 21 | type Props = { 22 | color: string, 23 | style?: string, 24 | className?: string, 25 | onPickColor: (color: string) => void 26 | } 27 | 28 | export default class ColorSquare extends React.PureComponent { 29 | 30 | pickColor = () => { 31 | this.props.onPickColor(this.props.color) 32 | } 33 | 34 | render() { 35 | return ( 36 | 42 | ) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathGraphics/containers/EvaluatedStatusSymbol/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { OptionalizeSome } from 'utils/flow' 3 | import EvaluatedStatusSymbol from './components/EvaluatedStatusSymbol' 4 | import type { Props, OwnProps, DefaultProps } from './components/EvaluatedStatusSymbol' 5 | import { 6 | toggleProperty, 7 | setProperty 8 | } from 'containers/MathObjects/actions' 9 | 10 | // For connecting 11 | import { connect } from 'react-redux' 12 | 13 | const mapStateToProps = ( { mathGraphics }, ownProps) => { 14 | const { id } = ownProps 15 | const { 16 | color, 17 | visible, 18 | useCalculatedVisibility, 19 | calculatedVisibility, 20 | type 21 | } = mathGraphics[id] 22 | return { 23 | color, 24 | visible, 25 | useCalculatedVisibility, 26 | calculatedVisibility, 27 | type 28 | } 29 | } 30 | 31 | const mapDispatchToProps = { 32 | toggleProperty, 33 | setProperty 34 | } 35 | 36 | type ConnectedProps = OptionalizeSome 37 | type ConnectedOwnProps = OptionalizeSome 38 | 39 | export default connect( 40 | mapStateToProps, mapDispatchToProps 41 | )(EvaluatedStatusSymbol) 42 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathGraphics/containers/MathGraphicUI/index.js: -------------------------------------------------------------------------------- 1 | import MathGraphicUI from './components/MathGraphicUI' 2 | import { connect } from 'react-redux' 3 | import { 4 | toggleProperty, 5 | setProperty 6 | } from 'containers/MathObjects/actions' 7 | 8 | const mapStateToProps = ( { activeObject, mathGraphics, parseErrors, evalErrors }, ownProps) => { 9 | const { id } = ownProps 10 | return { 11 | color: mathGraphics[id].color, // I do not think this is used, since EvalautedStatusSymbol is connected 12 | visible: mathGraphics[id].visible, // I do not think this is used, since EvalautedStatusSymbol is connected 13 | isActive: activeObject === id 14 | } 15 | } 16 | 17 | const mapDispatchToProps = { 18 | toggleProperty, 19 | setProperty 20 | } 21 | 22 | export default connect(mapStateToProps, mapDispatchToProps)(MathGraphicUI) 23 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathGraphics/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type MathGraphic from './MathGraphic' 3 | import Camera from './Camera' 4 | import Point from './Point' 5 | import Line from './Line' 6 | import Vector from './Vector' 7 | import Axis from './Axis' 8 | import Grid from './Grid' 9 | import ParametricCurve from './ParametricCurve' 10 | import { 11 | ParametricSurface, 12 | ExplicitSurface, 13 | ExplicitSurfacePolar 14 | } from './ParametricSurface' 15 | import ImplicitSurface from './ImplicitSurface' 16 | import VectorField from './VectorField' 17 | 18 | function makeExports(mathGraphics: Array) { 19 | return mathGraphics.reduce((acc, obj) => { 20 | acc[obj.type] = obj 21 | return acc 22 | }, {} ) 23 | } 24 | 25 | export default makeExports( [ 26 | Camera, 27 | Axis, 28 | Grid, 29 | Point, 30 | Line, 31 | Vector, 32 | ParametricCurve, 33 | ParametricSurface, 34 | ExplicitSurface, 35 | ExplicitSurfacePolar, 36 | ImplicitSurface, 37 | VectorField 38 | ] ) 39 | 40 | export { CAMERA } from './Camera' 41 | export { AXIS } from './Axis' 42 | export { GRID } from './Grid' 43 | export { POINT } from './Point' 44 | export { LINE } from './Line' 45 | export { VECTOR } from './Vector' 46 | export { PARAMETRIC_CURVE } from './ParametricCurve' 47 | export { 48 | PARAMETRIC_SURFACE, 49 | EXPLICIT_SURFACE, 50 | EXPLICIT_SURFACE_POLAR 51 | } from './ParametricSurface' 52 | export { IMPLICIT_SURFACE } from './ImplicitSurface' 53 | export { VECTOR_FIELD } from './VectorField' 54 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathGraphics/types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import typeof { Component, PureComponent } from 'react' 3 | import type { Node } from 'react' 4 | 5 | export type InputType = 'math' | 'boolean' | 'text' | 'numericArray' 6 | type InputTypeData = 7 | | { 8 | inputType: 'math', 9 | defaultValue: string, 10 | generateRandomValue?: () => string 11 | } 12 | | { 13 | inputType: 'boolean', 14 | defaultValue: bool, 15 | generateRandomValue?: () => bool 16 | } 17 | | { 18 | inputType: 'text', 19 | defaultValue: string, 20 | generateRandomValue?: () => string 21 | } 22 | // numericArray inputType is only used by Camera component, and it's not 23 | // really an inputType because users cannot edit it... hacky 24 | | { 25 | inputType: 'numericArray', 26 | defaultValue: Array, 27 | isPrimary: true, // prevents numericArray from appearing in settings 28 | generateRandomValue?: () => Array 29 | } 30 | 31 | export type MetaData = { 32 | [property: string]: InputTypeData & { 33 | label?: string, 34 | isPrimary?: boolean, 35 | allowEmpty?: boolean 36 | } 37 | } 38 | 39 | type FunctionalComponent = ( { [prop: string]: any } ) => Node 40 | 41 | export type MathGraphic = { 42 | type: string, 43 | metadata: MetaData, 44 | uiComponent: Component | PureComponent | FunctionalComponent, 45 | mathboxComponent: Component | PureComponent | FunctionalComponent 46 | } 47 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathObject.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export type Settings = { [property: string]: any, description: string } 4 | 5 | /** 6 | * @module MathObject 7 | * Defines wrapper objects that contains react component and metadata 8 | * 9 | * Three types of MathObjects: 10 | * - Folder: wrapper defiend here 11 | * - Symbol: wrapped defined here 12 | * - Graphic: wrapped defined in ./MathGraphics 13 | */ 14 | 15 | export type Support = 'full' | 'experimental' | 'deprecationWarning' 16 | 17 | type Config = { 18 | type: string, 19 | defaultSettings: Settings, 20 | uiComponent: Function, 21 | support?: Support 22 | } 23 | 24 | export interface MathObjectWrapper { 25 | type: string, 26 | defaultSettings: Settings, 27 | uiComponent: Function, 28 | reducer: string, 29 | support: Support 30 | } 31 | 32 | export class MathFolder implements MathObjectWrapper { 33 | 34 | type: string 35 | defaultSettings: Settings 36 | uiComponent: Function 37 | reducer = 'folders' 38 | support: Support 39 | 40 | constructor(config: Config) { 41 | if (!config.support) { 42 | config.support = 'full' 43 | } 44 | Object.assign(this, config) 45 | } 46 | 47 | } 48 | 49 | export class MathSymbol implements MathObjectWrapper { 50 | 51 | type: string 52 | defaultSettings: Settings 53 | uiComponent: Function 54 | reducer = 'mathSymbols' 55 | support: Support 56 | 57 | constructor(config: Config) { 58 | if (!config.support) { 59 | config.support = 'full' 60 | } 61 | Object.assign(this, config) 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathObjectUI.js: -------------------------------------------------------------------------------- 1 | import MathObjectUI from './components/MathObjectUI' 2 | import { connect } from 'react-redux' 3 | import { setActiveObject } from './services/activeObject/actions' 4 | import { getParent } from './selectors' 5 | import { FOLDER } from './Folder/metadata' 6 | import { 7 | deleteMathObject, 8 | setProperty 9 | } from './actions' 10 | 11 | function getMathObjectData(state, id) { 12 | const { mathGraphics, mathSymbols, folders } = state 13 | let found = 0 14 | let value 15 | const substates = [mathGraphics, mathSymbols, folders] 16 | for (const substate of substates) { 17 | if (substate[id] ) { 18 | found += 1 19 | value = substate[id] 20 | } 21 | } 22 | 23 | if (found !== 1) { 24 | throw Error(`Expected ${id} to be present in exactly 1 of [mathGraphics, mathSymbols, folders] but it was present in ${found}`) 25 | } 26 | return value 27 | } 28 | 29 | function getIsDeletable(ownProps, sortableTree) { 30 | const { id, type, isDeleteable } = ownProps 31 | return isDeleteable !== undefined 32 | ? isDeleteable 33 | // otherwise, infer the value 34 | : type === FOLDER 35 | ? sortableTree[id].length === 0 && sortableTree.root.length !== 1 36 | : true 37 | } 38 | 39 | const mapStateToProps = (state, ownProps) => { 40 | const { id } = ownProps 41 | const { sortableTree } = state 42 | const mathObjectData = getMathObjectData(state, id) 43 | const isDeleteable = getIsDeletable(ownProps, sortableTree) 44 | 45 | const parentId = getParent(sortableTree, id) 46 | 47 | return { 48 | isActive: state.activeObject === id, 49 | parentId, 50 | isDeleteable, 51 | positionInParent: sortableTree[parentId].indexOf(id), 52 | description: mathObjectData.description 53 | } 54 | } 55 | 56 | const mapDispatchToProps = { 57 | setActiveObject, 58 | deleteMathObject, 59 | setProperty 60 | } 61 | 62 | export default connect(mapStateToProps, mapDispatchToProps)(MathObjectUI) 63 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathSymbols/BooleanVariable/components/BooleanVariable.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { PureComponent } from 'react' 3 | import { Switch } from 'antd' 4 | import MathObjectUI from 'containers/MathObjects/MathObjectUI' 5 | import { MainRow, Cell, Label } from 'containers/MathObjects/components' 6 | import { 7 | MathInputLHS 8 | } from 'containers/MathObjects/containers/MathInput' 9 | import { BOOLEAN_VARIABLE } from '../metadata' 10 | import typeof { setProperty } from 'containers/MathObjects/actions' 11 | 12 | type Props = { 13 | id: string, 14 | setProperty: setProperty, 15 | value: bool 16 | } 17 | 18 | export default class BooleanVariable extends PureComponent { 19 | 20 | onChange = (value: bool) => { 21 | this.props.setProperty(this.props.id, BOOLEAN_VARIABLE, 'value', value) 22 | } 23 | 24 | render() { 25 | return ( 26 | 30 | 31 | 32 | 35 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | ) 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathSymbols/BooleanVariable/index.js: -------------------------------------------------------------------------------- 1 | import { BOOLEAN_VARIABLE, defaultSettings } from './metadata' 2 | import { connect } from 'react-redux' 3 | import { MathSymbol } from 'containers/MathObjects/MathObject' 4 | import { setProperty } from 'containers/MathObjects/actions' 5 | import BooleanVariable from './components/BooleanVariable' 6 | 7 | const mapStateToProps = ( { mathSymbols }, ownProps) => ( { 8 | value: mathSymbols[ownProps.id].value 9 | } ) 10 | 11 | const mapDispatchToProps = { 12 | setProperty 13 | } 14 | 15 | export default new MathSymbol( { 16 | type: BOOLEAN_VARIABLE, 17 | defaultSettings: defaultSettings, 18 | uiComponent: connect(mapStateToProps, mapDispatchToProps)(BooleanVariable) 19 | } ) 20 | 21 | export { BOOLEAN_VARIABLE } 22 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathSymbols/BooleanVariable/metadata.js: -------------------------------------------------------------------------------- 1 | export const BOOLEAN_VARIABLE = 'BOOLEAN_VARIABLE' 2 | 3 | export const defaultSettings = { 4 | type: BOOLEAN_VARIABLE, 5 | name: 'switch', 6 | value: true, 7 | description: 'Toggle Switch' 8 | } 9 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathSymbols/Variable/components/Variable.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import PropTypes from 'prop-types' 3 | import MathObjectUI from 'containers/MathObjects/MathObjectUI' 4 | import { MainRow } from 'containers/MathObjects/components' 5 | import { 6 | MathInputLHS, 7 | MathInputRHS, 8 | MathTextOutput, 9 | StaticMathStyled 10 | } from 'containers/MathObjects/containers/MathInput' 11 | import { VARIABLE } from '../metadata' 12 | 13 | const justifyRight = { 14 | justifyContent: 'flex-end' 15 | } 16 | 17 | export default class Variable extends PureComponent { 18 | 19 | static propTypes = { 20 | id: PropTypes.string.isRequired 21 | } 22 | 23 | render() { 24 | return ( 25 | 29 | 30 | 33 | 36 | 40 | 41 | 42 | 46 | 47 | 48 | ) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathSymbols/Variable/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { MathSymbol } from 'containers/MathObjects/MathObject' 3 | import Variable from './components/Variable' 4 | import { VARIABLE, defaultSettings } from './metadata' 5 | 6 | export default new MathSymbol( { 7 | type: VARIABLE, 8 | defaultSettings: defaultSettings, 9 | uiComponent: Variable 10 | } ) 11 | 12 | export { VARIABLE } 13 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathSymbols/Variable/metadata.js: -------------------------------------------------------------------------------- 1 | export const VARIABLE = 'VARIABLE' 2 | 3 | export const defaultSettings = { 4 | type: VARIABLE, 5 | name: 'f(x)', 6 | value: 'e^x', 7 | description: 'Variable or Function' 8 | } 9 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathSymbols/VariableSlider/actions.js: -------------------------------------------------------------------------------- 1 | export const SET_SLIDER_VALUE = 'SET_SLIDER_VALUE' 2 | 3 | export function setSliderValue(id, value) { 4 | return { 5 | type: SET_SLIDER_VALUE, 6 | payload: { id, value } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathSymbols/VariableSlider/components/SliderValueDisplay.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { 4 | MathInputLHS, 5 | MathInputRHS, 6 | StaticMathStyled 7 | } from 'containers/MathObjects/containers/MathInput' 8 | 9 | SliderValueDisplay.propTypes = { 10 | parentId: PropTypes.string.isRequired, 11 | valueText: PropTypes.string.isRequired 12 | } 13 | 14 | const valueStyle = { flex: 0, width: '50px' } 15 | 16 | export default function SliderValueDisplay(props) { 17 | return ( 18 | 19 | 22 | 25 | 31 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathSymbols/VariableSlider/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Dispatch } from 'redux' 3 | import { MathSymbol } from 'containers/MathObjects/MathObject' 4 | import VariableSlider from './components/VariableSlider' 5 | import { connect } from 'react-redux' 6 | import { setPropertyAndError, setProperty } from 'containers/MathObjects/actions' 7 | import { setSliderValue } from './actions' 8 | import { ParseErrorData, setError } from 'services/errors' 9 | import { defaultSettings, VARIABLE_SLIDER } from './metadata' 10 | 11 | const mapStateToProps = ( { mathSymbols, sliderValues, evalErrors }, ownProps) => { 12 | const { id } = ownProps 13 | const mathSymbol = mathSymbols[id] 14 | return { 15 | name: mathSymbol.name, 16 | value: sliderValues[id], // number 17 | manualValue: mathSymbol.value, // nullable string 18 | min: mathSymbol.min, 19 | max: mathSymbol.max, 20 | ownEvalErrors: evalErrors[id], 21 | isAnimating: mathSymbol.isAnimating, 22 | speedMultiplier: mathSymbol.speedMultiplier 23 | } 24 | } 25 | 26 | const mapDispatchToProps = (dispatch: Dispatch<*>) => ( { 27 | setProperty: (id, type, property, value) => dispatch(setProperty(id, type, property, value)), 28 | setError: (id, property, errorData) => dispatch(setError(id, property, errorData)), 29 | setSliderValue: (id, value, previousValueIsManual) => { 30 | // set the slider value 31 | dispatch(setSliderValue(id, value)) 32 | // reset valueText and clear parse errors if necessary 33 | if (previousValueIsManual) { 34 | const error = new ParseErrorData() 35 | dispatch(setPropertyAndError(id, VARIABLE_SLIDER, 'value', null, error)) 36 | } 37 | } 38 | } ) 39 | 40 | export default new MathSymbol( { 41 | type: VARIABLE_SLIDER, 42 | defaultSettings: defaultSettings, 43 | uiComponent: connect(mapStateToProps, mapDispatchToProps)(VariableSlider) 44 | } ) 45 | 46 | export { VARIABLE_SLIDER } 47 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathSymbols/VariableSlider/metadata.js: -------------------------------------------------------------------------------- 1 | export const VARIABLE_SLIDER = 'VARIABLE_SLIDER' 2 | export const defaultValue = 0 // slider value, used by slider reducer 3 | export const defaultSettings = { 4 | type: VARIABLE_SLIDER, 5 | name: 'T', 6 | value: null, // used as valueText ... consider renaming this key 7 | min: '-5', 8 | max: '5', 9 | description: 'Variable Slider', 10 | isAnimating: false, 11 | speedMultiplier: 1 12 | } 13 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathSymbols/VariableSlider/reducer.js: -------------------------------------------------------------------------------- 1 | import { VARIABLE_SLIDER, defaultValue } from './metadata' 2 | import { CREATE_MATH_OBJECT } from 'containers/MathObjects/actions' 3 | import { SET_SLIDER_VALUE } from './actions' 4 | const initialState = {} 5 | 6 | export default function sliderValues(state = initialState, action) { 7 | const { type, payload } = action 8 | switch (type) { 9 | 10 | case SET_SLIDER_VALUE: { 11 | const { id, value } = payload 12 | return { ...state, [id]: value } 13 | } 14 | 15 | case CREATE_MATH_OBJECT: { 16 | if (action.name !== VARIABLE_SLIDER) { return state } 17 | const { id } = payload 18 | return { ...state, [id]: defaultValue } 19 | } 20 | 21 | default: 22 | return state 23 | 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/MathSymbols/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import Variable, { VARIABLE } from './Variable' 3 | import BooleanVariable, { BOOLEAN_VARIABLE } from './BooleanVariable' 4 | import VariableSlider, { VARIABLE_SLIDER } from './VariableSlider' 5 | 6 | export default { 7 | [Variable.type]: Variable, 8 | [VariableSlider.type]: VariableSlider, 9 | [BooleanVariable.type]: BooleanVariable 10 | } 11 | 12 | export { VARIABLE, VARIABLE_SLIDER, BOOLEAN_VARIABLE } 13 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/actions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { batchActions } from 'redux-batched-actions' 3 | import { setError } from 'services/errors' 4 | import type { ErrorData } from 'services/errors/ErrorData' 5 | 6 | export const TOGGLE_PROPERTY = 'TOGGLE_PROPERTY' 7 | export const SET_PROPERTY = 'SET_PROPERTY' 8 | export const UNSET_PROPERTY = 'UNSET_PROPERTY' 9 | export const SET_PROPERTY_AND_ERROR = 'SET_PROPERTY_AND_ERROR' 10 | export const SET_ERROR = 'SET_ERROR' 11 | export const CREATE_MATH_OBJECT = 'CREATE_MATH_OBJECT' 12 | export const DELETE_MATH_OBJECT = 'DELETE_MATH_OBJECT' 13 | 14 | export function toggleProperty(id: string, name: string, property: string) { 15 | return { 16 | type: TOGGLE_PROPERTY, 17 | name, 18 | payload: { id, property } 19 | } 20 | } 21 | 22 | export function setProperty(id: string, name: string, property: string, value: any) { 23 | return { 24 | type: SET_PROPERTY, 25 | name, 26 | payload: { id, property, value } 27 | } 28 | } 29 | 30 | export function setPropertyAndError(id: string, name: string, property: string, value: any, error: ErrorData) { 31 | return batchActions( [ 32 | setProperty(id, name, property, value), 33 | setError(id, property, error) 34 | ], 'SET_PROPERTY_AND_ERROR') 35 | } 36 | 37 | export function createMathObject( 38 | id: string, 39 | name: string, 40 | parentFolderId: string, 41 | positionInFolder: number, 42 | settings: ?Object = {} 43 | ) { 44 | return { 45 | type: CREATE_MATH_OBJECT, 46 | name, 47 | payload: { parentFolderId, positionInFolder, id, settings } 48 | } 49 | } 50 | 51 | export function deleteMathObject(id: string, name: string, parentId: string, positionInParent: number) { 52 | return { 53 | type: DELETE_MATH_OBJECT, 54 | name, 55 | payload: { parentId, id, positionInParent } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/components/DeleteButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import SubtleButton from 'components/SubtleButton' 4 | import { Icon } from 'antd' 5 | 6 | const StyledDeleteButton = styled(SubtleButton)` 7 | padding:0px; 8 | margin-right: -3px; 9 | margin-top: -6px; 10 | min-width:25px; 11 | max-width:25px; 12 | height:25px; 13 | position:absolute; 14 | right:0; 15 | top:0; 16 | display:flex; 17 | justify-content: center; 18 | align-items: center; 19 | font-size:133%; 20 | color: ${props => props.theme.gray[5]}; 21 | ` 22 | 23 | export default function DeleteButton(props) { 24 | return ( 25 | 26 | 29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/components/MainRow.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export default styled.div` 4 | position:relative; 5 | display:flex; 6 | align-items:stretch; 7 | justify-content:space-between; 8 | ` 9 | 10 | export const Cell = styled.div` 11 | display: flex; 12 | 13 | align-items:center; 14 | ` 15 | export const Label = styled.span` 16 | padding-left:5px; 17 | padding-right:5px; 18 | ` 19 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/components/ObjectIcon.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const ObjectIcon = styled.div` 4 | width:20px; 5 | height:20px; 6 | margin:3px; 7 | background-color:darkblue; 8 | margin: 3px; 9 | ` 10 | 11 | export default ObjectIcon 12 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as MainRow, Cell, Label } from './MainRow' 2 | export { default as DeleteButton } from './DeleteButton' 3 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/containers/MathInput/ConnectedStaticMath.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react' 3 | import { connect } from 'react-redux' 4 | import { StaticMathStyled } from './components/MathQuillStyled' 5 | import { getMathObjectProp } from './selectors' 6 | 7 | type Props = React.ElementConfig 8 | type OwnProps = {| 9 | parentId: string, 10 | field: string 11 | |} 12 | 13 | const mapStateToProps = ( { mathSymbols, mathGraphics }, ownProps) => { 14 | const { parentId, field } = ownProps 15 | return { 16 | latex: getMathObjectProp( [mathGraphics, mathSymbols], parentId, field) 17 | } 18 | } 19 | 20 | export default connect(mapStateToProps)(StaticMathStyled) 21 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/containers/MathInput/components/MathQuillStyled.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import MathQuill, { StaticMath } from 'components/MathQuill' 3 | import styled, { css } from 'styled-components' 4 | 5 | const borderStyling = css` 6 | border-top none; 7 | border-left: none; 8 | border-right: none; 9 | margin-bottom:1px; 10 | border-bottom-width: 1px; 11 | border-bottom-color: ${props => props.theme.gray[5]}; 12 | &.mq-focused { 13 | box-shadow:none; 14 | margin-bottom: 0px; 15 | border-bottom-width: 2px; 16 | border-bottom-color: ${props => props.theme.primary[4]} 17 | }; 18 | ` 19 | 20 | const errorStyling = css` 21 | &, &.mq-focused { 22 | border-bottom-color: red; 23 | } 24 | ` 25 | 26 | const alignmentStyling = css` 27 | align-self:stretch; 28 | display: flex; 29 | align-items:center; 30 | padding:2px; 31 | ` 32 | 33 | export const MathQuillStyled = styled(MathQuill)` 34 | &.mq-editable-field.mq-math-mode { 35 | flex:1; 36 | max-width: 100%; 37 | font-size: ${props => props.size === 'small' ? '100%' : '125%'}; 38 | font-weight:bolder; 39 | ${borderStyling}; 40 | ${props => props.hasError && errorStyling}; 41 | ${alignmentStyling}; 42 | } 43 | ` 44 | // To help enzyme; finding by component reference not working 45 | MathQuillStyled.displayName = 'MathQuillStyled' 46 | 47 | export const StaticMathStyled = styled(StaticMath)` 48 | &.mq-math-mode { 49 | max-width: calc(100% - 30px); 50 | font-size: ${props => props.size === 'small' ? '100%' : '125%'}; 51 | font-weight:bolder; 52 | ${alignmentStyling}; 53 | } 54 | ` 55 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/containers/MathInput/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { StaticMathStyled } from './components/MathQuillStyled' 3 | export { default as MathInputLHS } from './MathInputLHS' 4 | export { default as MathInputRHS } from './MathInputRHS' 5 | export { default as ConnectedStaticMath } from './ConnectedStaticMath' 6 | export { default as MathTextOutput } from './MathTextOutput' 7 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/containers/Settings/components/Settings.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react' 3 | import SettingsContainer from './SettingsContainer' 4 | import SettingsContent from './SettingsContent' 5 | import type { FormRow } from './SettingsContent' 6 | import typeof { 7 | setProperty 8 | } from 'containers/MathObjects/actions' 9 | 10 | // MathObjectData is heavily inexact; missing many props that vary by type 11 | type MathObjectData = { 12 | type: string 13 | } 14 | export type OwnProps = {| 15 | title: string, 16 | parentId: string, 17 | settingsList: Array 18 | |} 19 | type StateProps = {| 20 | data: MathObjectData 21 | |} 22 | type DispatchProps = {| 23 | setProperty: setProperty, 24 | |} 25 | export type Props = { 26 | ...OwnProps, 27 | ...StateProps, 28 | ...DispatchProps 29 | } 30 | 31 | export default class Settings extends React.PureComponent { 32 | 33 | constructor(props: Props) { 34 | super(props) 35 | // $FlowFixMe: https://github.com/facebook/flow/issues/1517 36 | this.setProperty = this.setProperty.bind(this) 37 | } 38 | 39 | // TODO: remove explicit any type 40 | setProperty(property: string, value: any) { 41 | const { parentId } = this.props 42 | const { type } = this.props.data 43 | this.props.setProperty(parentId, type, property, value) 44 | } 45 | 46 | render() { 47 | return ( 48 | 49 | 55 | 56 | ) 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/containers/Settings/components/SettingsButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import SubtleButton from 'components/SubtleButton' 4 | import { Icon } from 'antd' 5 | 6 | const StyledSettingsButton = styled(SubtleButton)` 7 | width:30px; 8 | height:30px; 9 | display:flex; 10 | justify-content:center; 11 | align-items:center; 12 | font-size:150%; 13 | color: ${props => props.theme.gray[5]}; 14 | ` 15 | 16 | export default function SettingsButton(props) { 17 | return ( 18 | 19 | 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/containers/Settings/components/SettingsContainer.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import SettingsButton from './SettingsButton' 3 | import { Popover, Icon } from 'antd' 4 | import PropTypes from 'prop-types' 5 | import styled from 'styled-components' 6 | import SubtleButton from 'components/SubtleButton' 7 | const TitleBar = styled.div` 8 | display: flex; 9 | justify-content: space-between; 10 | ` 11 | const CloseButton = styled(SubtleButton)` 12 | width:30px; 13 | height:30px; 14 | ` 15 | 16 | export default class SettingsContainer extends PureComponent { 17 | 18 | static propTypes = { 19 | title: PropTypes.string.isRequired, 20 | children: PropTypes.oneOfType( [ 21 | PropTypes.string, 22 | PropTypes.node, 23 | PropTypes.arrayOf(PropTypes.node) 24 | ] ).isRequired 25 | } 26 | 27 | state = { 28 | isVisible: false 29 | } 30 | 31 | handleVisibleChange = (isVisible) => { 32 | this.setState( { isVisible } ) 33 | } 34 | 35 | closePopover = () => { 36 | this.setState( { isVisible: false } ) 37 | } 38 | 39 | render() { 40 | return ( 41 | 45 |

{this.props.title}

46 | 47 | 48 | 49 | 50 | } 51 | trigger='click' 52 | placement='right' 53 | visible={this.state.isVisible} 54 | onVisibleChange={this.handleVisibleChange} 55 | > 56 | 57 |
58 | ) 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/containers/Settings/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { connect } from 'react-redux' 3 | import Settings from './components/Settings' 4 | import type { OwnProps, Props } from './components/Settings' 5 | import { 6 | setProperty 7 | } from 'containers/MathObjects/actions' 8 | 9 | const mapStateToProps = ( { mathGraphics, mathSymbols }, ownProps) => { 10 | const { parentId: id } = ownProps 11 | const data = mathGraphics[id] ? mathGraphics[id] : mathSymbols[id] 12 | return { data } 13 | } 14 | 15 | const mapDispatchToProps = { 16 | setProperty 17 | } 18 | 19 | export default connect(mapStateToProps, mapDispatchToProps)(Settings) 20 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import Folder from './Folder' 3 | import MathGraphics from './MathGraphics' 4 | import MathSymbols from './MathSymbols' 5 | import type { MathObjectWrapper } from './MathObject' 6 | 7 | export { Folder, MathGraphics, MathSymbols } 8 | 9 | const mathObjects : { [string]: MathObjectWrapper } = { 10 | [Folder.type]: Folder, 11 | ...MathSymbols, 12 | ...MathGraphics 13 | } 14 | 15 | export default mathObjects 16 | 17 | export { FOLDER } from './Folder' 18 | export { VARIABLE, VARIABLE_SLIDER, BOOLEAN_VARIABLE } from './MathSymbols' 19 | export { 20 | CAMERA, 21 | AXIS, 22 | GRID, 23 | POINT, 24 | LINE, 25 | VECTOR, 26 | PARAMETRIC_CURVE, 27 | IMPLICIT_SURFACE, 28 | VECTOR_FIELD 29 | } from './MathGraphics' 30 | 31 | window.MathObjects = mathObjects 32 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/selectors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Given an item and an object of arrays, return keys of arrays containing item 3 | * 4 | * @param {object} objOfArrays maps keys to arrays 5 | * @param {[type]} item anything 6 | * @return {set} set of strings 7 | */ 8 | export function getContainingArrayKeys(objOfArrays, item) { 9 | const initial = new Set() 10 | return Object.keys(objOfArrays).reduce((acc, key) => { 11 | if (objOfArrays[key].includes(item)) { 12 | acc.add(key) 13 | } 14 | return acc 15 | }, initial) 16 | } 17 | 18 | /** 19 | * gets id of node's parent 20 | * 21 | * @param {Object} tree maps node to arrayOfNodes 22 | * @param {string} nodeName id of activeObject 23 | * @return {string} string of folder corresponding to activeObject 24 | */ 25 | export function getParent(tree, nodeId) { 26 | 27 | const parentFolders = getContainingArrayKeys(tree, nodeId) 28 | // I'm not really sure I need to validate my redux state in this way ... 29 | if (parentFolders.size < 1) { 30 | throw Error(`${nodeId} does not have parent`) 31 | } 32 | else if (parentFolders.size > 1) { 33 | throw Error(`${nodeId} has multiple parents`) 34 | } 35 | return Array.from(parentFolders)[0] 36 | } 37 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/selectors.test.js: -------------------------------------------------------------------------------- 1 | import { getContainingArrayKeys, getParent } from './selectors' 2 | 3 | describe('getContainingArrayKeys', () => { 4 | 5 | const objOfArrays = { 6 | a: [1, 2, 'cat', 2, 'dog', null], 7 | b: [3, 'dog', 5], 8 | c: [13, 'horse', 'cat', 19] 9 | } 10 | 11 | test('finds existing matches', () => { 12 | const result = getContainingArrayKeys(objOfArrays, 'cat') 13 | const expected = new Set( ['a', 'c'] ) 14 | expect(result).toEqual(expected) 15 | } ) 16 | 17 | test('returns empty set when no matches found', () => { 18 | const result = getContainingArrayKeys(objOfArrays, '17') 19 | const expected = new Set() 20 | expect(result).toEqual(expected) 21 | } ) 22 | 23 | } ) 24 | 25 | describe('getParent', () => { 26 | const tree = { 27 | root: ['folder0', 'folder1', 'folder2', 'folder3'], 28 | folder0: ['item0', 'item1', 'item2'], 29 | folder1: ['item3', 'item4', 'item5', 'item6'], 30 | folder2: ['item7', 'item8'], 31 | folder3: ['item9', 'item10', 'item11', 'item12', 'item13', 'item15'] 32 | } 33 | 34 | test('returns correct parent', () => { 35 | const result0 = getParent(tree, 'folder1') 36 | const expected0 = 'root' 37 | expect(result0).toEqual(expected0) 38 | 39 | const result1 = getParent(tree, 'item7') 40 | const expected1 = 'folder2' 41 | expect(result1).toEqual(expected1) 42 | 43 | } ) 44 | 45 | } ) 46 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/services/activeObject/actions.js: -------------------------------------------------------------------------------- 1 | export const SET_ACTIVE_OBJECT = 'SET_ACTIVE_OBJECT' 2 | 3 | export function setActiveObject(id = null) { 4 | return { 5 | type: SET_ACTIVE_OBJECT, 6 | payload: { id } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /client/src/containers/MathObjects/services/activeObject/reducer.js: -------------------------------------------------------------------------------- 1 | import { SET_ACTIVE_OBJECT } from './actions' 2 | 3 | const initialState = null 4 | 5 | export default function(state = initialState, { type, payload } ) { 6 | switch (type) { 7 | 8 | case SET_ACTIVE_OBJECT: 9 | return payload.id 10 | default: 11 | return state 12 | 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /client/src/containers/MathScopeContext/index.js: -------------------------------------------------------------------------------- 1 | import { MathScopeProvider } from './components/MathScopeContext' 2 | import { connect } from 'react-redux' 3 | import { getParseableSymbols } from './selectors' 4 | import { setError } from 'services/errors' 5 | export { MathScopeConsumer } from './components/MathScopeContext' 6 | 7 | // TODO: This updates when it does not need to because of parseErrors 8 | const mapStateToProps = ( { mathSymbols, sliderValues }, ownProps) => { 9 | const { symbols, idsByName, allSymbolIds } = getParseableSymbols(ownProps.parser, mathSymbols, sliderValues) 10 | const evaluationResult = ownProps.scopeEvaluator.evalScope(symbols) 11 | window.evaluationResult = evaluationResult 12 | return { 13 | idsByName, 14 | allSymbolIds, 15 | ...evaluationResult 16 | } 17 | } 18 | 19 | const mapDispatchToProps = { setError } 20 | 21 | export default connect(mapStateToProps, mapDispatchToProps)(MathScopeProvider) 22 | -------------------------------------------------------------------------------- /client/src/containers/MathScopeContext/selectors.js: -------------------------------------------------------------------------------- 1 | import VariableSlider from 'containers/MathObjects/MathSymbols/VariableSlider' 2 | import memoizeOne from 'memoize-one' 3 | 4 | function _getParseableSymbols(parser, mathSymbols, sliderValues) { 5 | const initial = { 6 | symbols: {}, 7 | idsByName: {}, 8 | allSymbolIds: Object.keys(mathSymbols), 9 | } 10 | const seen = new Set() 11 | return Object.keys(mathSymbols).reduce((acc, id) => { 12 | const { name: lhs, value, type } = mathSymbols[id] 13 | const expr = (value === null && type === VariableSlider.type) 14 | ? `${lhs}=${sliderValues[id]}` 15 | : `${lhs}=${value}` 16 | 17 | // for functions, redux name is f(x) not f, we want f 18 | // TODO: change redux key to lhs and rhs 19 | 20 | let name 21 | try { name = parser.parse(expr).name } 22 | catch (err) { return acc /* handled in parseErrors already */ } 23 | 24 | // we only want symbols with unique names 25 | if (seen.has(name)) { 26 | delete acc.symbols[name] 27 | delete acc.idsByName[name] 28 | } 29 | else { 30 | seen.add(name) 31 | acc.symbols[name] = expr 32 | acc.idsByName[name] = id 33 | } 34 | return acc 35 | 36 | }, initial) 37 | 38 | } 39 | 40 | export const getParseableSymbols = memoizeOne(_getParseableSymbols) 41 | -------------------------------------------------------------------------------- /client/src/containers/SortableTree/actions.js: -------------------------------------------------------------------------------- 1 | export const EDIT_FOLDER_TITLE = 'EDIT_FOLDER_TITLE' 2 | export const ADD_FOLDER = 'ADD_FOLDER' 3 | export const ADD_FOLDER_ITEM = 'ADD_FOLDER_ITEM' 4 | export const DRAG_TO_NEW_DROPPABLE = 'DRAG_TO_NEW_DROPPABLE' 5 | export const REORDER_WITHIN_DROPPABLE = 'REORDER_WITHIN_DROPPABLE' 6 | 7 | /** 8 | * action creator for reordering draggables. The argument object is essentially 9 | * a react-beautiful-dnd DropResult object. 10 | * 11 | * @param {object} source [description] 12 | * @param {string} source.droppableId 13 | * @param {number} source.index position in source list 14 | * @param {object} destination [description] 15 | * @param {string} destination.droppableId 16 | * @param {number} destination.index position in destination list 17 | * @param {string} draggableId 18 | */ 19 | export const dragToNewDroppable = ( { source, destination, draggableId } ) => ( { 20 | type: DRAG_TO_NEW_DROPPABLE, 21 | payload: { 22 | source, 23 | destination, 24 | draggableId 25 | } 26 | } ) 27 | 28 | export const reorderWithinDroppable = ( { source, destination, draggableId } ) => ( { 29 | type: REORDER_WITHIN_DROPPABLE, 30 | payload: { 31 | source, 32 | destination, 33 | draggableId 34 | } 35 | } ) 36 | -------------------------------------------------------------------------------- /client/src/containers/SortableTree/components/MathTree.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { DragDropContext } from 'react-beautiful-dnd' 3 | import SortableList from 'components/SortableList' 4 | import Folder from 'containers/MathObjects/Folder' 5 | import styled from 'styled-components' 6 | import PropTypes from 'prop-types' 7 | 8 | const FolderComponent = Folder.uiComponent 9 | 10 | const SortableListOfFolders = styled(SortableList)` 11 | width:100%; 12 | border-bottom: 1px solid ${props => props.theme.gray[5]}; 13 | ` 14 | 15 | MathTree.propTypes = { 16 | onDragEnd: PropTypes.func.isRequired, 17 | items: PropTypes.arrayOf(PropTypes.object.isRequired).isRequired 18 | } 19 | 20 | export default function MathTree(props) { 21 | return ( 22 | 23 | } 29 | /> 30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /client/src/containers/SortableTree/index.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { 3 | reorderWithinDroppable, 4 | dragToNewDroppable 5 | } from './actions' 6 | import MathTree from './components/MathTree' 7 | 8 | function getItems(itemIds, folders) { 9 | return itemIds.map(id => ( { 10 | id, 11 | isDragDisabled: folders[id].isDragDisabled 12 | } )) 13 | } 14 | 15 | const mapStateToProps = ( { sortableTree, folders }, ownProps) => ( { 16 | items: getItems(sortableTree[ownProps.root], folders) 17 | } ) 18 | 19 | const mapDispatchToProps = (dispatch) => ( { 20 | onDragEnd: (dropResult) => { 21 | if (!dropResult.destination) { 22 | return 23 | } 24 | else if (dropResult.destination.droppableId === dropResult.source.droppableId) { 25 | return dispatch(reorderWithinDroppable(dropResult)) 26 | } 27 | return dispatch(dragToNewDroppable(dropResult)) 28 | 29 | } 30 | } ) 31 | 32 | export default connect(mapStateToProps, mapDispatchToProps)(MathTree) 33 | -------------------------------------------------------------------------------- /client/src/containers/SortableTree/reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | REORDER_WITHIN_DROPPABLE, 3 | DRAG_TO_NEW_DROPPABLE 4 | } 5 | from './actions' 6 | import { CREATE_MATH_OBJECT, DELETE_MATH_OBJECT } from '../MathObjects/actions' 7 | import { Folder } from 'containers/MathObjects' 8 | import update from 'immutability-helper' 9 | 10 | export function reOrder(list, sourceIndex, destinationIndex) { 11 | const result = [...list] 12 | const [removed] = result.splice(sourceIndex, 1) 13 | result.splice(destinationIndex, 0, removed) 14 | return result 15 | } 16 | 17 | update.extend('$reOrder', (sourceDestIndexes, list) => ( 18 | reOrder(list, ...sourceDestIndexes) 19 | )) 20 | 21 | const initialState = { 22 | root: [] 23 | } 24 | 25 | export default (state = initialState, { type, payload, name } ) => { 26 | switch (type) { 27 | 28 | case REORDER_WITHIN_DROPPABLE: { 29 | 30 | const { source, destination } = payload 31 | return update(state, 32 | { 33 | [source.droppableId]: { $reOrder: [source.index, destination.index] } 34 | } ) 35 | 36 | } 37 | 38 | case DRAG_TO_NEW_DROPPABLE: { 39 | 40 | const { source, destination, draggableId } = payload 41 | return update(state, 42 | { 43 | [source.droppableId]: { $splice: [[source.index, 1]] }, 44 | [destination.droppableId]: { $splice: [[destination.index, 0, draggableId]] } 45 | } ) 46 | 47 | } 48 | 49 | case DELETE_MATH_OBJECT: { 50 | const { parentId, positionInParent } = payload 51 | return update(state, 52 | { 53 | [parentId]: { $splice: [[positionInParent, 1]] } 54 | } ) 55 | } 56 | 57 | case CREATE_MATH_OBJECT: { 58 | const { parentFolderId, positionInFolder, id } = payload 59 | const extraMerge = name === Folder.type 60 | ? { $merge: { [id]: [] } } 61 | : {} 62 | return update(state, 63 | { 64 | [parentFolderId]: { $splice: [[positionInFolder, 0, id]] }, 65 | ...extraMerge 66 | } ) 67 | } 68 | 69 | default: 70 | return state 71 | 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /client/src/containers/SortableTree/reducer.test.js: -------------------------------------------------------------------------------- 1 | import mathTree, { reOrder } from './reducer' 2 | 3 | describe('dragReOrder', () => { 4 | it('correctly moves an item down the list', () => { 5 | const initial = ['ant', 'bat', 'cat', 'dog', 'elf', 'frog'] 6 | const sourceIndex = 2 7 | const destinationIndex = 4 8 | const expected = ['ant', 'bat', 'dog', 'elf', 'cat', 'frog'] 9 | const reOrdered = reOrder(initial, sourceIndex, destinationIndex) 10 | expect(reOrdered).toEqual(expected) 11 | } ) 12 | 13 | it('correctly moves an item up the list', () => { 14 | 15 | } ) 16 | } ) 17 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | @import '~antd/dist/antd.css'; 2 | 3 | body { 4 | margin: 0; 5 | padding: 0; 6 | font-family: sans-serif; 7 | } 8 | 9 | html, body, #root { 10 | height: 100% 11 | } 12 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import store from './store' 3 | import { render } from 'react-dom' 4 | import { Provider } from 'react-redux' 5 | import './index.css' 6 | import App from './App' 7 | import { unregister } from './registerServiceWorker' 8 | import { hasMeaningfulChangeOccured } from './services/lastSavedState/index' 9 | 10 | import MathScopeProvider from './containers/MathScopeContext' 11 | import { scopeEvaluator, parser } from './constants/parsing' 12 | 13 | import theme from './constants/theme' 14 | import { ThemeProvider } from 'styled-components' 15 | 16 | import { BrowserRouter } from 'react-router-dom' 17 | 18 | import ReactGA from 'react-ga' 19 | // TODO: Replace this with redux-beacon middleware 20 | ReactGA.initialize('UA-131928751-1', { 21 | testMode: process.env.NODE_ENV === 'development' 22 | } ) 23 | ReactGA.pageview(window.location.pathname + window.location.search) 24 | 25 | // User confirmation for unload 26 | window.addEventListener('beforeunload', event => { 27 | const newState = store.getState() 28 | const oldState = newState.lastSavedState 29 | const shouldWarn = hasMeaningfulChangeOccured(newState, oldState) 30 | if (shouldWarn) { 31 | event.preventDefault() 32 | event.returnValue = '' 33 | } 34 | }) 35 | 36 | const target = document.querySelector('#root') 37 | 38 | render( 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | , 48 | target 49 | ) 50 | 51 | unregister() 52 | -------------------------------------------------------------------------------- /client/src/services/api/index.js: -------------------------------------------------------------------------------- 1 | // Example api call: 2 | 3 | export const getGraph = async (id) => { 4 | if (process.env.REACT_APP_NEXT_API_RETRIEVE_URL) { 5 | return fetch(`${process.env.REACT_APP_NEXT_API_RETRIEVE_URL}${id}`, { 6 | method: "GET", 7 | headers: {}, 8 | }).then((res) => res.json()); 9 | } 10 | return fetch(`api/graph/${id}`, { 11 | method: "GET", 12 | headers: {}, 13 | }).then((res) => res.json()); 14 | }; 15 | 16 | const oldSave = (id, dehydrated) => { 17 | const body = { 18 | urlKey: id, 19 | dehydrated, 20 | }; 21 | return fetch(`/api/graph`, { 22 | method: "POST", 23 | headers: { 24 | "Content-Type": "application/json", 25 | }, 26 | body: JSON.stringify(body), 27 | }).then((res) => res.json()); 28 | }; 29 | 30 | const newSave = async (dehydrated) => { 31 | const data = await fetch(process.env.REACT_APP_NEXT_API_CREATE_URL, { 32 | method: "POST", 33 | headers: { 34 | "Content-Type": "application/json", 35 | }, 36 | body: JSON.stringify({ dehydrated }), 37 | }).then((res) => res.json()); 38 | 39 | if (process.env.REACT_APP_DISABLE_LEGACY_SAVE) { 40 | return data.key; 41 | } 42 | 43 | oldSave(data.key, dehydrated); 44 | 45 | return data.key; 46 | }; 47 | 48 | export const saveGraph = async (id, dehydrated) => { 49 | if (process.env.REACT_APP_NEXT_API_CREATE_URL) { 50 | return newSave(dehydrated); 51 | } else { 52 | await oldSave(id, dehydrated); 53 | return id; 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /client/src/services/errors/ErrorData.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export const PARSE_ERROR = 'PARSE_ERROR' 4 | export const EVAL_ERROR = 'EVAL_ERROR' 5 | export const RENDER_ERROR = 'RENDER_ERROR' 6 | 7 | export class ErrorData { 8 | 9 | type: string 10 | errorMsg: ?string 11 | isError: boolean 12 | 13 | constructor(type: string, errorMsg: ?string) { 14 | this.type = type 15 | this.isError = errorMsg !== null && errorMsg !== undefined 16 | if (this.isError) { 17 | // do not set errorMsg at all if it's void. 18 | this.errorMsg = errorMsg 19 | } 20 | } 21 | 22 | } 23 | 24 | export class ParseErrorData extends ErrorData { 25 | 26 | constructor(errorMsg: ?string) { 27 | super(PARSE_ERROR, errorMsg) 28 | } 29 | 30 | } 31 | 32 | export class EvalErrorData extends ErrorData { 33 | 34 | constructor(errorMsg: ?string) { 35 | super(EVAL_ERROR, errorMsg) 36 | } 37 | 38 | } 39 | 40 | export class RenderErrorData extends ErrorData { 41 | 42 | constructor(errorMsg: ?string) { 43 | super(RENDER_ERROR, errorMsg) 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /client/src/services/errors/actions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { ErrorData } from './ErrorData' 3 | 4 | export const SET_ERROR = 'SET_ERROR' 5 | export const UNSET_ERROR = 'UNSET_ERROR' 6 | 7 | export function setError(id: string, property: string, errorData: ErrorData) { 8 | return errorData.isError 9 | ? { 10 | type: SET_ERROR, 11 | payload: { id, property, errorData } 12 | } 13 | : { 14 | type: UNSET_ERROR, 15 | payload: { id, property, errorData } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/src/services/errors/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { 3 | PARSE_ERROR, 4 | EVAL_ERROR, 5 | RENDER_ERROR, 6 | ErrorData, 7 | ParseErrorData, 8 | EvalErrorData, 9 | RenderErrorData 10 | } from './ErrorData' 11 | 12 | export { setError } from './actions' 13 | -------------------------------------------------------------------------------- /client/src/services/errors/reducer.js: -------------------------------------------------------------------------------- 1 | import update from 'immutability-helper' 2 | import { FOLDER } from 'containers/MathObjects' 3 | import { 4 | PARSE_ERROR, 5 | EVAL_ERROR, 6 | RENDER_ERROR 7 | } from './ErrorData' 8 | import { SET_ERROR, UNSET_ERROR } from './actions' 9 | import { 10 | CREATE_MATH_OBJECT, 11 | DELETE_MATH_OBJECT 12 | } from 'containers/MathObjects/actions' 13 | 14 | const initialState = {} 15 | 16 | export function createErrorReducer(errorTypes) { 17 | return (state = initialState, action) => { 18 | 19 | const { type, payload, name } = action 20 | 21 | switch (type) { 22 | 23 | case SET_ERROR: { 24 | const { id, property } = payload 25 | const { type: errorType, errorMsg } = payload.errorData 26 | 27 | return errorTypes.has(errorType) 28 | ? update(state, { 29 | [id]: { [property]: { $set: errorMsg } } 30 | } ) 31 | : state 32 | } 33 | 34 | case UNSET_ERROR: { 35 | const { id, property } = payload 36 | const { type: errorType } = payload.errorData 37 | return errorTypes.has(errorType) 38 | ? update(state, { 39 | [id]: { $unset: [property] } 40 | } ) 41 | : state 42 | } 43 | 44 | case CREATE_MATH_OBJECT: { 45 | const { id } = payload 46 | // Folders don't have any math-input fields, so they will 47 | // never contain errors 48 | if (name === FOLDER) { 49 | return state 50 | } 51 | return update(state, { [id]: { $set: {} } } ) 52 | } 53 | 54 | case DELETE_MATH_OBJECT: { 55 | const { id } = payload 56 | return update(state, { $unset: [id] } ) 57 | } 58 | 59 | default: 60 | return state 61 | 62 | } 63 | 64 | } 65 | } 66 | 67 | export const parseErrors = createErrorReducer(new Set( [PARSE_ERROR] )) 68 | export const evalErrors = createErrorReducer(new Set( [EVAL_ERROR] )) 69 | export const renderErrors = createErrorReducer(new Set( [RENDER_ERROR] )) 70 | -------------------------------------------------------------------------------- /client/src/services/errors/selectors.js: -------------------------------------------------------------------------------- 1 | export function getErrorMsg(id, prop, ...errorsArray) { 2 | // Loop over the errors in order we care about 3 | for (const errors of errorsArray) { 4 | if (errors[id] && errors[id][prop] ) { 5 | return errors[id][prop] 6 | } 7 | } 8 | 9 | // return a constant empty, so equal by reference 10 | return undefined 11 | 12 | } 13 | -------------------------------------------------------------------------------- /client/src/services/evalData.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { EvalErrorData, setError } from 'services/errors' 3 | import type { Scope, Parser, Symbols } from 'utils/mathParsing' 4 | type SetError = typeof setError 5 | 6 | // TODO test this 7 | export function evalData( 8 | parser: Parser, 9 | data: Symbols, 10 | scope: Scope 11 | ) { 12 | const initial = { evalErrors: {}, evaluated: {}, parseErrors: {} } 13 | return Object.keys(data).reduce((acc, prop) => { 14 | try { 15 | const parsed = parser.parse(data[prop] ) 16 | try { 17 | acc.evaluated[prop] = parsed.eval(scope) 18 | return acc 19 | } 20 | catch (evalError) { 21 | acc.evalErrors[prop] = evalError 22 | return acc 23 | } 24 | } 25 | catch (parseError) { 26 | acc.parseErrors[prop] = parseError 27 | return acc 28 | } 29 | }, initial) 30 | } 31 | 32 | export function handleEvalErrors( 33 | id: string, 34 | newErrors: { [propName: string]: Error }, 35 | existingErrors: { [propName: string]: string }, 36 | setError: SetError 37 | ) { 38 | // Remove old errors 39 | Object.keys(existingErrors).forEach((prop) => { 40 | if (newErrors[prop]===undefined) { 41 | setError(id, prop, new EvalErrorData(null)) 42 | } 43 | } ) 44 | // Add new Errors 45 | Object.keys(newErrors).forEach((prop) => { 46 | const { message } = newErrors[prop] 47 | setError(id, prop, new EvalErrorData(message)) 48 | } ) 49 | } 50 | 51 | export function filterObject(superObject: Object, keys: Array) { 52 | return keys.reduce((acc, key) => { 53 | acc[key] = superObject[key] 54 | return acc 55 | }, {} ) 56 | } 57 | -------------------------------------------------------------------------------- /client/src/services/getCameraData.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { mathbox } from "containers/MathBoxScene/components/MathBoxScene.js"; 4 | 5 | export default function () { 6 | const position: Array = mathbox.three.camera.position.toArray(); 7 | const lookAt: Array = mathbox.three.controls.target.toArray(); 8 | return { position, lookAt }; 9 | } 10 | -------------------------------------------------------------------------------- /client/src/services/lastSavedState/actions.js: -------------------------------------------------------------------------------- 1 | import cloneDeep from 'clone-deep' 2 | export const SET_SAVED_STATE = 'SET_SAVED_STATE'; 3 | 4 | function setLastSavedStateAs(state) { 5 | return { 6 | type: SET_SAVED_STATE, 7 | payload: { state }, 8 | } 9 | } 10 | 11 | export function setLastSavedState() { 12 | return (dispatch, getStore) => { 13 | const clonedStore = cloneDeep(getStore()); 14 | return dispatch(setLastSavedStateAs(clonedStore)) 15 | } 16 | } -------------------------------------------------------------------------------- /client/src/services/lastSavedState/index.js: -------------------------------------------------------------------------------- 1 | import deepEqual from 'deep-equal'; 2 | 3 | export function hasMeaningfulChangeOccured(newState, oldState) { 4 | const exemptKeys = new Set([ 5 | 'drawers', 6 | 'tabs', 7 | 'sliderValues', 8 | 'lastSavedState', 9 | 'activeObject' 10 | ]); 11 | return Object.keys(newState) 12 | .filter(key => !exemptKeys.has(key)) 13 | .some(key => { 14 | const oldValue = oldState[key] 15 | const newValue = newState[key] 16 | return !deepEqual(newValue, oldValue) 17 | }) 18 | } -------------------------------------------------------------------------------- /client/src/services/lastSavedState/reducer.js: -------------------------------------------------------------------------------- 1 | import { SET_SAVED_STATE } from './actions'; 2 | 3 | const initialState = { 4 | state: {}, 5 | } 6 | 7 | export default (state = initialState, action) => { 8 | const { type, payload } = action 9 | switch (type ) { 10 | case SET_SAVED_STATE: 11 | return payload.state 12 | default: 13 | return state 14 | } 15 | } -------------------------------------------------------------------------------- /client/src/services/metadata/actions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export const SET_TITLE = 'SET_TITLE' 3 | 4 | export function setTitle(title: string) { 5 | return { 6 | type: SET_TITLE, 7 | payload: { title } 8 | } 9 | } 10 | 11 | export const SET_CREATION_DATE = 'SET_CREATION_DATE' 12 | 13 | export function setCreationDate() { 14 | return { 15 | type: SET_CREATION_DATE, 16 | payload: { creationDate: JSON.stringify(new Date()) } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/src/services/metadata/reducer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { 3 | SET_CREATION_DATE, typeof setCreationDate, 4 | SET_TITLE, typeof setTitle 5 | } from './actions' 6 | import type { ExtractReturn } from 'utils/flow' 7 | 8 | type State = { 9 | title: string, 10 | versionAtCreation: ?string, 11 | creationDate: string 12 | } 13 | 14 | type Action = ExtractReturn | ExtractReturn 15 | 16 | export const initialState: State = { 17 | title: 'Untitled', 18 | versionAtCreation: process.env.REACT_APP_VERSION, 19 | creationDate: JSON.stringify(new Date()) 20 | } 21 | 22 | export default function(state: State = initialState, action: Action) { 23 | 24 | switch (action.type) { 25 | 26 | case SET_TITLE: { 27 | const title = action.payload.title 28 | return { ...state, title } 29 | } 30 | 31 | case SET_CREATION_DATE: { 32 | const creationDate = action.payload.creationDate 33 | return { ...state, creationDate } 34 | } 35 | 36 | default: { 37 | return state 38 | } 39 | 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /client/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // MathQuill requires jquery. For public, we load through CDN. 2 | // For tests, we need to prove it as a global variable. 3 | import $ from 'jquery' 4 | global.$ = global.jQuery = $ 5 | 6 | // load mathquill after jQuery; need require instead of import 7 | require('utils/testing/mathquill_for_tests_only') 8 | -------------------------------------------------------------------------------- /client/src/store/actions.js: -------------------------------------------------------------------------------- 1 | import { rehydrate } from './hydration' 2 | export const LOAD_STATE = 'LOAD_STATE' 3 | 4 | export function loadDehydratedState(dehydrated) { 5 | return { 6 | type: LOAD_STATE, 7 | payload: { 8 | state: rehydrate(dehydrated) 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /client/src/store/index.js: -------------------------------------------------------------------------------- 1 | // import React from 'react' // used by why-did-you-update 2 | import { createStore, applyMiddleware, compose } from 'redux' 3 | import rootReducer from './reducer' 4 | import thunk from 'redux-thunk' 5 | import { enableBatching } from 'redux-batched-actions' 6 | import { demoState } from './initialState' 7 | import { rehydrate } from './hydration' 8 | 9 | const enhancers = [] 10 | const middleware = [thunk] 11 | 12 | if (process.env.NODE_ENV === 'development') { 13 | const devToolsExtension = window.__REDUX_DEVTOOLS_EXTENSION__ 14 | 15 | if (typeof devToolsExtension === 'function') { 16 | enhancers.push(devToolsExtension()) 17 | } 18 | 19 | // const { whyDidYouUpdate } = require('why-did-you-update') 20 | // whyDidYouUpdate(React) // logs potentially unnecessary re-renders 21 | 22 | } 23 | 24 | const composedEnhancers = compose( 25 | applyMiddleware(...middleware), 26 | ...enhancers 27 | ) 28 | 29 | const store = createStore( 30 | enableBatching(rootReducer), 31 | rehydrate(demoState), 32 | composedEnhancers 33 | ) 34 | 35 | export default store 36 | -------------------------------------------------------------------------------- /client/src/store/initialState.js: -------------------------------------------------------------------------------- 1 | // These are "fixed" in the sense that they cannot be sorted (dragged/dropped) 2 | const sortableTreeFixedPortion = { 3 | 'setup': [ 4 | 'cameraFolder', 5 | 'axes' 6 | ], 7 | 'cameraFolder': ['camera'], 8 | 'axes': [ 9 | 'axis-x', 10 | 'axis-y', 11 | 'axis-z', 12 | 'grid-xy', 13 | 'grid-yz', 14 | 'grid-zx' 15 | ] 16 | } 17 | 18 | const initialState = { 19 | metaData: {}, 20 | sortableTree: { 21 | ...sortableTreeFixedPortion, 22 | root: ['mainFolder'], 23 | mainFolder: [] 24 | }, 25 | folders: { 26 | 'cameraFolder': { 27 | 'isCollapsed': true, 28 | 'isDropDisabled': true, 29 | 'isDragDisabled': true, 30 | 'description': 'Camera Controls' 31 | }, 32 | 'axes': { 33 | 'isCollapsed': false, 34 | 'isDropDisabled': true, 35 | 'isDragDisabled': true, 36 | 'description': 'Axes and Grids' 37 | }, 38 | 'mainFolder': { 39 | 'description': 'A Folder' 40 | } 41 | }, 42 | mathSymbols: {}, 43 | mathGraphics: { 44 | 'camera': { 45 | type: 'CAMERA' 46 | }, 47 | 'axis-x': { 48 | 'type': 'AXIS', 49 | 'label': 'x' 50 | }, 51 | 'axis-y': { 52 | 'type': 'AXIS', 53 | 'label': 'y', 54 | 'axis': 'y' 55 | }, 56 | 'axis-z': { 57 | 'type': 'AXIS', 58 | 'label': 'z', 59 | 'axis': 'z', 60 | 'scale': '1/2' 61 | }, 62 | 'grid-xy': { 63 | 'type': 'GRID', 64 | 'axes': 'xy' 65 | }, 66 | 'grid-yz': { 67 | 'type': 'GRID', 68 | 'visible': false, 69 | 'axes': 'yz' 70 | }, 71 | 'grid-zx': { 72 | 'type': 'GRID', 73 | 'visible': false, 74 | 'axes': 'zx' 75 | } 76 | }, 77 | sliders: {} 78 | } 79 | 80 | const demoState = JSON.parse(JSON.stringify(initialState)) 81 | demoState.mathGraphics['1'] = { 82 | 'type': 'EXPLICIT_SURFACE', 83 | 'color': 'rainbow' 84 | } 85 | demoState.sortableTree.mainFolder.push('1') 86 | 87 | export { sortableTreeFixedPortion, demoState } 88 | export default initialState 89 | -------------------------------------------------------------------------------- /client/src/store/reducer.js: -------------------------------------------------------------------------------- 1 | import cloneDeep from 'clone-deep' 2 | import { combineReducers } from 'redux' 3 | import drawers from 'containers/Drawer/reducer' 4 | import tabs from 'containers/ControlledTabs/reducer' 5 | import sortableTree from 'containers/SortableTree/reducer' 6 | import sliderValues from 'containers/MathObjects/MathSymbols/VariableSlider/reducer' 7 | import activeObject from 'containers/MathObjects/services/activeObject/reducer' 8 | import metadata from 'services/metadata/reducer' 9 | import { 10 | folders, 11 | mathGraphics, 12 | mathSymbols 13 | } from 'containers/MathObjects/reducer' 14 | import { LOAD_STATE } from './actions' 15 | import lastSavedState from 'services/lastSavedState/reducer' 16 | 17 | import { 18 | parseErrors, 19 | evalErrors, 20 | renderErrors 21 | } from 'services/errors/reducer' 22 | 23 | const combinedReducer = combineReducers( { 24 | metadata, 25 | drawers, 26 | tabs, 27 | sortableTree, 28 | folders, 29 | mathGraphics, 30 | mathSymbols, 31 | parseErrors, 32 | evalErrors, 33 | renderErrors, 34 | sliderValues, 35 | activeObject, 36 | lastSavedState 37 | } ) 38 | 39 | export default function rootReducer(state, action) { 40 | if (action.type === LOAD_STATE) { 41 | const { lastSavedState: previousLastSavedState, ...oldState } = state 42 | const newState = { ...oldState, ...action.payload.state } 43 | const lastSavedState = cloneDeep(newState) 44 | return { ...newState, lastSavedState } 45 | } 46 | return combinedReducer(state, action) 47 | } 48 | -------------------------------------------------------------------------------- /client/src/utils/colors.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import Color from 'color' 3 | 4 | export function lighten(colorStr: string, amount: number) { 5 | const color = Color(colorStr) 6 | const lightness = color.lightness() 7 | const increaseBy = amount * (1 - lightness / 100) 8 | return color.lighten(increaseBy).hex() 9 | } 10 | -------------------------------------------------------------------------------- /client/src/utils/flow/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export type ExtractReturn = $Call<((...Iterable) => T) => T, Fn>; 3 | 4 | // These two functions help in making default props optional 5 | export type Optionalize = $Rest 6 | export type OptionalizeSome = {|...$Diff, ...Optionalize|} 7 | -------------------------------------------------------------------------------- /client/src/utils/functions/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * Creates a Promise that is resolved after delay. 4 | * Good for using with async/await 5 | * 6 | * @param {number} delay in milliseconds 7 | */ 8 | export function timeout(delay: number): Promise { 9 | return new Promise(resolve => setTimeout(resolve, delay)) 10 | } 11 | -------------------------------------------------------------------------------- /client/src/utils/helpers.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export function capitalize(string: string) { 4 | return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase() 5 | } 6 | 7 | export function escapeRegExp(str: string) { 8 | // from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Using_Special_Characters 9 | // $& means the whole matched string 10 | return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') 11 | } 12 | 13 | export function replaceAll(str: string, find: string, replaceWith: string) { 14 | // from https://stackoverflow.com/a/1144788/2747370 15 | return str.replace(new RegExp(escapeRegExp(find), 'g'), replaceWith) 16 | } 17 | 18 | export function findClosingBrace(str: string, startIdx: number) { 19 | const braces = { 20 | '[': ']', 21 | '<': '>', 22 | '(': ')', 23 | '{': '}' 24 | } 25 | 26 | const openingBrace = str[startIdx] 27 | 28 | const closingBrace = braces[openingBrace] 29 | 30 | if (closingBrace === undefined) { 31 | throw Error(`${str} does not contain an opening brace at position ${startIdx}.`) 32 | } 33 | 34 | let stack = 1 35 | 36 | // eslint-disable-next-line no-plusplus 37 | for (let j = startIdx + 1; j < str.length; j++) { 38 | if (str[j] === openingBrace) { 39 | stack += +1 40 | } 41 | else if (str[j] === closingBrace) { 42 | stack += -1 43 | } 44 | if (stack === 0) { 45 | return j 46 | } 47 | } 48 | 49 | // stack !== 0 50 | throw Error(`${str} has a brace that opens at position ${startIdx} but does not close.`) 51 | } 52 | -------------------------------------------------------------------------------- /client/src/utils/helpers.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | escapeRegExp, 3 | replaceAll, 4 | findClosingBrace 5 | } from './helpers' 6 | 7 | test('escaping regular expressions', () => { 8 | const input = '3 * sin(x)^2' 9 | const goal = '3 \\* sin\\(x\\)\\^2' 10 | expect(escapeRegExp(input)).toBe(goal) 11 | } ) 12 | 13 | describe('replaceAll', () => { 14 | test('replacing normal characters', () => { 15 | expect(replaceAll('cat ate the bat', 'at', 'AT')) 16 | .toBe('cAT ATe the bAT') 17 | } ) 18 | 19 | test('replacing special characters', () => { 20 | expect(replaceAll('diff( diff( f ) ) ( t)', '( ', '(')) 21 | .toBe('diff(diff(f ) ) (t)') 22 | } ) 23 | } ) 24 | 25 | describe('findClosingBrace', () => { 26 | test('finding a closing brace', () => { 27 | // 01234567890123456789012345678901234567890123456789 28 | const expression = '4 + ( sin( 3^(1) - 7^2 ) + 5 ) + exp(8 - (4*3) )' 29 | const startIdx = 4 30 | expect(findClosingBrace(expression, startIdx)).toBe(29) 31 | } ) 32 | 33 | test('throws an error if no opening brace at specified location', () => { 34 | // 0123456789 35 | const expression = '4 + sin(x)' 36 | const startIdx = 2 37 | const badfunc = () => findClosingBrace(expression, startIdx) 38 | expect(badfunc).toThrow( 39 | `${expression} does not contain an opening brace at position ${startIdx}.` 40 | ) 41 | } ) 42 | 43 | test('throws an error if brace opens but does not close', () => { 44 | // 012345678901234 45 | const expression = '4 + sin( e^(2t)' 46 | const startIdx = 7 47 | const badfunc = () => findClosingBrace(expression, startIdx) 48 | expect(badfunc).toThrow( 49 | `${expression} has a brace that opens at position ${startIdx} but does not close.` 50 | ) 51 | } ) 52 | } ) 53 | -------------------------------------------------------------------------------- /client/src/utils/mathParsing/Parser.test.js: -------------------------------------------------------------------------------- 1 | import Parser, { getUsedSymbols } from './Parser' 2 | import MathExpression from './MathExpression' 3 | const DIGITS = 6 4 | 5 | describe('Parser', () => { 6 | test('getParsed returns ParsedExpression objects', () => { 7 | const parser = new Parser() 8 | const parsed = parser.parse('f(x) + \\frac{y^2}{2}') 9 | const scope = { x: 9, y: 4, f: Math.sqrt } // 3+16/2 = 11 10 | expect(parsed).toBeInstanceOf(MathExpression) 11 | expect(parsed.eval(scope)).toBeCloseTo(11, DIGITS) 12 | } ) 13 | 14 | test('uses cache when possible', () => { 15 | const parser = new Parser() 16 | jest.spyOn(parser, 'parse') 17 | jest.spyOn(parser, 'addToCache') 18 | 19 | parser.parse('a + b') 20 | parser.parse('a + b') 21 | parser.parse('a + b') 22 | 23 | expect(parser.parse).toHaveBeenCalledTimes(3) 24 | expect(parser.addToCache).toHaveBeenCalledTimes(1) 25 | } ) 26 | 27 | test('Errors are cached before being thrown', () => { 28 | const parser = new Parser() 29 | jest.spyOn(parser, 'parse') 30 | jest.spyOn(parser, 'addToCache') 31 | 32 | try { parser.parse('a + ') } 33 | catch (err) { } 34 | try { parser.parse('a + ') } 35 | catch (err) { } 36 | try { parser.parse('a + ') } 37 | catch (err) { } 38 | 39 | expect(parser.parse).toHaveBeenCalledTimes(3) 40 | expect(parser.addToCache).toHaveBeenCalledTimes(1) 41 | 42 | } ) 43 | } ) 44 | 45 | test('getUsedSymbols', () => { 46 | const parser = new Parser() 47 | const expressions = [ 48 | 'a+f\\left(t\\right)', 49 | 'b + x', 50 | '2^{g\\left(x,y\\right)}' 51 | ] 52 | // Function arguments should not be deteceted 53 | const usedSymbols = new Set( ['a', 'f', 't', 'b', 'x', 'g', 'y'] ) 54 | expect(getUsedSymbols(parser, expressions)).toEqual(usedSymbols) 55 | } ) 56 | -------------------------------------------------------------------------------- /client/src/utils/mathParsing/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default as Parser, getUsedSymbols } from './Parser' 3 | export { default as MathExpression } from './MathExpression' 4 | export { ScopeEvaluator } from './mathscope' 5 | 6 | export type { Symbols } from './mathscope' 7 | export type { Scope } from './MathExpression' 8 | -------------------------------------------------------------------------------- /client/src/utils/mathParsing/postprocessors/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default as reassignOperators } from './reassignOperators' 3 | -------------------------------------------------------------------------------- /client/src/utils/mathParsing/postprocessors/index.test.js: -------------------------------------------------------------------------------- 1 | import * as index from './index' 2 | 3 | test('import was successful', () => { 4 | expect(index).toBeDefined() 5 | } ) 6 | -------------------------------------------------------------------------------- /client/src/utils/mathParsing/postprocessors/reassignOperators.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import math from 'utils/mathjs' 3 | import type { Node } from 'utils/mathjs/types' 4 | 5 | /** 6 | * Returns a node hander to be used as a mathjs's ExpressionTree.traverse callback 7 | * 8 | * @param {Object} operatorFnMap maps old operator function names to new operator function names 9 | */ 10 | export default function reassignOperators(operatorFnMap: {[string]: string} ) { 11 | for (let key of Object.keys(operatorFnMap)) { 12 | const fnName = operatorFnMap[key] 13 | if (!math.hasOwnProperty(fnName)) { 14 | throw Error(`math['${fnName}'] does not exist. (ExpressionTree Operators can only be reassigned to functions within the math (mathjs) namespace.)`) 15 | } 16 | } 17 | 18 | return (node: Node) => { 19 | if (node.type === 'OperatorNode' && operatorFnMap.hasOwnProperty(node.op)) { 20 | node.fn = operatorFnMap[node.op] 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /client/src/utils/mathParsing/postprocessors/reassignOperators.test.js: -------------------------------------------------------------------------------- 1 | import math from 'utils/mathjs' 2 | import reassignOperators from './reassignOperators' 3 | 4 | describe('reassignOperators', () => { 5 | test('raises an error when operatorFnName not in scope', () => { 6 | const badfunc = () => reassignOperators( { '&': 'newAnd' } ) 7 | expect(badfunc).toThrow( 8 | `math['newAnd'] does not exist. (ExpressionTree Operators can only be reassigned to functions within the math (mathjs) namespace.)` 9 | ) 10 | } ) 11 | 12 | test('successfully replaces operatorFn names', () => { 13 | const preprocessor = reassignOperators( { '&': 'cross', '|': 'dot' } ) 14 | const tree = math.parse('(a & b) | c') 15 | tree.traverse(node => preprocessor(node)) 16 | 17 | const bitOrNode = tree 18 | const parensNode = tree.args[0] 19 | const bitAndNode = parensNode.content 20 | expect(bitOrNode.fn).toBe('dot') 21 | expect(bitAndNode.fn).toBe('cross') 22 | } ) 23 | } ) 24 | -------------------------------------------------------------------------------- /client/src/utils/mathParsing/preprocessors/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default as preprocessHOFs } from './preprocessHOFs' 3 | export { default as preprocessMathQuill } from './preprocessMathQuill' 4 | -------------------------------------------------------------------------------- /client/src/utils/mathParsing/preprocessors/index.test.js: -------------------------------------------------------------------------------- 1 | import * as index from './index' 2 | 3 | test('import was successful', () => { 4 | expect(index).toBeDefined() 5 | } ) 6 | -------------------------------------------------------------------------------- /client/src/utils/mathParsing/preprocessors/preprocessHOFs.test.js: -------------------------------------------------------------------------------- 1 | import preprocessHOFs, { normalizeHOFExpression } from './preprocessHOFs' 2 | 3 | describe('preprocessHOFs', () => { 4 | test('behaves correctly if no HOFs present', () => { 5 | const preprocess = preprocessHOFs( ['diff'] ) 6 | const input = 'cos(t)*i + sin(t)*j' 7 | expect(preprocess(input)).toBe(input) 8 | } ) 9 | 10 | test('unnested HOFs are converted correctly', () => { 11 | const preprocess = preprocessHOFs( ['diff'] ) 12 | const input = 'diff(g)(t) + diff(f)(u,v)' 13 | const goal = 'diff(g,t) + diff(f,u,v)' 14 | expect(preprocess(input)).toBe(goal) 15 | } ) 16 | 17 | test('nested HOF is converted correctly', () => { 18 | const preprocess = preprocessHOFs( ['diff'] ) 19 | const input = 'e^t + diff( diff(r) )(t)' 20 | const goal = 'e^t + diff( diff(r) ,t)' 21 | expect(preprocess(input)).toBe(goal) 22 | } ) 23 | 24 | test('multiple HOFs are converted correctly', () => { 25 | const preprocess = preprocessHOFs( ['diff', 'unitT'] ) 26 | const input = 'diff(unitT(r))(t) + unitT(r)(t) + diff(r)(t)' 27 | const goal = 'diff(unitT(r),t) + unitT(r,t) + diff(r,t)' 28 | expect(preprocess(input)).toBe(goal) 29 | } ) 30 | } ) 31 | 32 | describe('hof expression normalization', () => { 33 | test('simple hof name is replaced correctly', () => { 34 | const input = 'diff ( f ) (u, v)' 35 | const goal = 'diff( f )(u, v)' 36 | 37 | expect(normalizeHOFExpression(input, 'diff')).toBe(goal) 38 | } ) 39 | 40 | test('hof name with special characters is replaced correctly', () => { 41 | const input = '\\test ( f ) (u, v)' 42 | const goal = '\\test( f )(u, v)' 43 | 44 | expect(normalizeHOFExpression(input, '\\test')).toBe(goal) 45 | } ) 46 | } ) 47 | -------------------------------------------------------------------------------- /client/src/utils/mathjs/custom.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Math } from './types' 3 | import core from 'mathjs/core' 4 | const math: Math = core.create() 5 | 6 | math.import(require('mathjs/lib/type/matrix')) 7 | math.import(require('mathjs/lib/type/complex')) 8 | math.import(require('mathjs/lib/constants')) 9 | math.import(require('mathjs/lib/function/arithmetic')) 10 | math.import(require('mathjs/lib/function/trigonometry')) 11 | math.import(require('mathjs/lib/function/complex')) 12 | math.import(require('mathjs/lib/function/matrix')) 13 | math.import(require('mathjs/lib/function/statistics')) 14 | 15 | math.import(require('mathjs/lib/expression')) 16 | 17 | export default math 18 | -------------------------------------------------------------------------------- /client/src/utils/mathjs/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import customMath from './custom' 3 | import { diff, pdiff, unitT, unitN, unitB, curl, div } from './derivatives' 4 | import { LegendreP } from "./legendre" 5 | function arctan(arg0: number, arg1?: number) { 6 | return arg1 === undefined ? customMath.atan(arg0) : customMath.atan2(arg0, arg1) 7 | } 8 | 9 | const imaginaryUnit = customMath.i 10 | customMath.import( { 11 | diff, 12 | pdiff, 13 | unitT, 14 | unitN, 15 | unitB, 16 | div, 17 | curl, 18 | i: [1, 0, 0], 19 | j: [0, 1, 0], 20 | k: [0, 0, 1], 21 | I: imaginaryUnit, 22 | arcsin: customMath.asin, 23 | arccos: customMath.acos, 24 | arctan: arctan, 25 | arcsinh: customMath.asinh, 26 | arccosh: customMath.acosh, 27 | arctanh: customMath.atanh, 28 | LegendreP 29 | }, { override: true } ) 30 | 31 | export default customMath 32 | 33 | window.math = customMath 34 | -------------------------------------------------------------------------------- /client/src/utils/mathjs/legendre.js: -------------------------------------------------------------------------------- 1 | const legendrePolys = { 2 | 0: (x) => 1, 3 | 1: (x) => x, 4 | 2: x => 0.5 * (3 * x ** 2 - 1), 5 | 3: x =>0.5 * (5 * x ** 3 - 3 * x), 6 | 4: x => 0.125 * (35 * x ** 4 - 30 * x ** 2 + 3) 7 | } 8 | 9 | const associatedLegendrePolys = { 10 | 0: { 11 | 0: (x) => 1 12 | }, 13 | 1: { 14 | 0: legendrePolys[1], 15 | 1: x => -1 * (1 - (x ** 2)) ** 0.5, 16 | "-1": x => -0.5 * associatedLegendrePolys[1][1](x) 17 | }, 18 | 2: { 19 | 0: legendrePolys[2], 20 | 1: x => -3 * x * (1 - (x ** 2)) ** 0.5, 21 | 2: x => 3 * (1 - x ** 2), 22 | "-1": x => -(1 / 6) * associatedLegendrePolys[2][1](x), 23 | "-2": x => -(1 / 24) * associatedLegendrePolys[2][2](x) 24 | } 25 | } 26 | 27 | /** 28 | * This is a placeholder for a real implementation, but the function signature 29 | * should remain the same: 30 | * - LegendreP(x, l) returns the lth Legendre polynomial P_l(x) 31 | * - LegendreP(x, l, m) returns the associated Legendre polynomial P_l^m(x) 32 | */ 33 | const LegendreP = (x, l, m) => { 34 | if (m === undefined) { 35 | const f = legendrePolys[l] 36 | if (f) return f(x) 37 | throw new Error(`LegendreP not implemented for l=${l}`) 38 | } 39 | if (associatedLegendrePolys[l]) { 40 | const f = associatedLegendrePolys[l][m] 41 | if (f) return f(x) 42 | throw new Error(`LegendreP not implemented for l=${l}, m=${m}`) 43 | } 44 | 45 | } 46 | 47 | export { LegendreP } -------------------------------------------------------------------------------- /client/src/utils/sets/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * Set union 4 | * 5 | * @param {set} a 6 | * @param {set} b 7 | * @return {set} union of a and b 8 | */ 9 | export function union(a: Set, b: Set): Set { 10 | return new Set( [...a, ...b] ) 11 | } 12 | 13 | /** 14 | * Set intersection 15 | * 16 | * @param {set} a 17 | * @param {set} b 18 | * @return {set} union of a and b 19 | */ 20 | export function intersect(a: Set, b: Set): Set { 21 | return new Set( [...a].filter(item => b.has(item))) 22 | } 23 | 24 | /** 25 | * merge one set into another 26 | * 27 | * @param {set} target target set, mutated and returned 28 | * @param {set} source whose elements are merged into target 29 | */ 30 | export function setMergeInto(target: Set, source: Set): Set { 31 | for (const item of source) { 32 | target.add(item) 33 | } 34 | return target 35 | } 36 | -------------------------------------------------------------------------------- /client/src/utils/sets/index.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | union, 3 | intersect, 4 | setMergeInto 5 | } from './index' 6 | 7 | describe('union', () => { 8 | 9 | const a = new Set( ['cat', 1, 'fish'] ) 10 | const b = new Set( [1, 'whale', 'cat'] ) 11 | const expectedUnion = new Set( ['cat', 1, 'fish', 'whale'] ) 12 | const theUnion = union(a, b) 13 | 14 | test('union contains expected items', () => { 15 | expect(theUnion).toEqual(expectedUnion) 16 | } ) 17 | 18 | test('inputs were not mutated', () => { 19 | const expectedA = new Set( ['cat', 1, 'fish'] ) 20 | const expectedB = new Set( [1, 'whale', 'cat'] ) 21 | expect(a).toEqual(expectedA) 22 | expect(b).toEqual(expectedB) 23 | } ) 24 | } ) 25 | 26 | describe('intersection', () => { 27 | 28 | const a = new Set( ['cat', 1, 'fish'] ) 29 | const b = new Set( [1, 'whale', 'cat'] ) 30 | const expectedIntersection = new Set( ['cat', 1] ) 31 | const theIntersection = intersect(a, b) 32 | 33 | test('intersection contains expected items', () => { 34 | expect(theIntersection).toEqual(expectedIntersection) 35 | } ) 36 | 37 | test('inputs were not mutated', () => { 38 | const expectedA = new Set( ['cat', 1, 'fish'] ) 39 | const expectedB = new Set( [1, 'whale', 'cat'] ) 40 | expect(a).toEqual(expectedA) 41 | expect(b).toEqual(expectedB) 42 | } ) 43 | } ) 44 | 45 | describe('setMergeInto', () => { 46 | 47 | const target = new Set( ['cat', 1, 'fish'] ) 48 | const source = new Set( [1, 'whale', 'cat'] ) 49 | const expectedMerge = new Set( ['cat', 1, 'fish', 'whale'] ) 50 | setMergeInto(target, source) 51 | 52 | test('union contains expected items', () => { 53 | expect(target).toEqual(expectedMerge) 54 | } ) 55 | 56 | test('source was not mutated', () => { 57 | const expectedSource = new Set( [1, 'whale', 'cat'] ) 58 | expect(source).toEqual(expectedSource) 59 | } ) 60 | } ) 61 | -------------------------------------------------------------------------------- /client/src/utils/shallowDiffWithSets.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import diff from 'shallow-diff' 3 | // TODO Use this version everywhere, or fork original shallow-diff and make the 4 | // change there. 5 | export default function diffWithSets(base: Object, compared: Object) { 6 | const difference = diff(base, compared) 7 | return Object.keys(difference).reduce((acc, key) => { 8 | acc[key] = new Set(difference[key] ) 9 | return acc 10 | }, {} ) 11 | } 12 | -------------------------------------------------------------------------------- /client/src/views/MainView/Examples/ExamplesList.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react' 3 | import type { Examples, ExampleItem } from './data' 4 | import { NavLink } from 'react-router-dom' 5 | 6 | type Props = { 7 | data: Examples 8 | } 9 | 10 | export default function ExamplesList(props: Props) { 11 | const { data } = props 12 | return ( 13 |
    14 | { 15 | data.map((example: ExampleItem, index: number) => { 16 | return ( 17 |
  • 18 | 19 | {example.title} 20 | 21 | {example.description} 22 |
  • 23 | ) 24 | } ) 25 | } 26 |
27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /client/src/views/MainView/Examples/data.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export type ExampleItem = { 4 | id: string, 5 | title: string, 6 | description?: string 7 | } 8 | 9 | export type Examples = Array 10 | 11 | export const featureDemos: Examples = [ 12 | { 13 | id: 'sliders_intro', 14 | title: 'Using Variable Sliders' 15 | }, 16 | { 17 | id: 'sliders_plotrange', 18 | title: 'Using Sliders to Animate Plot Range' 19 | }, 20 | { 21 | id: 'color_and_visibility', 22 | title: 'Color and Visibility' 23 | }, 24 | { 25 | id: 'vectors', 26 | title: 'Vectors and Vector Tails' 27 | }, 28 | { 29 | id: 'labels', 30 | title: 'Labeling Points, Vectors, and Lines' 31 | }, 32 | { 33 | id: 'functions', 34 | title: 'Using Functions' 35 | }, 36 | { 37 | id: 'derivatives', 38 | title: 'diff: Using Derivatives' 39 | }, 40 | { 41 | id: 'calculated_visibility', 42 | title: 'Controlling Multiple Object Visibility with Toggles' 43 | }, 44 | { 45 | id: 'z_bias', 46 | title: 'Fine-tuning Visibility with z-bias' 47 | }, 48 | { 49 | id: 'tnb', 50 | title: 'The TNB Vectors for Parametric Curves' 51 | }, 52 | { 53 | id: 'animate_camera', 54 | title: 'Animating the Camera Position' 55 | } 56 | ] 57 | 58 | export const neatExamples: Examples = [ 59 | { 60 | id: 'motion', 61 | title: 'Motion: Velocity and Acceleration' 62 | }, 63 | { 64 | id: 'ruled_hyperboloid', 65 | title: 'Ruled Hyperboloid' 66 | }, 67 | { 68 | id: 'horizontal_revolution_washer', 69 | title: 'Surface of Revolution: Washer Method (Horizontal Axis)' 70 | }, 71 | { 72 | id: 'vertical_revolution_shell_method', 73 | title: 'Surface of Revolution: Shell Method (Vertical Axis)' 74 | }, 75 | { 76 | id: 'sphere_colormap', 77 | title: 'Color Maps on a Sphere' 78 | }, 79 | { 80 | id: 'osculating_circle', 81 | title: 'Osculating Circle' 82 | } 83 | ] 84 | -------------------------------------------------------------------------------- /client/src/views/MainView/Examples/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Drawer from 'containers/Drawer' 3 | import ControlledTabs, { TabPane } from 'containers/ControlledTabs' 4 | import ExamplesList from './ExamplesList' 5 | import { neatExamples, featureDemos } from './data' 6 | 7 | const Examples = (props) => { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ) 20 | } 21 | 22 | export default Examples 23 | -------------------------------------------------------------------------------- /client/src/views/MainView/Header/components/HeaderButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import styled from 'styled-components' 3 | 4 | export default styled.span` 5 | color: ${props => props.theme.gray[5]}; 6 | font-size: ${props => props.type === 'brand' ? '130%' : '115%'}; 7 | font-weight: ${props => props.type === 'brand' ? 900 : 'default'}; 8 | ` 9 | -------------------------------------------------------------------------------- /client/src/views/MainView/Header/components/HelpButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react' 3 | import { Icon } from 'antd' 4 | import PopModal from 'components/PopModal' 5 | import styled from 'styled-components' 6 | 7 | const ContactList = styled.ul` 8 | list-style-type: none; 9 | padding-left:0px; 10 | ` 11 | 12 | const HelpPopoverContainer = styled.div` 13 | max-width: 300px; 14 | width:300px; 15 | ` 16 | 17 | const ContactIcon = styled(Icon)` 18 | font-size:125%; 19 | padding-right:5px; 20 | ` 21 | 22 | const Email = styled.span` 23 | font-family: courier; 24 | text-decoration: underline; 25 | ` 26 | 27 | export default function HelpButton() { 28 | 29 | return ( 30 | 34 | 35 | Help/Contact 36 | 37 | } 38 | > 39 | 40 |

Math3d.org is a work in progress. Have a question? Suggestion? Found a bug?

41 | 42 |

Let us know!

43 | 44 | 45 |
56 |
  • 57 | 58 | Email math3dapp@gmail.com 59 |
  • 60 | 61 | 62 | 63 | ) 64 | 65 | } 66 | -------------------------------------------------------------------------------- /client/src/views/MainView/Header/containers/ExamplesButton/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { PureComponent } from 'react' 3 | import { Icon } from 'antd' 4 | import { connect } from 'react-redux' 5 | import { openDrawer, closeDrawer } from 'containers/Drawer/actions' 6 | 7 | type OwnProps = {||} 8 | type StateProps = {| 9 | isVisible: boolean, 10 | |} 11 | type DispatchProps = {| 12 | closeDrawer: (id: string, animationSpeed?: string) => void, 13 | openDrawer: (id: string, animationSpeed?: string) => void 14 | |} 15 | type Props = {| ...OwnProps, ...StateProps, ...DispatchProps |} 16 | 17 | class _ExamplesButton extends PureComponent { 18 | 19 | onClick = () => { 20 | if (this.props.isVisible) { 21 | this.props.closeDrawer('examples') 22 | } 23 | else { 24 | this.props.openDrawer('examples') 25 | } 26 | } 27 | 28 | render() { 29 | return ( 30 | 31 | 32 | Examples 33 | 34 | ) 35 | } 36 | 37 | } 38 | 39 | const mapStateToProps = ( { drawers } ) => ( { 40 | isVisible: drawers.examples.isVisible 41 | } ) 42 | 43 | const mapDispatchToProps = { 44 | openDrawer, 45 | closeDrawer 46 | } 47 | 48 | export default connect(mapStateToProps, mapDispatchToProps)(_ExamplesButton) 49 | -------------------------------------------------------------------------------- /client/src/views/MainView/Header/containers/HeaderMenu/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react' 3 | import { Dropdown, Button, Menu, Icon } from 'antd' 4 | import theme from 'constants/theme' 5 | import styled from 'styled-components' 6 | import withSizes from 'react-sizes' 7 | 8 | const IconHolder = styled.span` 9 | display:flex; 10 | justify-content: center; 11 | align-items: center; 12 | width: 32px; 13 | font-size: 150%; 14 | ` 15 | 16 | const style = { 17 | display: 'flex', 18 | backgroundColor: theme.gray[1], 19 | borderBottom: `1px solid ${theme.gray[5]}` 20 | } 21 | 22 | type Props = { 23 | children: React.Node, 24 | collapsed: boolean 25 | } 26 | 27 | const _HeaderMenu = (props: Props) => { 28 | 29 | return props.collapsed 30 | ? ( 31 | {props.children}} 33 | trigger={['click']} 34 | > 35 | 40 | 41 | ) 42 | : ( 43 | 47 | {props.children} 48 | 49 | ) 50 | } 51 | 52 | const mapSizesToProps = ( { width } ) => ( { collapsed: width < 570 } ) 53 | 54 | export default withSizes(mapSizesToProps)(_HeaderMenu) 55 | -------------------------------------------------------------------------------- /client/src/views/MainView/Header/containers/ShareButton/components/share.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/views/MainView/Header/containers/ShareButton/index.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import ShareButton from './components/ShareButton' 3 | import { setProperty } from 'containers/MathObjects/actions' 4 | import { setCreationDate } from 'services/metadata/actions' 5 | 6 | const mapStateToProps = null 7 | const mapDispatchToState = { setProperty, setCreationDate } 8 | 9 | export default connect(mapStateToProps, mapDispatchToState)(ShareButton) 10 | -------------------------------------------------------------------------------- /client/src/views/MainView/Header/containers/TitleInput/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react' 3 | import EditableDescription from 'components/EditableDescription' 4 | import { setTitle } from 'services/metadata/actions' 5 | import { connect } from 'react-redux' 6 | import styled from 'styled-components' 7 | 8 | const StyledInput = styled(EditableDescription)` 9 | background-color: rgba(0, 0, 0, 0); 10 | margin-left:8px; 11 | font-weight:bold; 12 | padding-top:4px; 13 | ` 14 | 15 | type OwnProps = {||} 16 | type StateProps = {| 17 | title: string 18 | |} 19 | type DispatchProps = {| 20 | setTitle: (title: string) => void 21 | |} 22 | type Props = {| 23 | ...OwnProps, 24 | ...StateProps, 25 | ...DispatchProps 26 | |} 27 | 28 | function _TitleInput(props: Props) { 29 | return ( 30 | 34 | ) 35 | } 36 | 37 | const mapStateToProps = ( { metadata } ) => ( { 38 | title: metadata.title 39 | } ) 40 | 41 | const mapDispatchToProps = { setTitle } 42 | 43 | export default connect(mapStateToProps, mapDispatchToProps)(_TitleInput) 44 | -------------------------------------------------------------------------------- /client/src/views/MainView/Header/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react' 3 | import styled from 'styled-components' 4 | import { HEADER_HEIGHT_PX } from '../../../constants' 5 | import ShareButton from './containers/ShareButton' 6 | import HeaderButton from './components/HeaderButton' 7 | import TitleInput from './containers/TitleInput' 8 | import HelpButton from './components/HelpButton' 9 | import ExamplesButton from './containers/ExamplesButton' 10 | import store from 'store/index' 11 | import HeaderMenu from './containers/HeaderMenu' 12 | import { Menu } from 'antd' 13 | const Item = Menu.Item 14 | 15 | const HeaderContainer = styled.div` 16 | background-color: ${props => props.theme.gray[1]}; 17 | height: ${ HEADER_HEIGHT_PX }px; 18 | display:flex; 19 | width:100%; 20 | align-items:center; 21 | justify-content: space-between; 22 | border-bottom: 1pt solid ${props => props.theme.gray[5]}; 23 | ` 24 | 25 | const HeaderGroup = styled.div` 26 | padding-left: 10px; 27 | padding-right: 10px; 28 | display:flex; 29 | align-items:center; 30 | ` 31 | 32 | const Header = () => { 33 | return ( 34 | 35 | 36 | Math3D 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | ) 48 | } 49 | 50 | export default Header 51 | -------------------------------------------------------------------------------- /client/src/views/MainView/Scene.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import MathBoxContainer from 'containers/MathBoxContainer' 3 | import MathBoxScene from 'containers/MathBoxScene' 4 | import { mathboxElement } from "containers/MathBoxScene/components/MathBoxScene"; 5 | import styled from 'styled-components' 6 | 7 | const SceneBoundary = styled.div` 8 | display:flex; 9 | height:100%; 10 | overflow: hidden; 11 | flex: 1; 12 | ` 13 | 14 | export default class Math3dScene extends PureComponent { 15 | 16 | render() { 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | ) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /client/src/views/MainView/UserControls/index.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import ControlsDrawer from './components/ControlsDrawer' 3 | import ScreenSizeDrawerManager from 'containers/Drawer/ScreenSizeDrawerManager' 4 | 5 | export default function UserControls() { 6 | return ( 7 | 8 | 9 | 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /client/src/views/MainView/actions.js: -------------------------------------------------------------------------------- 1 | import { getGraph } from 'services/api' 2 | import { loadDehydratedState } from 'store/actions' 3 | 4 | export function loadGraphFromDb(id) { 5 | 6 | return async dispatch => { 7 | const { dehydrated } = await getGraph(id) 8 | if (dehydrated) { 9 | const action = loadDehydratedState(dehydrated) 10 | return dispatch(action) 11 | } 12 | else { 13 | console.group() 14 | console.warn(`Graph ${id} not found`) 15 | // TODO: Better error handling on client 16 | console.groupEnd() 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /client/src/views/MainView/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { PureComponent } from 'react' 3 | import FlexContainer from 'components/FlexContainer' 4 | import UserControls from './UserControls' 5 | import Scene from './Scene' 6 | import Examples from './Examples' 7 | import Header from './Header' 8 | import { loadGraphFromDb } from './actions' 9 | import { loadDehydratedState } from 'store/actions' 10 | import initialState from 'store/initialState' 11 | import { connect } from 'react-redux' 12 | import { setLastSavedState } from 'services/lastSavedState/actions' 13 | 14 | type OwnProps = {| 15 | graphId?: string 16 | |} 17 | 18 | type DispatchProps = {| 19 | loadGraphFromDb: (id: string) => Function, 20 | setLastSavedState: typeof setLastSavedState, 21 | loadDehydratedState: (dehydrated: {} ) => void 22 | |} 23 | 24 | type Props = {| 25 | ...DispatchProps, 26 | ...OwnProps 27 | |} 28 | 29 | class MainView extends PureComponent { 30 | 31 | componentDidMount() { 32 | if (this.props.graphId) { 33 | this.props.loadGraphFromDb(this.props.graphId) 34 | } 35 | this.props.setLastSavedState() 36 | } 37 | 38 | componentDidUpdate() { 39 | if (this.props.graphId) { 40 | this.props.loadGraphFromDb(this.props.graphId) 41 | } 42 | else { 43 | this.props.loadDehydratedState(initialState) 44 | } 45 | } 46 | 47 | render() { 48 | return 49 |
    50 | 51 | 52 | 53 | 54 | 55 | 56 | } 57 | 58 | } 59 | 60 | const mapDispatchToProps = { 61 | loadGraphFromDb, 62 | loadDehydratedState, 63 | setLastSavedState 64 | } 65 | 66 | export default connect(null, mapDispatchToProps)(MainView) 67 | -------------------------------------------------------------------------------- /data/db/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | 4 | # Except for these two files 5 | !.gitignore 6 | !README.md -------------------------------------------------------------------------------- /data/db/README.md: -------------------------------------------------------------------------------- 1 | This directory is used for database storage during local development. 2 | 3 | All files in this directory are ignored by git, except: 4 | 5 | - `README.md` 6 | - `.gitignore` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "math3d", 3 | "version": "1.3.0", 4 | "description": "Express server for math3d-react app.", 5 | "engines": { 6 | "node": "14.1.x" 7 | }, 8 | "scripts": { 9 | "postinstall": "npm-run-all --parallel ci:server ci:client", 10 | "ci:server": "npm --prefix ./server ci", 11 | "ci:client": "npm --prefix ./client ci", 12 | "install:dev": "npm-run-all --parallel install:dev:server install:dev:client", 13 | "install:dev:server": "npm --prefix ./server --only=dev install", 14 | "install:dev:client": "npm --prefix ./client --only=dev install", 15 | "build": "npm-run-all --parallel build:server build:client", 16 | "build:server": "npm --prefix ./server run build", 17 | "build:client": "npm --prefix ./client run build", 18 | "start": "npm --prefix ./server start", 19 | "start:dev:server": "npm --prefix ./server run dev", 20 | "start:dev:client": "npm --prefix ./client run start" 21 | }, 22 | "cacheDirectories": [ 23 | "server/node_modules", 24 | "client/node_modules" 25 | ], 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/mars/heroku-cra-node.git" 29 | }, 30 | "dependencies": { 31 | "npm-run-all": "^4.1.5" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /server/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "plugins": [], 6 | "extends": [ 7 | "standard" 8 | ], 9 | "parser": "babel-eslint", 10 | "rules": { 11 | "no-unused-expressions": "error", 12 | "space-infix-ops": "off", 13 | "multiline-ternary": ["error", "always-multiline"], 14 | "space-in-parens": ["error", "never", { "exceptions": ["{}", "[]"] }], 15 | "object-curly-spacing": ["error", "always"], 16 | "no-use-before-define": ["error", { "functions": false }], 17 | "complexity": [1, 10], 18 | "space-before-function-paren": ["error", { 19 | "anonymous": "never", 20 | "named": "never", 21 | "asyncArrow": "always" 22 | }], 23 | "brace-style": ["error", "stroustrup", { "allowSingleLine": true }], 24 | "padded-blocks": ["error", {"classes": "always", "switches": "always"}] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/dotenv_template: -------------------------------------------------------------------------------- 1 | # rename this to .env and update the DATABASE_URL 2 | # Something of form: 3 | # 4 | # DATABASE_URL=postgres://[user[:password]@][netloc][:port][,...][/dbname] 5 | # 6 | # like: 7 | DATABASE_URL=postgres://math3d_user:abc-123@localhost:5432/math3d 8 | NODE_ENV=development -------------------------------------------------------------------------------- /server/examples/animate_camera.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "title":"Animating the Camera Position", 4 | "versionAtCreation": "0.2.0", 5 | "creationDate": "\"2018-12-22T17:40:32.554Z\"" 6 | }, 7 | "sortableTree": { 8 | "root": ["mainFolder"], 9 | "mainFolder": ["1", "5", "2", "3", "4"] 10 | }, 11 | "folders": { 12 | "cameraFolder": { 13 | "isCollapsed": false 14 | } 15 | }, 16 | "mathSymbols": { 17 | "2": { 18 | "type": "VARIABLE", 19 | "name": "f(t)", 20 | "value": "\\left[R\\left(t\\right)\\cdot\\cos\\left(t\\right),\\ R\\left(t\\right)\\cdot\\sin\\left(t\\right),\\ t\\right]", 21 | "description": "Function:\nThis function and the slider above are used to animate the camera position. Activate the \"Axes & Camera\" tab above to see how." 22 | }, 23 | "3": { 24 | "type": "VARIABLE", 25 | "name": "R\\left(t\\right)", 26 | "value": "3+0.25\\cdot t^2", 27 | "description": "Function" 28 | }, 29 | "5": { 30 | "type": "VARIABLE_SLIDER", 31 | "min": "-2\\pi", 32 | "max": "2\\pi", 33 | "isAnimating": true 34 | } 35 | }, 36 | "mathGraphics": { 37 | "1": { 38 | "type": "VECTOR", 39 | "color": "#e74c3c" 40 | }, 41 | "4": { 42 | "type": "PARAMETRIC_CURVE", 43 | "description": "Parametric Curve: Camera Path", 44 | "opacity": "0.25", 45 | "expr": "_f(t)=f\\left(t\\right)" 46 | }, 47 | "camera": { 48 | "type": "CAMERA", 49 | "description": "Camera\nWhile the camera position is animated, rotate, zoom, and pan are automatically turned off.", 50 | "isZoomEnabled": false, 51 | "isRotateEnabled": false, 52 | "relativePosition": [-0.9196249390446015, -0.35301093982086884, -0.2775073510670975], 53 | "relativeLookAt": [0, 0, 0], 54 | "computedPosition": "f\\left(T\\right)", 55 | "useComputed": true 56 | } 57 | }, 58 | "sliderValues": { 59 | "5": -2.7750735106709747 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /server/examples/color_and_visibility.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "title": "Changing Color and Visibility", 4 | "versionAtCreation": "0.2.0", 5 | "creationDate": "\"2018-12-22T15:59:38.144Z\"" 6 | }, 7 | "sortableTree": { 8 | "root": ["mainFolder"], 9 | "mainFolder": ["1"] 10 | }, 11 | "folders": {}, 12 | "mathSymbols": {}, 13 | "mathGraphics": { 14 | "1": { 15 | "type": "EXPLICIT_SURFACE_POLAR", 16 | "description": "Explicit Surface (Polar)\nPress the colored dot next to a graphics object to toggle visibility; press and hold to change color.", 17 | "color": "#e67e22" 18 | }, 19 | "camera": { 20 | "type": "CAMERA", 21 | "relativePosition": [1.0195314560707958, -1.0045557671355836, 0.3727241886392763], 22 | "relativeLookAt": [0, 0, 0] 23 | } 24 | }, 25 | "sliderValues": {} 26 | } 27 | -------------------------------------------------------------------------------- /server/examples/derivatives.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "title": "diff: Using Derivatives", 4 | "versionAtCreation": "0.2.0", 5 | "creationDate": "\"2018-12-22T16:53:47.714Z\"" 6 | }, 7 | "sortableTree": { 8 | "root": ["mainFolder", "4"], 9 | "4": ["3", "5", "6"], 10 | "mainFolder": ["1", "2", "7"] 11 | }, 12 | "folders": { 13 | "4": { 14 | "description": "Graphics" 15 | }, 16 | "mainFolder": { 17 | "description": "Controls" 18 | } 19 | }, 20 | "mathSymbols": { 21 | "1": { 22 | "type": "VARIABLE", 23 | "name": "f(t)", 24 | "value": "\\left[t,t^2,t^3\\right]", 25 | "description": "Function" 26 | }, 27 | "2": { 28 | "type": "VARIABLE", 29 | "name": "df(t)", 30 | "value": "\\operatorname{diff}\\left(f,\\ t\\right)", 31 | "description": "Function:\n\"diff\" is an operator that differentiates functions using the syntax below.\nIn general, \"diff\" returns the total derivative and may be scalar-valued, vector-valued, or matrix-valued." 32 | }, 33 | "7": { 34 | "type": "VARIABLE_SLIDER", 35 | "min": "-2", 36 | "max": "2" 37 | } 38 | }, 39 | "mathGraphics": { 40 | "3": { 41 | "type": "PARAMETRIC_CURVE", 42 | "expr": "_f(t)=f\\left(t\\right)", 43 | "range": "\\left[-2,2\\right]" 44 | }, 45 | "5": { 46 | "type": "POINT", 47 | "color": "#2c3e50", 48 | "coords": "f\\left(T\\right)" 49 | }, 50 | "6": { 51 | "type": "VECTOR", 52 | "color": "#e74c3c", 53 | "components": "df\\left(T\\right)", 54 | "tail": "f\\left(T\\right)" 55 | }, 56 | "camera": { 57 | "type": "CAMERA", 58 | "relativePosition": [-0.7500000000000001, -1.25, 0.2499999999999999], 59 | "relativeLookAt": [0, 0, 0] 60 | } 61 | }, 62 | "sliderValues": { 63 | "7": -1.04 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /server/examples/functions.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "title": "Using Functions", 4 | "versionAtCreation": "0.2.0", 5 | "creationDate": "\"2018-12-22T16:21:58.183Z\"" 6 | }, 7 | "sortableTree": { 8 | "root": ["mainFolder"], 9 | "mainFolder": ["1", "2", "3", "4"] 10 | }, 11 | "folders": {}, 12 | "mathSymbols": { 13 | "1": { 14 | "type": "VARIABLE", 15 | "name": "f\\left(t\\right)", 16 | "value": "\\left[t,\\ 3\\cdot\\cos\\left(\\pi\\ t\\right),\\ 2\\sin\\left(\\pi\\ t\\right)\\right]", 17 | "description": "Function\nFunctions can be scalar-valued or vector-valued. Here's a vector-valued function." 18 | }, 19 | "4": { 20 | "type": "VARIABLE_SLIDER", 21 | "min": "-4", 22 | "max": "4", 23 | "isAnimating": true 24 | } 25 | }, 26 | "mathGraphics": { 27 | "2": { 28 | "type": "PARAMETRIC_CURVE", 29 | "description": "Parametric Curve\nOnce a function is defined, we can re-use it for different purposes. Here, we draw a curve.", 30 | "expr": "_f(t)=f\\left(t\\right)", 31 | "range": "\\left[-4,\\ 4\\right]" 32 | }, 33 | "3": { 34 | "type": "POINT", 35 | "description": "Point\nAnd here we animate a point. Note that \"T\" is the slider value.", 36 | "color": "#e74c3c", 37 | "zBias": "1", 38 | "coords": "f\\left(T\\right)" 39 | }, 40 | "camera": { 41 | "type": "CAMERA", 42 | "relativePosition": [-0.6128516294248658, -1.5813337531654748, 0.3156182457264015], 43 | "relativeLookAt": [0, 0, 0] 44 | } 45 | }, 46 | "sliderValues": { 47 | "4": -2.900000000000004 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /server/examples/labels.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "versionAtCreation": "0.2.0", 4 | "creationDate": "\"2018-12-22T16:16:49.247Z\"" 5 | }, 6 | "sortableTree": { 7 | "root": ["mainFolder"], 8 | "mainFolder": ["1", "2", "3"] 9 | }, 10 | "folders": {}, 11 | "mathSymbols": {}, 12 | "mathGraphics": { 13 | "1": { 14 | "type": "POINT", 15 | "description": "Point:\nOpen object settings (press the gear) to add a label to a point. Make sure to also set \"labelVisible\".", 16 | "label": "My Point", 17 | "labelVisible": true, 18 | "coords": "\\left[3,-2,4\\right]" 19 | }, 20 | "2": { 21 | "type": "VECTOR", 22 | "description": "Vector\nVectors and lines can be labeled, too. Vector and line labels appear at the end.", 23 | "color": "#e74c3c", 24 | "label": "Your Vector", 25 | "labelVisible": true 26 | }, 27 | "3": { 28 | "type": "LINE", 29 | "color": "#8e44ad", 30 | "label": "A Line!", 31 | "labelVisible": true, 32 | "coords": "\\left[\\left[0,-4,\\ 0\\right],\\ \\left[-3,-1,2\\right]\\right]" 33 | }, 34 | "camera": { 35 | "type": "CAMERA", 36 | "relativePosition": [1.012829030692171, -1.9719352172426445, 0.23623915126975092], 37 | "relativeLookAt": [0, 0, 0] 38 | } 39 | }, 40 | "sliderValues": {} 41 | } 42 | -------------------------------------------------------------------------------- /server/examples/sliders_intro.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "title": "Using Variable Sliders", 4 | "versionAtCreation": "0.2.0", 5 | "creationDate": "\"2018-12-21T00:00:00.00Z\"" 6 | }, 7 | "sortableTree": { 8 | "root": ["mainFolder"], 9 | "mainFolder": ["1", "2", "3"] 10 | }, 11 | "folders": {}, 12 | "mathSymbols": { 13 | "1": { 14 | "type": "VARIABLE_SLIDER", 15 | "name": "A", 16 | "min": "-20", 17 | "max": "20" 18 | }, 19 | "2": { 20 | "type": "VARIABLE_SLIDER", 21 | "name": "b", 22 | "min": "0.5", 23 | "max": "3", 24 | "description": "Another Slider!" 25 | } 26 | }, 27 | "mathGraphics": { 28 | "3": { 29 | "type": "EXPLICIT_SURFACE", 30 | "description": "Explicit Surface:\nWe can use the slider variables defined above in defining a surface (or any expression)", 31 | "expr": "_f(x,y)=\\frac{A}{b}\\cdot x\\cdot e^{-\\frac{x^2+y^2}{b^2}}", 32 | "rangeU": "\\left[-4,4\\right]", 33 | "rangeV": "\\left[-4,4\\right]" 34 | }, 35 | "camera": { 36 | "type": "CAMERA", 37 | "relativePosition": [-0.5471573933914443, -1.6103676673024785, 0.636113597408501], 38 | "relativeLookAt": [0, 0, 0] 39 | } 40 | }, 41 | "sliderValues": { 42 | "1": 10, 43 | "2": 2.1 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /server/examples/sliders_plotrange.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "title": "Using Sliders to Animate Plot Range", 4 | "versionAtCreation": "0.2.0", 5 | "creationDate": "\"2018-12-21T00:00:00.00Z\"" 6 | }, 7 | "sortableTree": { 8 | "root": ["mainFolder"], 9 | "mainFolder": ["1", "3"] 10 | }, 11 | "folders": {}, 12 | "mathSymbols": { 13 | "3": { 14 | "type": "VARIABLE_SLIDER", 15 | "min": "-2\\pi", 16 | "max": "2\\pi", 17 | "description": "Variable Slider\nSlider endpoints can contain symbols like pi, or other even variables.", 18 | "isAnimating": true 19 | } 20 | }, 21 | "mathGraphics": { 22 | "1": { 23 | "type": "PARAMETRIC_CURVE", 24 | "description": "Parametric Curve\nSliders can be used to animate the plot range for curves and surfaces.", 25 | "color": "#e67e22", 26 | "expr": "_f(t)=\\left[4\\cos\\left(2t\\right),\\ 2\\sin\\left(2t\\right),\\ t\\right]", 27 | "range": "\\left[-2\\pi,\\ T\\right]" 28 | }, 29 | "camera": { 30 | "type": "CAMERA", 31 | "relativePosition": [-0.7167441624363, -2.104800027939397, 0.7504646780824356], 32 | "relativeLookAt": [0, 0, 0] 33 | } 34 | }, 35 | "sliderValues": { 36 | "3": -2.6179938779914855 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /server/examples/sphere_colormap.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "title": "Color Maps on a Sphere", 4 | "versionAtCreation": "1.0.0", 5 | "creationDate": "\"2019-01-02T15:11:05.430Z\"" 6 | }, 7 | "sortableTree": { 8 | "root": ["mainFolder"], 9 | "mainFolder": ["6", "3", "4", "5"] 10 | }, 11 | "folders": {}, 12 | "mathSymbols": { 13 | "4": { 14 | "type": "VARIABLE", 15 | "name": "f\\left(\\phi,\\ \\theta\\right)", 16 | "value": "\\left[\\cos\\left(\\phi\\right)\\cdot\\sin\\left(\\theta\\right),\\ \\sin\\left(\\phi\\right)\\cdot\\sin\\left(\\theta\\right),\\cos\\left(\\theta\\right)\\right]", 17 | "description": "Parametric description of sphere:" 18 | }, 19 | "5": { 20 | "type": "VARIABLE", 21 | "name": "g\\left(\\phi,\\ \\theta\\right)", 22 | "value": "\\frac{1+\\cos\\left(3\\phi\\right)}{2}\\cdot\\sin\\left(2\\theta\\right)^2", 23 | "description": "Color function on Sphere" 24 | }, 25 | "6": { 26 | "type": "VARIABLE_SLIDER", 27 | "min": "0", 28 | "max": "2\\pi", 29 | "description": "Variable Slider: Angle Shift", 30 | "isAnimating": true, 31 | "speedMultiplier": 2 32 | } 33 | }, 34 | "mathGraphics": { 35 | "3": { 36 | "type": "PARAMETRIC_SURFACE", 37 | "description": "Parametric Surface:\nThis sphere uses g(u - T, v) as its color map.\nHold-press the rainbow, then switch to \"Color Map\" for details.", 38 | "color": "temperature", 39 | "shaded": false, 40 | "expr": "_f(u,v)=3f\\left(u,v\\right)", 41 | "rangeU": "\\left[0,2\\pi\\right]", 42 | "rangeV": "\\left[0,\\pi\\right]", 43 | "colorExpr": "_f(X, Y, Z, u, v)=g\\left(u-T,v\\right)", 44 | "uSamples": "128", 45 | "vSamples": "128" 46 | }, 47 | "camera": { 48 | "type": "CAMERA", 49 | "relativePosition": [-2.9080376238585313, 1.9807994129981867, 1.21822050953895], 50 | "relativeLookAt": [0, 0, 0] 51 | }, 52 | "axis-z": { 53 | "type": "AXIS", 54 | "scale": "1" 55 | } 56 | }, 57 | "sliderValues": { 58 | "6": 5.864306286700953 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /server/examples/tnb.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "title": "The TNB Vectors for Parametric Curves", 4 | "versionAtCreation": "0.2.0", 5 | "creationDate": "\"2018-12-22T17:21:31.242Z\"" 6 | }, 7 | "sortableTree": { 8 | "root": ["mainFolder"], 9 | "mainFolder": ["1", "2", "3", "4", "5", "6"] 10 | }, 11 | "folders": {}, 12 | "mathSymbols": { 13 | "1": { 14 | "type": "VARIABLE", 15 | "name": "f\\left(t\\right)", 16 | "value": "\\left[t^3,t^2,t\\right]" 17 | }, 18 | "2": { 19 | "type": "VARIABLE_SLIDER", 20 | "min": "-2", 21 | "max": "2", 22 | "isAnimating": true 23 | } 24 | }, 25 | "mathGraphics": { 26 | "3": { 27 | "type": "PARAMETRIC_CURVE", 28 | "expr": "_f(t)=f\\left(t\\right)", 29 | "range": "\\left[-2,2\\right]" 30 | }, 31 | "4": { 32 | "type": "VECTOR", 33 | "color": "#e74c3c", 34 | "components": "\\operatorname{unitT}\\left(f,\\ T\\right)", 35 | "tail": "f\\left(T\\right)" 36 | }, 37 | "5": { 38 | "type": "VECTOR", 39 | "color": "#9b59b6", 40 | "components": "\\operatorname{unitN}\\left(f,\\ T\\right)", 41 | "tail": "f\\left(T\\right)" 42 | }, 43 | "6": { 44 | "type": "VECTOR", 45 | "color": "#e67e22", 46 | "components": "\\operatorname{unitB}\\left(f,\\ T\\right)", 47 | "tail": "f\\left(T\\right)" 48 | }, 49 | "camera": { 50 | "type": "CAMERA", 51 | "relativePosition": [-0.5865871668594441, -0.6193625968949049, 0.23768345443481234], 52 | "relativeLookAt": [0, 0, 0] 53 | }, 54 | "axis-z": { 55 | "type": "AXIS", 56 | "scale": "1" 57 | } 58 | }, 59 | "sliderValues": { 60 | "2": -0.6666666666666637 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /server/examples/vectors.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "title": "Vectors and Vector Tails", 4 | "versionAtCreation": "0.2.0", 5 | "creationDate": "\"2018-12-22T16:08:02.841Z\"" 6 | }, 7 | "sortableTree": { 8 | "root": ["mainFolder"], 9 | "mainFolder": ["1", "2", "3", "4"] 10 | }, 11 | "folders": {}, 12 | "mathSymbols": { 13 | "4": { 14 | "type": "VARIABLE_SLIDER", 15 | "name": "X", 16 | "value": "-2" 17 | } 18 | }, 19 | "mathGraphics": { 20 | "1": { 21 | "type": "VECTOR", 22 | "description": "Vector:\nBy default, vectors have their tails at the origin" 23 | }, 24 | "2": { 25 | "type": "VECTOR", 26 | "description": "Vector:\nA different tail can be specified in object settings. Open object settings by pressing the gear.", 27 | "color": "#f1c40f", 28 | "tail": "\\left[-3,2,1\\right]" 29 | }, 30 | "3": { 31 | "type": "VECTOR", 32 | "description": "Vector:\nThe tail can position can contain variables or functions.\nHere the tail has coordinates [X,-2,-1]", 33 | "color": "#9b59b6", 34 | "components": "\\left[3,\\ 2,\\ 1\\right]", 35 | "tail": "\\left[X,\\ -2,\\ -1\\right]" 36 | }, 37 | "camera": { 38 | "type": "CAMERA", 39 | "relativePosition": [-0.47385710974897977, -1.3505688130352802, 0.3727241886392734], 40 | "relativeLookAt": [0, 0, 0] 41 | } 42 | }, 43 | "sliderValues": { 44 | "4": 0 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /server/examples/z_bias.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "title": "Fine-tuning Visibility with z-bias", 4 | "versionAtCreation": "0.2.0", 5 | "creationDate": "\"2018-12-22T17:12:37.750Z\"" 6 | }, 7 | "sortableTree": { 8 | "root": ["mainFolder"], 9 | "mainFolder": ["4", "5", "2", "3"] 10 | }, 11 | "folders": {}, 12 | "mathSymbols": { 13 | "4": { 14 | "type": "VARIABLE", 15 | "name": "f\\left(x,\\ y\\right)", 16 | "value": "\\frac{1}{2}y^2-x^2" 17 | } 18 | }, 19 | "mathGraphics": { 20 | "2": { 21 | "type": "PARAMETRIC_CURVE", 22 | "description": "ParametricCurve\nThis red curve is hard to see because it has the same 3D-position as the blue surface. The renderer can't decide which object to draw on top.", 23 | "color": "#e74c3c", 24 | "width": "6", 25 | "expr": "_f(t)=\\left[t,\\ 2,\\ f\\left(t,\\ 2\\right)\\right]", 26 | "range": "\\left[-3,3\\right]" 27 | }, 28 | "3": { 29 | "type": "PARAMETRIC_CURVE", 30 | "description": "Parametric Curve\nSetting the z-bias in object settings (click the gear) is a nice way to solve this problem. Try viewing the yellow line from different angle: it will always appear closer to the camera.\nThe z-bias of an object is a positional bias toward the camera (\"z\" in the context refers to an axis pointing toward the camera, not the mathematical z axis.)", 31 | "color": "#f1c40f", 32 | "zBias": "1", 33 | "width": "6", 34 | "expr": "_f(t)=\\left[t,\\ 2.1,\\ f\\left(t,2.1\\right)\\right]", 35 | "range": "\\left[-3,3\\right]" 36 | }, 37 | "5": { 38 | "type": "EXPLICIT_SURFACE", 39 | "opacity": "1", 40 | "expr": "_f(x,y)=f\\left(x,\\ y\\right)", 41 | "rangeU": "\\left[-2,2\\right]", 42 | "rangeV": "\\left[-3,3\\right]" 43 | }, 44 | "camera": { 45 | "type": "CAMERA", 46 | "relativePosition": [1.588086401796261, -1.1559540607206158, 0.7920361512245293], 47 | "relativeLookAt": [0, 0, 0] 48 | } 49 | }, 50 | "sliderValues": {} 51 | } 52 | -------------------------------------------------------------------------------- /server/migrations/create_database.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE math3d; 2 | CREATE USER math3d_user; 3 | GRANT ALL PRIVILEGES ON DATABASE math3d TO math3d_user; 4 | GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO math3d_user; 5 | GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO math3d_user; -------------------------------------------------------------------------------- /server/migrations/database_setup.sql: -------------------------------------------------------------------------------- 1 | /* 2 | brew update 3 | brew install postgresql 4 | initdb /usr/local/var/postgres 5 | pg_ctl -D /usr/local/var/postgres start 6 | psql -d postgres 7 | CREATE DATABASE math3d; 8 | CREATE USER math3d_user; 9 | GRANT ALL PRIVILEGES ON DATABASE math3d TO math3d_user; 10 | GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO math3d_user; 11 | GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO math3d_user; 12 | */ 13 | 14 | CREATE TABLE graphs ( 15 | id SERIAL PRIMARY KEY, 16 | url_key text NOT NULL UNIQUE, 17 | dehydrated JSONB NOT NULL, 18 | times_accessed INTEGER NOT NULL DEFAULT 1, 19 | last_accessed timestamp with time zone NOT NULL DEFAULT now() 20 | ); -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "math3d-server", 3 | "version": "1.3.0", 4 | "description": "Express server for math3d-react app.", 5 | "scripts": { 6 | "build": "webpack --config webpack.config.server.js", 7 | "dev": "npm-run-all --parallel dev:webpack dev:nodemon", 8 | "dev:webpack": "webpack --mode development --config webpack.config.server.js --watch", 9 | "dev:nodemon": "nodemon --require dotenv/config build/main.js", 10 | "start": "node --require dotenv/config build/main.js" 11 | }, 12 | "dependencies": { 13 | "@babel/core": "^7.5.5", 14 | "@babel/preset-env": "^7.5.5", 15 | "babel-eslint": "^10.0.3", 16 | "babel-loader": "^8.0.6", 17 | "body-parser": "^1.18.3", 18 | "core-js": "^3.2.1", 19 | "dotenv": "^6.0.0", 20 | "express": "^4.16.3", 21 | "fs": "0.0.1-security", 22 | "minimist": "^1.2.5", 23 | "mongodb": "^3.1.6", 24 | "mongoose": "^5.7.5", 25 | "nodemon": "^1.19.1", 26 | "npm-run-all": "^4.1.5", 27 | "pg-promise": "^10.6.2", 28 | "raw-loader": "^4.0.1", 29 | "webpack": "^4.39.2", 30 | "webpack-cli": "^3.3.7", 31 | "webpack-node-externals": "^1.7.2" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "https://github.com/mars/heroku-cra-node.git" 36 | }, 37 | "devDependencies": { 38 | "eslint": "^6.3.0", 39 | "eslint-config-standard": "^14.1.0", 40 | "eslint-plugin-import": "^2.18.2", 41 | "eslint-plugin-node": "^9.2.0", 42 | "eslint-plugin-promise": "^4.2.1", 43 | "eslint-plugin-standard": "^4.0.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /server/src/database/attachDb.js: -------------------------------------------------------------------------------- 1 | export const attachDb = db => (req, res, next) => { 2 | req.db = db 3 | next() 4 | } 5 | -------------------------------------------------------------------------------- /server/src/database/getDb.js: -------------------------------------------------------------------------------- 1 | import pg from 'pg-promise' 2 | 3 | /** 4 | * This doesn't seem great. 5 | */ 6 | const DEFAULT_OPTIONS = 7 | process.env.NODE_ENV === 'development' 8 | ? {} 9 | : { ssl: { sslmode: 'require', rejectUnauthorized: false } }; 10 | 11 | export const getDb = (options) => pg()({ 12 | connectionString: process.env.DATABASE_URL, 13 | ...DEFAULT_OPTIONS, 14 | ...options 15 | }); 16 | -------------------------------------------------------------------------------- /server/src/database/index.js: -------------------------------------------------------------------------------- 1 | import { getDb } from './getDb' 2 | import { attachDb } from './attachDb' 3 | import { seedDb } from './seedDb' 4 | 5 | export { getDb, attachDb, seedDb } 6 | -------------------------------------------------------------------------------- /server/src/database/mongoose.config.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | 3 | // Be sure that this runs before any other mongoose code. 4 | 5 | // To avoid deprecation warnings. 6 | // https://mongoosejs.com/docs/deprecations.html 7 | mongoose.set('useNewUrlParser', true) 8 | mongoose.set('useFindAndModify', false) 9 | mongoose.set('useCreateIndex', true) 10 | // More customizations 11 | mongoose.set('strict', 'throw') -------------------------------------------------------------------------------- /server/src/database/seedDb.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import { promisify } from 'util' 3 | import * as queries from '../graph/queries' 4 | 5 | const readdir = promisify(fs.readdir) 6 | const readFile = promisify(fs.readFile) 7 | 8 | export async function seedDb(db) { 9 | const examplesDir = './examples' 10 | 11 | // Save database object from the callback for reuse. 12 | const files = await readdir(examplesDir) 13 | const contents = await Promise.all(files.map(file => { 14 | return readFile(`${examplesDir}/${file}`, 'utf8') 15 | } )) 16 | 17 | const extension = '.json' 18 | const examples = contents.map((content, i) => { 19 | const urlKey = files[i].slice(0, -extension.length) 20 | const dehydrated = JSON.parse(content) 21 | return { urlKey, dehydrated } 22 | } ) 23 | 24 | await Promise.all(examples.map(example => db.none(queries.upsertGraph, example))) 25 | } 26 | -------------------------------------------------------------------------------- /server/src/graph/controller.js: -------------------------------------------------------------------------------- 1 | import * as queries from "./queries"; 2 | 3 | export async function saveNewGraph(req, res) { 4 | const db = req.db; 5 | const { urlKey, dehydrated } = req.body; 6 | await db.none(queries.insertGraph, { urlKey, dehydrated }); 7 | res.status(201).json({ success: true }); 8 | } 9 | 10 | export async function loadGraph(req, res) { 11 | const db = req.db; 12 | const row = await db.one(queries.getGraph, { urlKey: req.params.id }); 13 | res.status(200).json(row); 14 | } 15 | -------------------------------------------------------------------------------- /server/src/graph/index.js: -------------------------------------------------------------------------------- 1 | export { saveNewGraph, loadGraph } from './controller' 2 | -------------------------------------------------------------------------------- /server/src/graph/queries/get_graph.sql: -------------------------------------------------------------------------------- 1 | WITH update_access AS ( 2 | UPDATE graphs SET 3 | times_accessed = times_accessed + 1, 4 | last_accessed = now() 5 | WHERE url_key = ${ urlKey } 6 | ) 7 | SELECT dehydrated FROM graphs WHERE url_key = ${ urlKey }; -------------------------------------------------------------------------------- /server/src/graph/queries/index.js: -------------------------------------------------------------------------------- 1 | import getGraph from './get_graph.sql' 2 | import insertGraph from './insert_graph.sql' 3 | import upsertGraph from './upsert_graph.sql' 4 | 5 | /** 6 | * TODO: Probably would be better to turn these queries into pg-promise 7 | * QueryFile objects, but having trouble with webpack. 8 | * Probably should not have used webpack on the backend =/. 9 | */ 10 | 11 | export { insertGraph, getGraph, upsertGraph } 12 | -------------------------------------------------------------------------------- /server/src/graph/queries/insert_graph.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO graphs ( 2 | url_key, 3 | dehydrated 4 | ) 5 | VALUES (${ urlKey }, ${dehydrated}); -------------------------------------------------------------------------------- /server/src/graph/queries/upsert_graph.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO graphs ( 2 | url_key, 3 | dehydrated 4 | ) 5 | VALUES (${ urlKey }, ${dehydrated}) 6 | ON CONFLICT (url_key) DO UPDATE SET dehydrated = ${dehydrated}; -------------------------------------------------------------------------------- /server/src/index.js: -------------------------------------------------------------------------------- 1 | // From https://github.com/mars/heroku-cra-node 2 | import 'source-map-support/register' 3 | import './database/mongoose.config' 4 | import { seedDb, getDb } from './database' 5 | import cluster from 'cluster' 6 | import os from 'os' 7 | import { startServer } from './server' 8 | 9 | // Multi-process to utilize all CPU cores. 10 | if (cluster.isMaster) { 11 | console.error(`Node cluster master ${process.pid} is running`) 12 | seedDb(getDb()) 13 | 14 | // Fork workers. 15 | for (let i = 0; i < os.cpus().length; i++) { 16 | cluster.fork() 17 | } 18 | 19 | cluster.on('exit', (worker, code, signal) => { 20 | console.error(`Node cluster worker ${worker.process.pid} exited: code ${code}, signal ${signal}`); 21 | } ) 22 | 23 | } 24 | else { 25 | startServer() 26 | } 27 | -------------------------------------------------------------------------------- /server/src/script.js: -------------------------------------------------------------------------------- 1 | import 'source-map-support/register' 2 | import './database/mongoose.config' 3 | import { migrate } from './scripts/to_postgres' 4 | 5 | /** 6 | * Run with: 7 | * node --require dotenv/config build/script.js 8 | */ 9 | migrate() -------------------------------------------------------------------------------- /server/src/scripts/to_postgres.js: -------------------------------------------------------------------------------- 1 | import { getDb } from '../database' 2 | import mongoose, { Schema, model } from 'mongoose' 3 | import * as queries from '../graph/queries' 4 | 5 | const { MONGODB_URI } = process.env 6 | 7 | const GRAPH_COLLECTION = 'graph' 8 | 9 | const dehydratedSchema = new Schema( { 10 | metadata: Object, 11 | sortableTree: Object, 12 | folders: Object, 13 | mathSymbols: Object, 14 | mathGraphics: Object, 15 | sliderValues: Object 16 | }, { _id: false, minimize: false } ) 17 | 18 | const graphSchema = Schema( { 19 | dehydrated: dehydratedSchema, 20 | _id: String, 21 | timesAccessed: { type: Number, default: 0 } 22 | } ) 23 | 24 | const Graph = model('Graph', graphSchema, GRAPH_COLLECTION) 25 | 26 | mongoose.connect(MONGODB_URI) 27 | 28 | const migrateOne = async (tx, graph, status) => { 29 | status.started++ 30 | const { _id: urlKey, dehydrated } = graph; 31 | if (status.started % 100 === 0 ) { 32 | console.log(status); 33 | } 34 | await tx.none(queries.upsertGraph, { urlKey, dehydrated }); 35 | if (status.finished % 100 === 0) { 36 | console.log(status); 37 | } 38 | status.finished++ 39 | } 40 | 41 | export async function migrate() { 42 | const db = getDb({ 43 | ssl: { sslmode: 'require', rejectUnauthorized: false } 44 | }); 45 | 46 | const cursor = Graph.find( {} ).cursor() 47 | const status = { 48 | started: 0, 49 | finished: 0, 50 | } 51 | 52 | const readData = new Promise((resolve, reject) => { 53 | cursor.on('close', () => { 54 | console.log('Done reading.') 55 | resolve(); 56 | } ) 57 | cursor.on('error', e => reject(e)); 58 | }); 59 | 60 | db.txIf(async tx => { 61 | const writeData = []; 62 | cursor.on('data', graph => { 63 | writeData.push(migrateOne(tx, graph, status)) 64 | }) 65 | await readData; 66 | await Promise.all(writeData); 67 | console.log(status); 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /server/src/server.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { saveNewGraph, loadGraph } from './graph' 3 | import { wrapAsync } from './utils' 4 | import bodyParser from 'body-parser' 5 | import path from 'path' 6 | import { getDb, attachDb } from './database' 7 | 8 | const STATIC_DIR = path.resolve(__dirname, '../../client/build') 9 | const PORT = process.env.PORT || 5000 10 | 11 | export function startServer() { 12 | // will hold the db object 13 | const app = express() 14 | 15 | // get json from request bodies 16 | app.use(bodyParser.json()) 17 | 18 | // Priority serve any static files. 19 | app.use(express.static(STATIC_DIR)) 20 | 21 | const db = getDb(); 22 | app.use(attachDb(db)); 23 | 24 | app.listen(PORT, () => { 25 | console.error(`Node cluster worker ${process.pid}: listening on port ${PORT}`) 26 | } ) 27 | 28 | app.post('/api/graph', saveNewGraph) 29 | 30 | app.get('/api/graph/:id', wrapAsync(loadGraph)) 31 | 32 | app.use(function(error, req, res, next) { 33 | console.group() 34 | console.warn('Server Error!') 35 | console.warn(error) 36 | console.groupEnd() 37 | res.status(500).json( { error: error.message } ) 38 | } ) 39 | 40 | // All remaining requests return the React app, so it can handle routing. 41 | app.get('*', function(request, response) { 42 | console.log('BACKUP ROUTE:') 43 | response.sendFile(path.resolve(STATIC_DIR, 'index.html')); 44 | } ) 45 | } 46 | -------------------------------------------------------------------------------- /server/src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Automatically catch asynchronous errors for express 3 | * @param {Function} fn an asynchronous express route handler 4 | */ 5 | export function wrapAsync(fn) { 6 | return function(req, res, next) { 7 | fn(req, res, next).catch(next) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /server/webpack.config.server.js: -------------------------------------------------------------------------------- 1 | // Followed https://medium.com/@christossotiriou/speed-up-nodejs-server-side-development-with-webpack-4-hmr-8b99a932bdda 2 | // with some modifications. 3 | const webpack = require('webpack') 4 | const path = require('path') 5 | const nodeExternals = require('webpack-node-externals') 6 | 7 | module.exports = { 8 | mode: process.env.NODE_ENV, 9 | devtool: 'source-map', 10 | externals: [ 11 | nodeExternals() 12 | ], 13 | name: 'server', 14 | plugins: [ 15 | new webpack.NamedModulesPlugin() 16 | ], 17 | target: 'node', 18 | entry: { 19 | main: path.resolve(path.join(__dirname, './src/index.js')), 20 | script: path.resolve(path.join(__dirname, './src/script.js')) 21 | }, 22 | output: { 23 | publicPath: './', 24 | path: path.resolve(__dirname, './build/'), 25 | filename: '[name].js', 26 | libraryTarget: 'commonjs2' 27 | }, 28 | resolve: { 29 | extensions: ['.webpack-loader.js', '.web-loader.js', '.loader.js', '.js'], 30 | modules: [ 31 | path.resolve(__dirname, 'node_modules') 32 | ] 33 | }, 34 | module: { 35 | rules: [ 36 | { 37 | test: /\.(js)$/, 38 | loader: 'babel-loader' 39 | }, 40 | { 41 | test: /\.sql$/i, 42 | use: 'raw-loader', 43 | }, 44 | ] 45 | }, 46 | node: { 47 | console: false, 48 | global: false, 49 | process: false, 50 | Buffer: false, 51 | __filename: false, 52 | __dirname: false 53 | } 54 | }; 55 | --------------------------------------------------------------------------------
  • 46 | 47 | 48 | 53 | Open an issue on Github. 54 | 55 |