Open-sourcing Doodle3D Transform. Enjoy!
33
.eslintrc
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"extends": "eslint-config-airbnb",
|
||||
"parser": "babel-eslint",
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true,
|
||||
"modules": true,
|
||||
"jsx": true
|
||||
},
|
||||
"rules": {
|
||||
"comma-dangle": [1, "never"],
|
||||
"no-else-return": 0,
|
||||
"no-use-before-define": [2, "nofunc"],
|
||||
"no-param-reassign": 0,
|
||||
"no-var": 1,
|
||||
"no-labels": 0,
|
||||
"guard-for-in": 0,
|
||||
"prefer-const": 0,
|
||||
"no-unused-vars": 1,
|
||||
"key-spacing": [1, {"beforeColon": false, "afterColon": true, "mode": "minimum"}],
|
||||
"no-loop-func": 1,
|
||||
"react/sort-comp": [0],
|
||||
"max-len": [1, 110, 4],
|
||||
"camelcase": 1,
|
||||
"new-cap": 0
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true
|
||||
},
|
||||
"globals": {
|
||||
"THREE": false
|
||||
}
|
||||
}
|
88
.gitignore
vendored
Normal file
@ -0,0 +1,88 @@
|
||||
# JSPM files
|
||||
jspm_packages/*
|
||||
build.js.map
|
||||
build.js
|
||||
bundle.js
|
||||
bundle.js.map
|
||||
bundle-tests.js
|
||||
bundle-tests.js.map
|
||||
bundle-deps.js
|
||||
bundle-deps.js.map
|
||||
|
||||
# Cordova files
|
||||
res/ios
|
||||
www
|
||||
platforms
|
||||
plugins
|
||||
|
||||
# Im done extension files
|
||||
.imdone/*
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
jspm_packages
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# private environment variables
|
||||
env.sh
|
||||
*-env.sh
|
||||
*.env
|
||||
|
||||
# certificate files
|
||||
server/cert
|
||||
|
||||
# Build files
|
||||
dist/*
|
||||
|
||||
# Cache files
|
||||
.cache-loader
|
||||
|
||||
*.DS_Store
|
||||
|
||||
# generated environments variables file
|
||||
src/js/env.js
|
||||
|
||||
CHANGELOG.html
|
||||
|
||||
# backup files
|
||||
.backups
|
||||
|
||||
app.env
|
||||
|
||||
# generated cordova assets
|
||||
cordova/res/ios
|
||||
|
||||
data/licenses.json
|
13
.tern-project
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"ecmaVersion": 6,
|
||||
"libs": [
|
||||
"browser"
|
||||
],
|
||||
"loadEagerly": [
|
||||
"js/app.js"
|
||||
],
|
||||
"plugins": {
|
||||
"modules": {},
|
||||
"es_modules": {}
|
||||
}
|
||||
}
|
123
ADD_OBJECT.md
Normal file
@ -0,0 +1,123 @@
|
||||
```javascript
|
||||
actions.sketcher.addObject(objectData);
|
||||
```
|
||||
|
||||
**objectData**
|
||||
|
||||
Only type is required
|
||||
|
||||
```javascript
|
||||
const objectData = {
|
||||
type: String:isRequired,
|
||||
height: Number,
|
||||
transform: CAL.Matrix,
|
||||
z: Number,
|
||||
sculpt: [
|
||||
...{ pos: Number, scale: Number }
|
||||
],
|
||||
twist: Number,
|
||||
color: HexNumber,
|
||||
space: 'String',
|
||||
fill : Bool
|
||||
}
|
||||
```
|
||||
|
||||
**Defining Sculpt**
|
||||
|
||||
Sculp has to have at least 2 entries. The first entry has to have a position of `0.0`, and the last one has to have a position of `1.0`. There can be added additional sculpt steps with increasing positions.
|
||||
|
||||
The scale property defines the scale factor on that position
|
||||
|
||||
```javascript
|
||||
sculpt = [
|
||||
{ pos: 0.0, scale: 1.0 },
|
||||
{ pos: 1.0, scale: 1.0 }
|
||||
]
|
||||
```
|
||||
|
||||
**Defining a Matrix**
|
||||
|
||||
```javascript
|
||||
new CAL.Matrix({
|
||||
x: Number,
|
||||
y: Number
|
||||
sx: Number,
|
||||
sy: Number,
|
||||
rotation: Number
|
||||
})
|
||||
```
|
||||
|
||||
**objectData.type = CIRCLE**
|
||||
|
||||
```javascript
|
||||
{
|
||||
...objectData,
|
||||
type: 'CIRCLE'
|
||||
circle: {
|
||||
radius: Number,
|
||||
segment: Number(rad)
|
||||
}
|
||||
}
|
||||
|
||||
actions.sketcher.addObject({ type: 'CIRCLE', circle: { radius: 10, segment: Math.PI * 2 } });
|
||||
```
|
||||
|
||||
**objectData.type = RECT**
|
||||
|
||||
```javascript
|
||||
{
|
||||
...objectData,
|
||||
type: 'RECT'
|
||||
rectSize: CAL.Vector
|
||||
}
|
||||
|
||||
actions.sketcher.addObject({ type: 'CIRCLE', rectSize: new CAL.Vector(10, 10) });
|
||||
```
|
||||
|
||||
**objectData.type = TRIANGLE**
|
||||
|
||||
```javascript
|
||||
{
|
||||
...objectData,
|
||||
type: 'TRIANGLE'
|
||||
triangleSize: CAL.Vector
|
||||
}
|
||||
|
||||
actions.sketcher.addObject({ type: 'CIRCLE', rectSize: new CAL.Vector(10, 10) });
|
||||
```
|
||||
|
||||
**objectData.type = STAR**
|
||||
|
||||
```javascript
|
||||
{
|
||||
...objectData,
|
||||
type: 'STAR'
|
||||
star: { innerRadius: Number, outerRadius: Number, rays: Int }
|
||||
}
|
||||
|
||||
actions.sketcher.addObject({ type: 'STAR', star: { innerRadius: 10, outerRadius: 15, rays: 5 } });
|
||||
```
|
||||
|
||||
**objectData.type = TEXT**
|
||||
|
||||
```javascript
|
||||
{
|
||||
...objectData,
|
||||
type: 'TEXT'
|
||||
text: { text: String, family: String, weight: String, style: String }
|
||||
}
|
||||
|
||||
actions.sketcher.addObject({ type: 'TEXT', text: { text: 'ABC', family: 'Arial', weight: 'normal', style: 'normal' } });
|
||||
```
|
||||
|
||||
**objectData.type = FREE_HAND**
|
||||
|
||||
```javascript
|
||||
{
|
||||
...objectData,
|
||||
type: 'FREE_HAND'
|
||||
points: [...CAL.Vector]
|
||||
}
|
||||
|
||||
actions.sketcher.addObject({ type: 'FREE_HAND', points: [new CAL.Vector(0, 0), new CAL.Vector(100, 0), new CAL.Vector(100, 100)] });
|
||||
```
|
1170
CHANGELOG.md
Normal file
22
LICENSE.md
Normal file
@ -0,0 +1,22 @@
|
||||
Copyright (c) 2021 Doodle3D
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
75
README.md
@ -1,2 +1,73 @@
|
||||
# Doodle3D-Transform
|
||||
Official repository of the Doodle3D Transform web-app
|
||||
# Doodle3D-App
|
||||
Doodle3D Transform is a free and open-source web-app that makes designing in 3D easy and fun! Created with love by Casper, Peter, Rick, Nico, Jeroen, Simon, Donna and Arne. With the support of 1,626 Kickstarter backers.
|
||||
|
||||
As of 2021-05-26 Doodle3D Transform is distributed under the MIT License. This gives everyone the freedoms to use Doodle3D Transform in any context: commercial or non-commercial, public or private, open or closed source.
|
||||
|
||||
![Screenshot](screenshot.png)
|
||||
|
||||
## Prerequisites
|
||||
- Install [Node.js](https://nodejs.org/en/) v6 LTS (v8 is not supported yet).
|
||||
- Update NPM to at least v5.
|
||||
`$ npm install npm@latest -g`
|
||||
|
||||
## Install
|
||||
- Clone our repo.
|
||||
`$ git clone git@github.com:Doodle3D/Doodle3D-App.git`
|
||||
- Go into it.
|
||||
`$ cd Doodle3D-App`
|
||||
- Install all dependencies
|
||||
`$ npm install`
|
||||
- Setup environment variables, see below.
|
||||
|
||||
## Run
|
||||
Start the server, you can visit it at http://localhost:8080
|
||||
```
|
||||
npm start
|
||||
```
|
||||
|
||||
## Create distribution
|
||||
To create a optimized version of our app that's ready for deployment run:
|
||||
```
|
||||
npm run dist
|
||||
```
|
||||
To test this distribution start our app in production mode by running:
|
||||
```
|
||||
npm run start-production
|
||||
```
|
||||
|
||||
## Build & Run as App
|
||||
[Cordova](https://cordova.apache.org/) is used to build the app. Before building with Cordova, it needs to be installed.
|
||||
```
|
||||
npm run cordova:install
|
||||
```
|
||||
- **Run iOS** (mac only)
|
||||
Build app and deploy to ios device:
|
||||
`npm run ios`
|
||||
- **Emulate iOS** (mac only)
|
||||
Build app and start iOS emulator:
|
||||
`npm run ios-emulate [-- -{platform}]`
|
||||
- **Run Android**
|
||||
Build app and deploy on device or emulator:
|
||||
`npm run android`
|
||||
- **Run Windows** (win only)
|
||||
Build app and deploy on device:
|
||||
`npm run windows`
|
||||
|
||||
## Import / Export dev tool
|
||||
In `Developer tools` > `Console`:
|
||||
- `files.exportFile(name)`
|
||||
- `name`: name of file to export
|
||||
|
||||
```
|
||||
files.exportFile('star')
|
||||
```
|
||||
- `files.importFile(data, name)`
|
||||
- `data`: result of `exportFile` between single quotes
|
||||
- `name`: override name of file (optionally)
|
||||
|
||||
```
|
||||
files.importFile('{"data":[{"height":40,"transform":{"metadata":{"library":"CAL","type":"Matrix"},"matrix":[1,0,-7.73286467486821,0,1,18.980667838312854]},"sculpt":[1,1,1],"cut":[true,true],"twist":0,"type":"STAR","star":{"rays":5,"innerRadius":25,"outerRadius":53}}],"_attachments":{"img":{"content_type":"image/jpeg","digest":"md5-NOT2LxdCerpIrG/7vEczDw==","length":7442,"stub":true}},"_id":"star","_rev":"3-6ea2bc9e62628a25078d9d8d90aff151"}','imported star')
|
||||
```
|
||||
- `files.loadAllNames()`
|
||||
Get a list of the names all saves sketches
|
||||
|
||||
|
BIN
favicon.ico
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
favicon.png
Normal file
After Width: | Height: | Size: 814 B |
BIN
favicon_alt.ico
Normal file
After Width: | Height: | Size: 97 KiB |
BIN
img/apple-touch-icon-144x144-precomposed.png
Normal file
After Width: | Height: | Size: 8.3 KiB |
BIN
img/btnMollie.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
img/btnPayPal.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
img/heart.jpg
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
img/menu/btnExport.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
img/menu/btnExportSmall.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
img/menu/btnHelp.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
img/menu/btnHelpSmall.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
img/menu/btnLove.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
img/menu/btnLoveSmall.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
img/menu/btnMenu.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
img/menu/btnMenuSmall.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
img/menu/btnNew.png
Normal file
After Width: | Height: | Size: 6.4 KiB |
BIN
img/menu/btnNewSmall.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
img/menu/btnOpen.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
BIN
img/menu/btnOpenSmall.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
img/menu/btnSave.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
img/menu/btnSaveSmall.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
img/paypal-donate-button-doodle3d-QR-code.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
46
licenses-to-json.js
Normal file
@ -0,0 +1,46 @@
|
||||
/* eslint no-console: 0 */
|
||||
|
||||
const checker = require('license-checker');
|
||||
const _ = require('lodash');
|
||||
require('core-js/fn/object/entries');
|
||||
|
||||
const [projectPath = __dirname] = process.argv.slice(2);
|
||||
if (!projectPath) {
|
||||
console.log('No project path specified');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const corrections = {
|
||||
'eventdispatcher.js': 'MIT',
|
||||
'pouchdb-collections': 'Apache 2',
|
||||
bufferjs: 'MIT'
|
||||
};
|
||||
|
||||
checker.init({
|
||||
start: projectPath,
|
||||
production: true,
|
||||
development: false,
|
||||
customFormat: {
|
||||
name: ''
|
||||
}
|
||||
}, (err, json) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
process.exit(1);
|
||||
} else {
|
||||
// the checker returns an object instead of an array, so use Object.entries to iterate
|
||||
let dependencies = Object.entries(json).map(dep => {
|
||||
const { name, publisher, licenses } = dep[1];
|
||||
const result = {
|
||||
name,
|
||||
publisher,
|
||||
licenses: corrections[name] || licenses,
|
||||
url: `https://www.npmjs.com/package/${name}`
|
||||
};
|
||||
return result;
|
||||
});
|
||||
dependencies = _.uniqBy(dependencies, dep => dep.name);
|
||||
|
||||
console.log(JSON.stringify(dependencies));
|
||||
}
|
||||
});
|
33068
package-lock.json
generated
Normal file
130
package.json
Normal file
@ -0,0 +1,130 @@
|
||||
{
|
||||
"name": "doodle3d-app",
|
||||
"version": "0.23.0",
|
||||
"main": "src/js/index.js",
|
||||
"license": "MIT",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"postinstall": "npm run dist:prepare",
|
||||
"start": "webpack-dev-server -w",
|
||||
"start-production": "NODE_ENV=production npm start",
|
||||
"lint": "eslint src/js",
|
||||
"dist:prepare": "npm run dist:licenses && mkdir -p dist && rimraf dist/*",
|
||||
"dist:licenses": "node licenses-to-json.js . > data/licenses.json",
|
||||
"dist": "npm run dist:prepare && NODE_ENV=production webpack -p",
|
||||
"analyze": "NODE_ENV=production ANALYZE_BUNDLE=true webpack -p"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"ios-deploy": "^1.8.4",
|
||||
"ios-sim": "^5.0.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@doodle3d/cal": "0.0.8",
|
||||
"@doodle3d/doodle3d-core": "github:Doodle3D/Doodle3D-Core",
|
||||
"@doodle3d/doodle3d-slicer": "github:Doodle3D/Doodle3D-Slicer",
|
||||
"@doodle3d/redux-prompt": "^1.0.2",
|
||||
"autoprefixer": "^5.2.0",
|
||||
"babel-core": "^6.25.0",
|
||||
"babel-eslint": "^5.0.0-beta6",
|
||||
"babel-loader": "^7.1.1",
|
||||
"babel-plugin-lodash": "^3.2.11",
|
||||
"babel-plugin-ramda": "^1.2.0",
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||
"babel-plugin-transform-es2015-classes": "^6.24.1",
|
||||
"babel-plugin-transform-imports": "^1.4.0",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.23.0",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-polyfill": "^6.23.0",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"babel-preset-latest": "^6.24.1",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"bluebird": "^3.3.4",
|
||||
"body-parser": "^1.15.2",
|
||||
"bowser": "^1.5.0",
|
||||
"cli": "^1.0.1",
|
||||
"compression": "^1.6.2",
|
||||
"connect-redis": "^3.3.3",
|
||||
"cors": "^2.8.1",
|
||||
"css-loader": "^0.28.7",
|
||||
"css-polyfills": "0.0.16",
|
||||
"debug": "^2.2.0",
|
||||
"eslint": "^1.10.3",
|
||||
"eslint-config-airbnb": "^3.1.0",
|
||||
"eslint-plugin-react": "^3.15.0",
|
||||
"executive": "^1.5.11",
|
||||
"fastclick": "^1.0.6",
|
||||
"file-loader": "^0.11.2",
|
||||
"file-saver": "^1.3.3",
|
||||
"form-data": "^2.1.1",
|
||||
"html-webpack-plugin": "^2.30.1",
|
||||
"html-webpack-template": "^6.1.0",
|
||||
"image-webpack-loader": "^3.4.2",
|
||||
"imports-loader": "^0.7.1",
|
||||
"json-loader": "^0.5.4",
|
||||
"jss": "^9.3.3",
|
||||
"jss-preset-default": "^4.0.1",
|
||||
"jszip": "^3.1.3",
|
||||
"keycode": "^2.1.8",
|
||||
"license-checker": "^13.0.2",
|
||||
"markdown-to-jsx": "^5.4.0",
|
||||
"marked": "^0.3.5",
|
||||
"material-ui": "^0.19.0",
|
||||
"material-ui-pagination": "^1.1.6",
|
||||
"mime-types": "^2.1.12",
|
||||
"morgan": "^1.7.0",
|
||||
"node-fetch": "^1.6.3",
|
||||
"normalize-jss": "^4.0.0",
|
||||
"postinstall": "^0.7.0",
|
||||
"pouchdb": "^7.2.2",
|
||||
"pouchdb-seed-design": "^0.3.0",
|
||||
"prop-types": "^15.6.0",
|
||||
"raw-loader": "^0.5.1",
|
||||
"react": "^16.1.0",
|
||||
"react-addons-css-transition-group": "^15.6.2",
|
||||
"react-addons-update": "^15.6.2",
|
||||
"react-dom": "^16.1.0",
|
||||
"react-ga": "^2.1.2",
|
||||
"react-hot-loader": "^3.0.0-beta.7",
|
||||
"react-jss": "^7.0.2",
|
||||
"react-loader": "^2.4.0",
|
||||
"react-markdown": "^2.4.2",
|
||||
"react-newline-to-break": "^1.0.6",
|
||||
"react-notification-system-redux": "^1.2.0",
|
||||
"react-redux": "^4.4.8",
|
||||
"react-router": "^3.2.0",
|
||||
"react-router-redux": "^4.0.8",
|
||||
"react-sortable": "^1.3.2",
|
||||
"redux": "^3.7.1",
|
||||
"redux-action-wrapper": "^1.0.1",
|
||||
"redux-auth-wrapper": "^2.0.2",
|
||||
"redux-form": "^7.2.0",
|
||||
"redux-form-material-ui": "^4.0.1",
|
||||
"redux-logger": "^2.3.1",
|
||||
"redux-promise-action": "github:casperlamboo/redux-promise-action",
|
||||
"redux-promise-middleware": "^5.0.0",
|
||||
"redux-thunk": "^2.2.0",
|
||||
"sanitize-filename": "^1.6.1",
|
||||
"semver": "^5.3.0",
|
||||
"serve-favicon": "^2.3.0",
|
||||
"serve-static": "^1.11.1",
|
||||
"shapeways": "^1.0.2",
|
||||
"sharp": "^0.28.2",
|
||||
"shortid": "^2.2.6",
|
||||
"sofa-model": "^0.2.0",
|
||||
"spin.js": "^2.3.2",
|
||||
"style-loader": "^0.18.2",
|
||||
"three": "^0.88.0",
|
||||
"url-parse": "^1.1.9",
|
||||
"url-polyfill": "^1.0.11",
|
||||
"valid-url": "^1.0.9",
|
||||
"webpack": "^3.4.1",
|
||||
"webpack-bundle-analyzer": "^2.8.3",
|
||||
"webpack-dev-middleware": "^2.0.6",
|
||||
"webpack-dev-server": "^2.5.1",
|
||||
"webpack-hot-middleware": "^2.18.2",
|
||||
"whatwg-fetch": "^1.0.0",
|
||||
"worker-loader": "^0.8.1",
|
||||
"yml-loader": "^2.1.0"
|
||||
}
|
||||
}
|
BIN
screenshot.png
Normal file
After Width: | Height: | Size: 195 KiB |
30
src/js/Root.js
Normal file
@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { Router, Route, IndexRedirect } from 'react-router';
|
||||
import App from './containers/App.js';
|
||||
import MyDoodles from './containers/Pages/MyDoodles.js';
|
||||
import Save from './containers/Pages/Save.js';
|
||||
import About from './containers/Pages/About.js';
|
||||
import Licenses from './containers/Pages/Licenses.js';
|
||||
import Help from './containers/Pages/Help.js';
|
||||
import Donate from './containers/Pages/Donate.js';
|
||||
import Slicer from './containers/Pages/Slicer.js';
|
||||
import ReleaseNotes from './containers/Pages/ReleaseNotes.js';
|
||||
import AddImage from './containers/Pages/AddImage.js';
|
||||
import * as envs from 'src/js/constants/envs.js';
|
||||
import { browserHistory, hashHistory } from 'react-router';
|
||||
|
||||
export default () => (
|
||||
<Router history={envs.platform === 'ios-app' ? hashHistory : browserHistory}>
|
||||
<Route path="/" component={App}>
|
||||
<Route path="my-doodles" component={MyDoodles}/>
|
||||
<Route path="import" component={AddImage} />
|
||||
<Route path="save" component={Save} />
|
||||
<Route path="settings" component={About} />
|
||||
<Route path="licenses" component={Licenses} />
|
||||
<Route path="releasenotes" component={ReleaseNotes} />
|
||||
<Route path="help" component={Help} />
|
||||
<Route path="donate" component={Donate} />
|
||||
<Route path="slicer" component={Slicer} />
|
||||
</Route>
|
||||
</Router>
|
||||
);
|
9
src/js/actions/blockingSpinner.js
Normal file
@ -0,0 +1,9 @@
|
||||
export const START = 'START_BLOCKING_SPINNER';
|
||||
export const STOP = 'STOP_BLOCKING_SPINNER';
|
||||
|
||||
export function start() {
|
||||
return { type: START };
|
||||
}
|
||||
export function stop() {
|
||||
return { type: STOP };
|
||||
}
|
262
src/js/actions/files.js
Normal file
@ -0,0 +1,262 @@
|
||||
import { createUniqueName, sketchDataToDoc, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT } from 'src/js/utils/saveUtils.js';
|
||||
import sketchDataToJSON from '@doodle3d/doodle3d-core/lib/shape/sketchDataToJSON';
|
||||
import { JSONToBlob } from '@doodle3d/doodle3d-core/lib/utils/binaryUtils';
|
||||
import JSZip from 'jszip';
|
||||
import * as actions from './index.js';
|
||||
import { currentFileName, } from 'src/js/reducers/index.js';
|
||||
import { notification } from './index.js';
|
||||
import * as envs from 'src/js/constants/envs.js';
|
||||
import { saveAs } from 'file-saver';
|
||||
import seed from 'pouchdb-seed-design';
|
||||
import { createPromiseAction } from 'redux-promise-action';
|
||||
import PouchDB from 'pouchdb';
|
||||
import * as notificationActions from 'react-notification-system-redux';
|
||||
import JSONToSketchData from '@doodle3d/doodle3d-core/lib/shape/JSONToSketchData';
|
||||
import { VERSION } from '@doodle3d/doodle3d-core/lib/constants/general.js';
|
||||
import createSceneData from '@doodle3d/doodle3d-core/lib/d3/createSceneData';
|
||||
import { generateThumb } from '@doodle3d/doodle3d-core/lib/utils/generateThumb.js';
|
||||
import { blobToJSON } from '@doodle3d/doodle3d-core/lib/utils/binaryUtils.js';
|
||||
|
||||
export const SKETCH_EXPORT = 'SKETCH_EXPORT';
|
||||
export const ALL_SKETCHES_EXPORT = 'ALL_SKETCHES_EXPORT';
|
||||
|
||||
export const OPEN_FILE = 'OPEN_FILE';
|
||||
export const openFile = (data, name, id) => {
|
||||
return (dispatch) => {
|
||||
dispatch({ type: OPEN_FILE, name, data, id });
|
||||
dispatch(actions.sketcher.openSketch({ data }));
|
||||
};
|
||||
};
|
||||
|
||||
export const saveFile = (name) => {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
const currentFileID = state.files.id;
|
||||
const savedFile = currentFileID !== null; // saved files always have an id
|
||||
const changedName = currentFileName(state) !== name;
|
||||
|
||||
const overrideId = (!savedFile || changedName) ? null : currentFileID;
|
||||
|
||||
const sketcherState = getState().sketcher.present;
|
||||
const doc = await sketchDataToDoc(name, sketcherState);
|
||||
|
||||
return dispatch(actions.files.saveDoodle(name, doc, overrideId));
|
||||
};
|
||||
};
|
||||
|
||||
export const downloadAllSketches = () => {
|
||||
// this function can be alot more optimized
|
||||
// pouch db actually returns attachments as blobs
|
||||
// when setting sketch to true this attachment is automatically converted to sketch data
|
||||
// in the for loop the sketch data is converted back to blob
|
||||
return async (dispatch, getState) => {
|
||||
const names = [];
|
||||
const zip = new JSZip();
|
||||
|
||||
const { value: files } = await dispatch(actions.files.loadAll({
|
||||
include_docs: true,
|
||||
attachments: true,
|
||||
binary: true
|
||||
}));
|
||||
for (const { name, _attachments: { sketch: { data } }, updatedOn } of files) {
|
||||
const uniqueName = createUniqueName(name || 'Doodle', names);
|
||||
|
||||
zip.file(`${uniqueName}.doodle3d`, data, { binary: true, date: new Date(updatedOn) });
|
||||
names.push(uniqueName);
|
||||
}
|
||||
const dataBlob = await zip.generateAsync({ type: 'blob' });
|
||||
return dispatch({
|
||||
type: ALL_SKETCHES_EXPORT,
|
||||
payload: dispatch(saveAs(dataBlob, 'My Doodles.zip'))
|
||||
}).catch(error => {
|
||||
dispatch(notification.error({ title: 'Saving doodle failed' }));
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const openFileSelector = () => {
|
||||
return async (dispatch, getState) => {
|
||||
const files = await window.showOpenFilePicker({ multiple: true });
|
||||
for (let file of files) {
|
||||
await loadFile(dispatch, file.name, () => file.getFile());
|
||||
}
|
||||
};
|
||||
|
||||
async function loadFile(dispatch, fileName, getData) {
|
||||
switch (fileName.match(/\.[0-9a-z]+$/i)[0].toUpperCase()) {
|
||||
case ".ZIP":
|
||||
let zip = await JSZip.loadAsync(await getData());
|
||||
|
||||
for (let fileName in zip.files) {
|
||||
await loadFile(dispatch, fileName, () => zip.file(fileName).async("blob"));
|
||||
}
|
||||
|
||||
break;
|
||||
case ".DOODLE3D":
|
||||
const sketchData = await getData();
|
||||
|
||||
const sketcherState = await createSceneData(await JSONToSketchData(await blobToJSON(sketchData)));
|
||||
const imgBlob = await generateThumb(sketcherState, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, 'blob');
|
||||
const sketchBlob = new Blob([sketchData], { type: 'application/json' });
|
||||
|
||||
const doc = {
|
||||
name: fileName,
|
||||
appVersion: VERSION,
|
||||
_attachments: {
|
||||
img: { content_type: imgBlob.type, data: imgBlob },
|
||||
sketch: { content_type: sketchBlob.type, data: sketchBlob }
|
||||
}
|
||||
};
|
||||
|
||||
await dispatch(actions.files.saveDoodle(fileName, doc, null));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const downloadSketch = (doc) => {
|
||||
return (dispatch, getState) => {
|
||||
console.log(doc);
|
||||
const { name, _attachments: { sketch: { data } }, updatedOn } = doc;
|
||||
|
||||
const fileName = `${name || 'Doodle'}.doodle3d`;
|
||||
|
||||
return dispatch({
|
||||
type: SKETCH_EXPORT,
|
||||
payload: dispatch(saveAs(data, fileName))
|
||||
}).catch(error => {
|
||||
dispatch(notification.error({ title: 'Downloading doodle failed' }));
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
export const downloadCurrentSketch = (name) => {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
const fileName = `${name || 'Doodle'}.doodle3d`;
|
||||
|
||||
const json = sketchDataToJSON(state.sketcher.present);
|
||||
const blob = JSONToBlob(json);
|
||||
return dispatch({
|
||||
type: SKETCH_EXPORT,
|
||||
payload: dispatch(saveAs(blob, fileName))
|
||||
}).catch(error => {
|
||||
dispatch(notification.error({ title: 'Downloading doodle failed' }));
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
let db;
|
||||
let seedingDesignDoc;
|
||||
|
||||
export function init() {
|
||||
return async (dispatch, getState) => {
|
||||
db = new PouchDB('doodle3d-files');
|
||||
|
||||
seedingDesignDoc = seed(db, {
|
||||
sketches: {
|
||||
views: {
|
||||
updatedOn: {
|
||||
map: function (doc) {
|
||||
if (doc.updatedOn) {
|
||||
emit(doc.updatedOn);
|
||||
}
|
||||
}.toString()
|
||||
},
|
||||
name: {
|
||||
map: function (doc) {
|
||||
if (doc.name) {
|
||||
emit(doc.name);
|
||||
}
|
||||
}.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const LOAD_GALLERY = 'LOAD_GALLERY';
|
||||
export const loadGallery = createPromiseAction(async (dispatch, getState, page = 0, pageLength = 10, type = 'updatedOn', desc = true) => {
|
||||
console.log("db", db);
|
||||
console.log("seedingDesignDoc", seedingDesignDoc);
|
||||
await seedingDesignDoc;
|
||||
return db.query(`sketches/${type}`, {
|
||||
include_docs: true,
|
||||
attachments: true,
|
||||
binary: true,
|
||||
descending: desc,
|
||||
limit: pageLength,
|
||||
skip: page * pageLength
|
||||
});
|
||||
}, LOAD_GALLERY);
|
||||
|
||||
|
||||
export const REMOVE_DOODLE = 'REMOVE_DOODLE';
|
||||
export const removeDoodle = createPromiseAction(async (dispatch, getState, id) => {
|
||||
const doc = await db.get(id);
|
||||
const { _id, _rev } = doc;
|
||||
await db.put({ _id, _rev, _deleted: true });
|
||||
return doc;
|
||||
}, REMOVE_DOODLE, {
|
||||
onSuccess: ({ name }) => notificationActions.success({ position: 'tc', title: `successfully deleted doodle: ${name}` }),
|
||||
onError: () => notificationActions.error({ position: 'tc', title: `failed to delete doodle` })
|
||||
});
|
||||
|
||||
export const SAVE_DOODLE = 'SAVE_DOODLE';
|
||||
export const saveDoodle = createPromiseAction(async (dispatch, getState, name, doc, overrideId) => {
|
||||
doc.updatedOn = Date.now();
|
||||
|
||||
if (overrideId) {
|
||||
const oldDoc = await db.get(overrideId);
|
||||
doc = {
|
||||
...doc,
|
||||
_id: oldDoc._id,
|
||||
_rev: oldDoc._rev
|
||||
};
|
||||
} else {
|
||||
doc.createdOn = Date.now();
|
||||
}
|
||||
|
||||
const { id, ok } = overrideId ? await db.put(doc) : await db.post(doc);
|
||||
if (!ok) new Error('Error updating doc');
|
||||
return { id, name };
|
||||
}, SAVE_DOODLE, {
|
||||
onSuccess: ({ name }) => notificationActions.success({ position: 'tc', title: `successfully saved doodle: ${name}` }),
|
||||
onError: () => notificationActions.error({ position: 'tc', title: `failed to save doodle` })
|
||||
});
|
||||
|
||||
export const LOAD_ALL = 'LOAD_ALL';
|
||||
export const loadAll = createPromiseAction(async (dispatch, getState, options = {}) => {
|
||||
const { rows } = await db.query('sketches/name', options);
|
||||
return rows.map(({ doc }) => doc);
|
||||
}, LOAD_ALL);
|
||||
|
||||
export const LOAD_ALL_NAMES = 'LOAD_ALL_NAMES';
|
||||
export const loadAllNames = createPromiseAction(async () => {
|
||||
const { rows } = await db.query('sketches/name', { include_docs: true });
|
||||
return rows.map(({ doc }) => doc.name);
|
||||
}, LOAD_ALL_NAMES);
|
||||
|
||||
export const LOAD_NUM_FILES = 'LOAD_NUM_FILES';
|
||||
export const loadNumFiles = createPromiseAction(async () => {
|
||||
const { rows } = await db.query('sketches/name');
|
||||
return rows.length;
|
||||
}, LOAD_NUM_FILES);
|
||||
|
||||
export const OPEN_CONTEXT_MENU = 'OPEN_CONTEXT_MENU';
|
||||
export function openContextMenu(id) {
|
||||
return { type: OPEN_CONTEXT_MENU, id };
|
||||
}
|
||||
|
||||
export const CLOSE_CONTEXT_MENU = 'CLOSE_CONTEXT_MENU';
|
||||
export function closeContextMenu() {
|
||||
return { type: CLOSE_CONTEXT_MENU };
|
||||
}
|
40
src/js/actions/hotkeys.js
Normal file
@ -0,0 +1,40 @@
|
||||
import * as actions from './index.js';
|
||||
import bowser from 'bowser';
|
||||
import keycode from 'keycode';
|
||||
// import createDebug from 'debug';
|
||||
// const debug = createDebug('d3d:actions:hotkeys');
|
||||
|
||||
export const keyPress = (event, pathname) => {
|
||||
return (dispatch) => {
|
||||
const { metaKey, ctrlKey } = event;
|
||||
const key = keycode(event);
|
||||
|
||||
const commandKey = bowser.mac ? metaKey : ctrlKey;
|
||||
|
||||
// ignore key events from input fields by checking event target
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Event/target
|
||||
const targetTag = event.target.tagName.toLowerCase();
|
||||
if (targetTag === 'input' || targetTag === 'textarea') return;
|
||||
|
||||
if (pathname === '/') {
|
||||
switch (key) {
|
||||
case 's':
|
||||
if (commandKey) {
|
||||
event.preventDefault(); // pevent default browser saving behaviour
|
||||
dispatch(actions.router.push('/save'));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'o':
|
||||
if (commandKey) {
|
||||
event.preventDefault(); // pevent default browser opening behaviour
|
||||
dispatch(actions.router.push('/my-doodles'));
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
92
src/js/actions/index.js
Normal file
@ -0,0 +1,92 @@
|
||||
// sketcher
|
||||
import * as sketcher from '@doodle3d/doodle3d-core/lib/actions';
|
||||
export { sketcher };
|
||||
import * as config from 'src/js/services/config.js';
|
||||
|
||||
// libs
|
||||
import { saveAs } from 'file-saver';
|
||||
import { routerActions as router } from 'react-router-redux';
|
||||
import * as notificationActions from 'react-notification-system-redux';
|
||||
import * as prompt from 'redux-prompt/actions';
|
||||
export const notification = {
|
||||
show: ({ position = 'tc', ...args }) => notificationActions.show({ position, ...args }),
|
||||
success: ({ position = 'tc', ...args }) => notificationActions.success({ position, ...args }),
|
||||
warning: ({ position = 'tc', ...args }) => notificationActions.warning({ position, ...args }),
|
||||
error: ({ position = 'tc', ...args }) => notificationActions.error({ position, ...args }),
|
||||
info: ({ position = 'tc', ...args }) => notificationActions.info({ position, ...args }),
|
||||
hide: notificationActions.hide,
|
||||
removeAll: notificationActions.removeAll
|
||||
};
|
||||
export { router, prompt };
|
||||
|
||||
import * as localStore from './localStore.js';
|
||||
import * as files from './files.js';
|
||||
import * as hotkeys from './hotkeys.js';
|
||||
import * as blockingSpinner from './blockingSpinner.js';
|
||||
export { localStore, files, hotkeys, blockingSpinner };
|
||||
|
||||
import { currentFileName } from 'src/js/reducers/index.js';
|
||||
import { createFile } from '@doodle3d/doodle3d-core/lib/utils/exportUtils.js';
|
||||
|
||||
export const STL_EXPORT = 'STL_EXPORT';
|
||||
export const OBJ_EXPORT = 'OBJ_EXPORT';
|
||||
export const JSON_EXPORT = 'JSON_EXPORT';
|
||||
export const FACEBOOK_EXPORT = 'FACEBOOK_EXPORT';
|
||||
export const TWITTER_EXPORT = 'TWITTER_EXPORT';
|
||||
export const MATERIALIZE_EXPORT = 'MATERIALIZE_EXPORT';
|
||||
export const YOUMAGINE_EXPORT = 'YOUMAGINE_EXPORT';
|
||||
export const THINGIVERSE_EXPORT = 'THINGIVERSE_EXPORT';
|
||||
|
||||
export function downloadStl() {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
const name = `${currentFileName(state) || 'Doodle'}.stl`;
|
||||
const { exportLineWidth: lineWidth } = config.get();
|
||||
|
||||
return dispatch({
|
||||
type: STL_EXPORT,
|
||||
payload: createFile(state.sketcher.present, 'stl-blob', { lineWidth })
|
||||
.then(blob => dispatch(saveAs(blob, name)))
|
||||
.catch(() => {
|
||||
// dispatch(notification.error({ title: 'Downloading stl file failed' }));
|
||||
})
|
||||
});
|
||||
};
|
||||
}
|
||||
export function downloadJSON() {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
const name = `${currentFileName(state) || 'Doodle'}.json`;
|
||||
|
||||
return dispatch({
|
||||
type: JSON_EXPORT,
|
||||
payload: createFile(state.sketcher.present, 'json-blob')
|
||||
.then(blob => dispatch(saveAs(blob, name)))
|
||||
.catch(() => {
|
||||
// dispatch(notification.error({ title: 'Downloading json file failed' }));
|
||||
})
|
||||
});
|
||||
};
|
||||
}
|
||||
export function downloadObj() {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
const name = `${currentFileName(state) || 'Doodle'}.zip`;
|
||||
|
||||
return dispatch({
|
||||
type: OBJ_EXPORT,
|
||||
payload: createFile(state.sketcher.present, 'obj-blob')
|
||||
.then(blob => dispatch(saveAs(blob, name)))
|
||||
.catch(() => {
|
||||
// dispatch(notification.error({ title: 'Downloading obj file failed' }));
|
||||
})
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
import bowser from 'bowser';
|
||||
import { platform } from 'src/js/constants/envs.js';
|
||||
import sanitize from 'sanitize-filename';
|
15
src/js/actions/localStore.js
Normal file
@ -0,0 +1,15 @@
|
||||
import * as localStore from 'src/js/services/localStore.js';
|
||||
|
||||
export const LOCAL_STORE_READ = 'LOCAL_STORE_READ';
|
||||
export const LOCAL_STORE_LEAVE_TRACE = 'LOCAL_STORE_LEAVE_TRACE';
|
||||
export const ADD_PAYMENT = 'ADD_PAYMENT';
|
||||
|
||||
export function read() {
|
||||
return { type: LOCAL_STORE_READ, data: localStore.read() };
|
||||
}
|
||||
export function leaveTrace() {
|
||||
return { type: LOCAL_STORE_LEAVE_TRACE };
|
||||
}
|
||||
export function addPayment(payments) {
|
||||
return { type: ADD_PAYMENT, payments };
|
||||
}
|
40
src/js/actions/print.js
Normal file
@ -0,0 +1,40 @@
|
||||
import { createQuery, awsUpload } from 'src/js/utils/utils.js';
|
||||
import * as actions from './index.js';
|
||||
import sketchDataToJSON from '@doodle3d/doodle3d-core/lib/shape/sketchDataToJSON';
|
||||
import { JSONToBlob } from '@doodle3d/doodle3d-core/lib/utils/binaryUtils';
|
||||
import { isSketchEmpty, currentFileName } from 'src/js/reducers/index.js';
|
||||
import { printUrl as API } from 'src/js/constants/envs.js';
|
||||
import { createPromiseAction } from 'redux-promise-action';
|
||||
|
||||
export const UPLOAD = 'PRINT_UPLOAD';
|
||||
export const upload = createPromiseAction(async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
if (isSketchEmpty(state)) throw new Error('Sketch is empty');
|
||||
|
||||
try {
|
||||
dispatch(actions.blockingSpinner.start());
|
||||
|
||||
const name = currentFileName(state) || 'Doodle';
|
||||
const json = sketchDataToJSON(state.sketcher.present);
|
||||
const blob = JSONToBlob(json);
|
||||
const file = await awsUpload(blob, `${name}.doodle3d`);
|
||||
const url = `${API}/?${createQuery({ file, name })}`;
|
||||
|
||||
const popupWindow = window.open(url, '_blank');
|
||||
if (!popupWindow) {
|
||||
dispatch(actions.prompt.open({
|
||||
title: `Print file`,
|
||||
message: 'Open in Doodle3D Slicer',
|
||||
link: url,
|
||||
submitText: 'Open',
|
||||
form: []
|
||||
}));
|
||||
}
|
||||
} finally {
|
||||
dispatch(actions.blockingSpinner.stop());
|
||||
}
|
||||
}, UPLOAD, {
|
||||
onError: (error) => actions.notification.error({
|
||||
title: `Failed to upload Doodle3D Slicer: ${error.message}`
|
||||
})
|
||||
});
|
34
src/js/components/BlockingSpinner.js
Normal file
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import injectSheet from 'react-jss';
|
||||
import CircularProgress from 'material-ui/CircularProgress';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
position: 'absolute',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 997
|
||||
}
|
||||
};
|
||||
|
||||
const BlockingSpinner = ({ classes, active }) => (active ? (
|
||||
<div className={classes.container}>
|
||||
<CircularProgress size={60} thickness={5} color="white"/>
|
||||
</div>
|
||||
) : null);
|
||||
BlockingSpinner.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
active: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default connect(state => ({
|
||||
active: state.blockingSpinner.active
|
||||
}))(injectSheet(styles)(BlockingSpinner));
|
20
src/js/components/Line.js
Normal file
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import injectSheet from 'react-jss';
|
||||
|
||||
const styles = {
|
||||
line: {
|
||||
display: 'block',
|
||||
borderColor: 'rgb(189, 189, 189)',
|
||||
marginLeft: '-6px',
|
||||
borderTopStyle: 'solid',
|
||||
borderTopWidth: '1px'
|
||||
}
|
||||
};
|
||||
|
||||
const Line = ({ classes }) => <span className={classes.line} />;
|
||||
Line.propTypes = {
|
||||
classes: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default injectSheet(styles)(Line);
|
4
src/js/components/NotificationsWrapper.js
Normal file
@ -0,0 +1,4 @@
|
||||
import { connect } from 'react-redux';
|
||||
import Notifications from 'react-notification-system-redux';
|
||||
|
||||
export default connect(({ notifications }) => ({ notifications }))(Notifications);
|
64
src/js/components/SignUpPay.js
Normal file
@ -0,0 +1,64 @@
|
||||
import * as actions from 'src/js/actions/index.js';
|
||||
import { connect } from 'react-redux';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import RaisedButton from 'material-ui/RaisedButton';
|
||||
import Dialog from 'material-ui/Dialog';
|
||||
import injectSheet from 'react-jss';
|
||||
import { Step, Stepper, StepLabel } from 'material-ui/Stepper';
|
||||
import { red500 } from 'material-ui/styles/colors';
|
||||
import WarningIcon from 'material-ui/svg-icons/alert/warning';
|
||||
import FlatButton from 'material-ui/FlatButton';
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
display: 'grid',
|
||||
gridGap: '5px',
|
||||
gridTemplateColumns: '3fr 2fr'
|
||||
},
|
||||
header: {
|
||||
gridColumn: '1 / 3'
|
||||
},
|
||||
logoutButton: {
|
||||
top: '20px',
|
||||
right: '20px',
|
||||
position: 'absolute',
|
||||
zIndex: 10000
|
||||
}
|
||||
};
|
||||
|
||||
const STEP = [
|
||||
'Create account',
|
||||
'Confirm your email',
|
||||
'Payment',
|
||||
'Enjoy!'
|
||||
];
|
||||
|
||||
const SignUpPay = ({ classes, children, step, error, small, medium, onClose }) => (
|
||||
<div>
|
||||
<Dialog
|
||||
open
|
||||
modal={!onClose}
|
||||
autoScrollBodyContent
|
||||
contentStyle={{ ...(small ? { maxWidth: '460px' } : medium ? { maxWidth: '780px' } : { maxWidth: 'none', width: '92%' }) }}
|
||||
bodyStyle={{ paddingTop: '24px' }}
|
||||
actions={onClose}
|
||||
// && <FlatButton label="Close" onClick={onClose} />}
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
{children}
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
SignUpPay.defaultProps = {
|
||||
small: false
|
||||
};
|
||||
SignUpPay.propTypes = {
|
||||
small: PropTypes.bool.isRequired,
|
||||
classes: PropTypes.object.isRequired,
|
||||
logout: PropTypes.func.isRequired,
|
||||
error: PropTypes.sting,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default connect(null, { })(injectSheet(styles)(SignUpPay));
|
201
src/js/components/Thumb.js
Normal file
@ -0,0 +1,201 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import injectSheet from 'react-jss';
|
||||
import * as actions from '../actions/index.js';
|
||||
import { connect } from 'react-redux';
|
||||
import StarIcon from 'material-ui-icons/Star';
|
||||
import StarBorderIcon from 'material-ui-icons/StarBorder';
|
||||
import FlagIcon from 'material-ui-icons/Flag';
|
||||
import IconButton from 'material-ui/IconButton';
|
||||
|
||||
const styles = {
|
||||
iconsContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
},
|
||||
icon: {
|
||||
margin: '-10px'
|
||||
},
|
||||
container: {
|
||||
position: 'relative',
|
||||
background: 'white',
|
||||
boxShadow: '1px 2px 5px #70767F',
|
||||
overflow: 'hidden'
|
||||
},
|
||||
button: {
|
||||
cursor: 'pointer'
|
||||
},
|
||||
img: {
|
||||
width: '100%',
|
||||
display: 'block'
|
||||
},
|
||||
footer: {
|
||||
fontFamily: 'Helvetica, arial',
|
||||
color: '#375051',
|
||||
padding: '5px',
|
||||
display: 'flex',
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
},
|
||||
label: {
|
||||
fontSize: '1.2rem',
|
||||
fontWeight: 'bold',
|
||||
textTransform: 'uppercase',
|
||||
display: 'box',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
boxOrient: 'vertical',
|
||||
wordWrap: 'break-word',
|
||||
lineClamp: '2'
|
||||
},
|
||||
openContextMenu: {
|
||||
cursor: 'pointer',
|
||||
padding: '15px',
|
||||
margin: '-10px -10px -10px -5px'
|
||||
},
|
||||
contextMenu: {
|
||||
zIndex: '1',
|
||||
position: 'absolute',
|
||||
bottom: '0',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
color: 'white',
|
||||
transitionProperty: 'transform',
|
||||
transitionDuration: '0.3s',
|
||||
transform: 'translateY(100%)',
|
||||
'& li': {
|
||||
cursor: 'pointer',
|
||||
flex: 'auto',
|
||||
display: 'flex',
|
||||
listStyleType: 'none',
|
||||
height: 55,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontFamily: 'helvetica, arial',
|
||||
fontWeight: 'bold',
|
||||
textTransform: 'uppercase',
|
||||
textShadow: '1px 1px 1px #375051'
|
||||
}
|
||||
},
|
||||
open: {
|
||||
backgroundColor: 'lightblue'
|
||||
},
|
||||
remove: {
|
||||
backgroundColor: 'red'
|
||||
},
|
||||
cancel: {
|
||||
backgroundColor: 'lightgreen'
|
||||
},
|
||||
download: {
|
||||
backgroundColor: 'gold'
|
||||
},
|
||||
active: {
|
||||
transform: 'translateY(0)'
|
||||
},
|
||||
contextMenuContainer: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
overflow: 'hidden'
|
||||
},
|
||||
likeIcon: {
|
||||
zIndex: 1
|
||||
}
|
||||
};
|
||||
|
||||
class Thumb extends React.Component {
|
||||
static propTypes = {
|
||||
doc: PropTypes.shape({
|
||||
_id: PropTypes.string.isRequired,
|
||||
_rev: PropTypes.string,
|
||||
data: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
_attachments: PropTypes.objectOf(PropTypes.shape({
|
||||
data: PropTypes.oneOfType([PropTypes.instanceOf(Blob), PropTypes.string]).isRequired,
|
||||
content_type: PropTypes.sting
|
||||
})).isRequired
|
||||
}).isRequired,
|
||||
contextMenuOpen: PropTypes.bool,
|
||||
onOpen: PropTypes.func,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onDeleted: PropTypes.func,
|
||||
onOpenContextMenu: PropTypes.func,
|
||||
onCloseContextMenu: PropTypes.func,
|
||||
showContextMenu: PropTypes.bool,
|
||||
onDownloadSketch: PropTypes.func.isRequired,
|
||||
classes: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
showContextMenu: true
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.onCloseContextMenu();
|
||||
}
|
||||
render() {
|
||||
const {
|
||||
contextMenuOpen, showContextMenu, classes, doc,
|
||||
onOpenContextMenu, onDelete, onCloseContextMenu, onDownloadSketch
|
||||
} = this.props;
|
||||
let onOpen;
|
||||
if (this.props.onOpen) onOpen = () => this.props.onOpen(doc);
|
||||
|
||||
let img = doc._attachments.img.data;
|
||||
if (img instanceof Blob) img = URL.createObjectURL(doc._attachments.img.data);
|
||||
|
||||
const contextMenuClassNames = [classes.contextMenu];
|
||||
if (contextMenuOpen) contextMenuClassNames.push(classes.active);
|
||||
|
||||
const containerClassNames = [classes.container];
|
||||
if (onOpen) containerClassNames.push(classes.button);
|
||||
|
||||
const numLikes = doc.likes ? doc.likes.length : 0;
|
||||
|
||||
return (
|
||||
<div className={containerClassNames.join(' ')} onClick={onOpen}>
|
||||
{showContextMenu && <ul className={contextMenuClassNames.join(' ')}>
|
||||
{onOpen && <li onClick={onOpen} className={classes.open}><div>Open</div></li>}
|
||||
<li onClick={onDelete} className={classes.remove}><div>Delete</div></li>
|
||||
<li onClick={onDownloadSketch} className={classes.download}><div>Download</div></li>
|
||||
<li onClick={onCloseContextMenu} className={classes.cancel}><div>Cancel</div></li>
|
||||
</ul>}
|
||||
<img src={img} className={classes.img} />
|
||||
<div className={classes.footer}>
|
||||
<div>
|
||||
<p className={classes.label}>{doc.name}</p>
|
||||
{doc.author && <p>{doc.author}</p>}
|
||||
</div>
|
||||
{showContextMenu && <p className={classes.openContextMenu} onClick={onOpenContextMenu}>...</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect((state, props) => ({
|
||||
contextMenuOpen: state.files.activeContextMenu === props.doc._id
|
||||
}), (dispatch, props) => ({
|
||||
onDownloadSketch: (event) => {
|
||||
if (event) event.stopPropagation();
|
||||
dispatch(actions.files.downloadSketch(props.doc));
|
||||
},
|
||||
onDelete: (event) => {
|
||||
if (event) event.stopPropagation();
|
||||
dispatch(actions.files.removeDoodle(props.doc._id)).then(() => {
|
||||
if (props.onDeleted) props.onDeleted();
|
||||
dispatch(actions.files.closeContextMenu());
|
||||
});
|
||||
},
|
||||
onCloseContextMenu: (event) => {
|
||||
if (event) event.stopPropagation();
|
||||
dispatch(actions.files.closeContextMenu());
|
||||
},
|
||||
onOpenContextMenu: (event) => {
|
||||
if (event) event.stopPropagation();
|
||||
dispatch(actions.files.openContextMenu(props.doc._id));
|
||||
}
|
||||
}))(injectSheet(styles)(Thumb));
|
62
src/js/components/VoucherInput.js
Normal file
@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Field } from 'redux-form';
|
||||
import { TextField } from 'redux-form-material-ui';
|
||||
import RaisedButton from 'material-ui/RaisedButton';
|
||||
import injectSheet from 'react-jss';
|
||||
// import createDebug from 'debug';
|
||||
// const debug = createDebug('d3d:comp:voucherInput');
|
||||
|
||||
export const styles = {
|
||||
voucherButton: {
|
||||
margin: '0px 10px 10px 10px'
|
||||
},
|
||||
voucherInput: {
|
||||
width: '140px',
|
||||
top: '-28px'
|
||||
},
|
||||
voucherInputSection: {
|
||||
height: '52px',
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start'
|
||||
}
|
||||
};
|
||||
|
||||
const VoucherInput = (props) => {
|
||||
const { input, meta, onApply, validating, classes } = props;
|
||||
|
||||
const onClick = () => onApply();
|
||||
|
||||
return (
|
||||
<div className={classes.voucherInputSection}>
|
||||
<Field
|
||||
name="voucherCode"
|
||||
component={TextField}
|
||||
floatingLabelText="Apply coupon code"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="none"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
input={input}
|
||||
meta={meta}
|
||||
className={classes.voucherInput}
|
||||
/>
|
||||
<RaisedButton
|
||||
label="Apply"
|
||||
primary
|
||||
onClick={onClick}
|
||||
disabled={meta.pristine || validating || meta.invalid || meta.submitting}
|
||||
className={classes.voucherButton}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
VoucherInput.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
input: PropTypes.object,
|
||||
meta: PropTypes.object,
|
||||
validating: PropTypes.bool,
|
||||
onApply: PropTypes.func
|
||||
};
|
||||
|
||||
export default injectSheet(styles)(VoucherInput);
|
3
src/js/constants/envs.js
Normal file
@ -0,0 +1,3 @@
|
||||
// process.env is replaced by webpack define plugin
|
||||
export const nodeEnv = process.env.NODE_ENV;
|
||||
export const platform = process.env.PLATFORM;
|
12
src/js/constants/general.js
Normal file
@ -0,0 +1,12 @@
|
||||
import * as envs from 'src/js/constants/envs.js';
|
||||
import bowser from 'bowser';
|
||||
|
||||
export const CLICK_TIME_THRESHOLD = 300;
|
||||
export const DOUBLE_CLICK_TIME_THRESHOLD = 300;
|
||||
export const MAX_CLICK_MOVE_THRESHOLD = 10;
|
||||
// On android and iOS autofocus means the keyboard pops up for one second and then hides
|
||||
// Disable autofocus on these devices
|
||||
export const AUTO_FOCUS_TEXT_FIELDS = !(bowser.mobile || bowser.tablet);
|
||||
export const REQUEST_CONFIG = {
|
||||
LARGE: { timeout: 0 }
|
||||
};
|
304
src/js/containers/App.js
Normal file
@ -0,0 +1,304 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import * as actions from 'src/js/actions/index.js';
|
||||
import App from '@doodle3d/doodle3d-core/lib/components/App';
|
||||
import injectSheet from 'react-jss';
|
||||
import Popover from 'material-ui/Popover/Popover';
|
||||
import Badge from 'material-ui/Badge';
|
||||
import btnPrintWiFiImageURL from 'img/export/btnPrintWiFi.png';
|
||||
import btnSaveSTLImageURL from 'img/export/btnSaveSTL.png';
|
||||
import btn3DHubsImageURL from 'img/export/btn3DHubs.png';
|
||||
import btnShapewaysImageURL from 'img/export/btnShapeways.png';
|
||||
import btnThingiverseImageURL from 'img/export/btnThingiverse.png';
|
||||
import btnSketchFabImageURL from 'img/export/btnSketchFab.png';
|
||||
import btnMyMiniFactoryImageURL from 'img/export/btnMyMiniFactory.png';
|
||||
import btnPolar3DImageURL from 'img/export/btnPolar3D.png';
|
||||
import btnNewUrl from 'img/menu/btnNew.png';
|
||||
import btnOpenUrl from 'img/menu/btnOpen.png';
|
||||
import btnSaveUrl from 'img/menu/btnSave.png';
|
||||
import btnSettingsUrl from 'img/menu/btnMenu.png';
|
||||
import btnNewSmallUrl from 'img/menu/btnNewSmall.png';
|
||||
import btnOpenSmallUrl from 'img/menu/btnOpenSmall.png';
|
||||
import btnSaveSmallUrl from 'img/menu/btnSaveSmall.png';
|
||||
import btnSettingsSmallUrl from 'img/menu/btnMenuSmall.png';
|
||||
import btnExportUrl from 'img/menu/btnExport.png';
|
||||
import btnExportSmallUrl from 'img/menu/btnExportSmall.png';
|
||||
import btnLoveUrl from 'img/menu/btnLove.png';
|
||||
import btnLoveSmallUrl from 'img/menu/btnLoveSmall.png';
|
||||
import btnHelpUrl from 'img/menu/btnHelp.png';
|
||||
import btnHelpSmallUrl from 'img/menu/btnHelpSmall.png';
|
||||
|
||||
import Menu from 'material-ui/Menu';
|
||||
import MenuItem from 'material-ui/MenuItem';
|
||||
import ArrowDropLeft from 'material-ui/svg-icons/navigation/chevron-left';
|
||||
|
||||
const button = {
|
||||
cursor: 'pointer',
|
||||
margin: '0 2px'
|
||||
};
|
||||
|
||||
const styles = {
|
||||
badge: {
|
||||
right: '-44px',
|
||||
content: {
|
||||
backgroundColor: 'red'
|
||||
}
|
||||
},
|
||||
buttonLeft: {
|
||||
'@media (max-width: 900px)': {
|
||||
backgroundColor: 'white'
|
||||
},
|
||||
userSelect: 'none',
|
||||
position: 'absolute',
|
||||
top: '0',
|
||||
left: '0',
|
||||
display: 'flex'
|
||||
},
|
||||
new: {
|
||||
'@media (max-width: 900px)': {
|
||||
backgroundImage: `url(${btnNewSmallUrl})`,
|
||||
backgroundSize: '40px auto',
|
||||
width: '40px',
|
||||
height: '40px'
|
||||
},
|
||||
backgroundImage: `url(${btnNewUrl})`,
|
||||
backgroundSize: '72px auto',
|
||||
width: '72px',
|
||||
height: '51px',
|
||||
...button
|
||||
},
|
||||
open: {
|
||||
'@media (max-width: 900px)': {
|
||||
backgroundImage: `url(${btnOpenSmallUrl})`,
|
||||
backgroundSize: '40px auto',
|
||||
width: '40px',
|
||||
height: '40px'
|
||||
},
|
||||
backgroundImage: `url(${btnOpenUrl})`,
|
||||
backgroundSize: '80px auto',
|
||||
width: '80px',
|
||||
height: '55px',
|
||||
...button
|
||||
},
|
||||
save: {
|
||||
'@media (max-width: 900px)': {
|
||||
backgroundImage: `url(${btnSaveSmallUrl})`,
|
||||
backgroundSize: '40px auto',
|
||||
width: '40px',
|
||||
height: '40px'
|
||||
},
|
||||
backgroundImage: `url(${btnSaveUrl})`,
|
||||
backgroundSize: '80px auto',
|
||||
width: '80px',
|
||||
height: '56px',
|
||||
...button
|
||||
},
|
||||
settings: {
|
||||
'@media (max-width: 900px)': {
|
||||
backgroundImage: `url(${btnSettingsSmallUrl})`,
|
||||
backgroundSize: '40px auto',
|
||||
width: '40px',
|
||||
height: '40px'
|
||||
},
|
||||
backgroundImage: `url(${btnSettingsUrl})`,
|
||||
backgroundSize: '77px auto',
|
||||
width: '77px',
|
||||
height: '57px',
|
||||
...button
|
||||
},
|
||||
love: {
|
||||
'@media (max-width: 900px)': {
|
||||
backgroundImage: `url(${btnLoveSmallUrl})`,
|
||||
backgroundSize: '40px auto',
|
||||
width: '40px',
|
||||
height: '40px'
|
||||
},
|
||||
backgroundImage: `url(${btnLoveUrl})`,
|
||||
backgroundSize: '77px auto',
|
||||
width: '77px',
|
||||
height: '57px',
|
||||
...button
|
||||
},
|
||||
help: {
|
||||
'@media (max-width: 900px)': {
|
||||
backgroundImage: `url(${btnHelpSmallUrl})`,
|
||||
backgroundSize: '40px auto',
|
||||
width: '40px',
|
||||
height: '40px'
|
||||
},
|
||||
backgroundImage: `url(${btnHelpUrl})`,
|
||||
backgroundSize: '77px auto',
|
||||
width: '77px',
|
||||
height: '57px',
|
||||
...button
|
||||
},
|
||||
export: {
|
||||
'@media (max-width: 900px)': {
|
||||
backgroundImage: `url(${btnExportSmallUrl})`,
|
||||
backgroundSize: '40px auto',
|
||||
width: '40px',
|
||||
height: '40px'
|
||||
},
|
||||
backgroundImage: `url(${btnExportUrl})`,
|
||||
backgroundSize: '60px auto',
|
||||
position: 'absolute',
|
||||
top: '0',
|
||||
right: '0',
|
||||
width: '60px',
|
||||
height: '71px',
|
||||
cursor: 'pointer'
|
||||
},
|
||||
};
|
||||
|
||||
class AppContainer extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
classes: PropTypes.objectOf(PropTypes.string),
|
||||
downloadAllSketches: PropTypes.func.isRequired,
|
||||
downloadStl: PropTypes.func.isRequired,
|
||||
downloadObj: PropTypes.func.isRequired,
|
||||
clear: PropTypes.func.isRequired,
|
||||
toMyDoodles: PropTypes.func.isRequired,
|
||||
toSave: PropTypes.func.isRequired,
|
||||
toSettings: PropTypes.func.isRequired,
|
||||
toDonate: PropTypes.func.isRequired,
|
||||
toSlicer: PropTypes.func.isRequired,
|
||||
toHelp: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
popover: { open: false, element: null },
|
||||
popoverAbout: { open: false, element: null },
|
||||
}
|
||||
|
||||
openPopover = (event) => {
|
||||
this.setState({
|
||||
popover: {
|
||||
element: event.currentTarget,
|
||||
open: true
|
||||
}
|
||||
});
|
||||
};
|
||||
closePopover = () => {
|
||||
this.setState({
|
||||
popover: {
|
||||
element: null,
|
||||
open: false
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
openPopoverAbout = (event) => {
|
||||
this.setState({
|
||||
popoverAbout: {
|
||||
element: event.currentTarget,
|
||||
open: true
|
||||
}
|
||||
});
|
||||
};
|
||||
closePopoverAbout = () => {
|
||||
this.setState({
|
||||
popoverAbout: {
|
||||
element: null,
|
||||
open: false
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
wrapFunction = (callback) => {
|
||||
return async () => {
|
||||
callback();
|
||||
this.closePopover();
|
||||
this.closePopoverAbout();
|
||||
};
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
downloadStl, downloadObj, downloadAllSketches,
|
||||
clear, toMyDoodles, toSettings, toSave, children, classes,
|
||||
toDonate, toHelp, toSlicer
|
||||
} = this.props;
|
||||
|
||||
const _downloadStl = this.wrapFunction(downloadStl);
|
||||
const _downloadObj = this.wrapFunction(downloadObj);
|
||||
const _downloadAllSketches = this.wrapFunction(downloadAllSketches);
|
||||
const _toSettings = this.wrapFunction(toSettings);
|
||||
const _toHelp = this.wrapFunction(toHelp);
|
||||
const _toDonate = this.wrapFunction(toDonate);
|
||||
const _toSlicer = this.wrapFunction(toSlicer);
|
||||
|
||||
return (
|
||||
<span>
|
||||
<App />
|
||||
<div className={classes.buttonLeft}>
|
||||
<div className={classes.new} onClick={clear} />
|
||||
<div className={classes.open} onClick={toMyDoodles} />
|
||||
<div className={classes.save} onClick={toSave} />
|
||||
<div className={classes.settings} onClick={_toSettings} />
|
||||
<div className={classes.help} onClick={_toHelp} />
|
||||
<div className={classes.love} onClick={_toDonate} />
|
||||
</div>
|
||||
<div className={classes.export} onClick={this.openPopover} />
|
||||
<Popover
|
||||
open={this.state.popover.open}
|
||||
anchorEl={this.state.popover.element}
|
||||
targetOrigin={{ horizontal: 'right', vertical: 'top' }}
|
||||
anchorOrigin={{ horizontal: 'right', vertical: 'top' }}
|
||||
style={{ marginTop:"50px" }}
|
||||
onRequestClose={this.closePopover}
|
||||
>
|
||||
<Menu>
|
||||
<MenuItem
|
||||
primaryText="STL"
|
||||
onClick={_downloadStl}
|
||||
/>
|
||||
<MenuItem
|
||||
primaryText="OBJ"
|
||||
onClick={_downloadObj}
|
||||
/>
|
||||
<MenuItem
|
||||
primaryText="GCODE"
|
||||
onClick={_toSlicer}
|
||||
/>
|
||||
<MenuItem
|
||||
primaryText="Backup as ZIP"
|
||||
onClick={_downloadAllSketches}
|
||||
/>
|
||||
</Menu>
|
||||
</Popover>
|
||||
|
||||
<Popover
|
||||
open={this.state.popoverAbout.open}
|
||||
anchorEl={this.state.popoverAbout.element}
|
||||
targetOrigin={{ horizontal: 'left', vertical: 'top' }}
|
||||
anchorOrigin={{ horizontal: 'right', vertical: 'top' }}
|
||||
style={{marginTop: "50px", marginLeft: "-20px", minWidth: "140px", }}
|
||||
onRequestClose={this.closePopoverAbout}
|
||||
>
|
||||
<Menu>
|
||||
<MenuItem primaryText="Donate" onClick={_toDonate}/>
|
||||
<MenuItem primaryText="Help" onClick={_toHelp}/>
|
||||
<MenuItem primaryText="About" onClick={_toSettings}/>
|
||||
</Menu>
|
||||
</Popover>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => ({ }), {
|
||||
toMyDoodles: () => actions.router.push('/my-doodles'),
|
||||
toSave: () => actions.router.push('/save'),
|
||||
toDonate: () => actions.router.push('/donate'),
|
||||
toHelp: () => actions.router.push('/help'),
|
||||
toSettings: () => actions.router.push('/settings'),
|
||||
toSlicer: () => actions.router.push('/slicer'),
|
||||
clear: actions.sketcher.clear,
|
||||
downloadStl: actions.downloadStl,
|
||||
downloadObj: actions.downloadObj,
|
||||
downloadAllSketches: actions.files.downloadAllSketches
|
||||
})(injectSheet(styles)(AppContainer));
|
68
src/js/containers/Pages/About.js
Normal file
@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as actions from 'src/js/actions/index.js';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link } from 'react-router';
|
||||
import { VERSION } from '@doodle3d/doodle3d-core/lib/constants/general.js';
|
||||
import injectSheet from 'react-jss';
|
||||
import textMarkup from 'src/jss/textMarkup.js';
|
||||
import SignUpPay from 'src/js/components/SignUpPay.js';
|
||||
import Dialog from 'material-ui/Dialog';
|
||||
import iconDoodle3D from 'img/apple-touch-icon-144x144-precomposed.png';
|
||||
|
||||
// import createDebug from 'debug';
|
||||
// const debug = createDebug('d3d:popup:about');
|
||||
|
||||
const styles = {
|
||||
...textMarkup,
|
||||
content: {
|
||||
gridColumn: '1 / 3'
|
||||
},
|
||||
header: {
|
||||
'& h2, & p': {
|
||||
lineHeight: '1.25em',
|
||||
margin: '0'
|
||||
}
|
||||
},
|
||||
floatRight: {
|
||||
float: "right",
|
||||
marginLeft: "20px"
|
||||
}
|
||||
};
|
||||
|
||||
class About extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
classes: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { classes, onClose } = this.props;
|
||||
|
||||
return (
|
||||
<SignUpPay medium onClose={onClose}>
|
||||
<div className={`${classes.text} ${classes.content}`}>
|
||||
<div className={classes.header}>
|
||||
<h2><b>Doodle3D Transform</b> <small>v{VERSION}</small></h2>
|
||||
</div>
|
||||
<img src={iconDoodle3D} className={classes.floatRight} />
|
||||
<p>Doodle3D Transform is a free and open-source web-app that makes designing in 3D easy and fun!
|
||||
Created with love by Casper, Peter, Rick, Nico, Jeroen, Simon, Donna and Arne. With the support of 1,626 Kickstarter backers.</p>
|
||||
<p>As of 2021-05-26 Doodle3D Transform is distributed under the MIT License. This gives everyone the freedoms to use Doodle3D Transform in any context: commercial or non-commercial, public or private, open or closed source.</p>
|
||||
<p>
|
||||
<Link to={'/releasenotes'}>Release Notes</Link> |
|
||||
<Link to={'/licenses'}>Licenses</Link> |
|
||||
<Link to={'/help'}>Help</Link> |
|
||||
<Link to={'/donate'}>Donate</Link> |
|
||||
<a target='_blank' href='https://github.com/doodle3d/'>Source code on Github</a>
|
||||
</p>
|
||||
</div>
|
||||
</SignUpPay>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => ({ }), dispatch => ({
|
||||
onClose: () => dispatch(actions.router.push(`/`)),
|
||||
}))(injectSheet(styles)(About));
|
62
src/js/containers/Pages/AddImage.js
Normal file
@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as actions from 'src/js/actions/index.js';
|
||||
import { connect } from 'react-redux';
|
||||
// import Gallery from 'doodle3d-user/components/Gallery.js';
|
||||
import JSONToSketchData from '@doodle3d/doodle3d-core/lib/shape/JSONToSketchData';
|
||||
import { blobToJSON } from '@doodle3d/doodle3d-core/lib/utils/binaryUtils.js';
|
||||
// import createDebug from 'debug';
|
||||
// const debug = createDebug('d3d:popup:addImage');
|
||||
import SignUpPay from 'src/js/components/SignUpPay.js';
|
||||
|
||||
class AddImage extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
addImage: PropTypes.func.isRequired,
|
||||
routes: PropTypes.array.isRequired,
|
||||
route: PropTypes.object.isRequired,
|
||||
onOpen: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired
|
||||
};
|
||||
onFileChange = (event) => {
|
||||
const { addImage, onClose } = this.props;
|
||||
const input = event.target;
|
||||
|
||||
// there is a file selected?
|
||||
if (!input.files || !input.files[0]) return;
|
||||
const file = input.files[0];
|
||||
addImage(file).then(onClose);
|
||||
};
|
||||
onSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
};
|
||||
render() {
|
||||
const { onOpen, onClose } = this.props;
|
||||
return (
|
||||
<SignUpPay onClose={onClose}>
|
||||
<h2>Add Image</h2>
|
||||
<form onSubmit={this.onSubmit}>
|
||||
<input type="file"
|
||||
accept="image/*"
|
||||
required
|
||||
onChange={this.onFileChange}
|
||||
/>
|
||||
</form>
|
||||
<h2>Import Sketch</h2>
|
||||
</SignUpPay>
|
||||
);
|
||||
}
|
||||
}
|
||||
// <Gallery onOpen={onOpen} />
|
||||
|
||||
export default connect(null, dispatch => ({
|
||||
addImage: () => dispatch(actions.sketcher.addImage),
|
||||
onClose: () => dispatch(actions.router.push(`/`)),
|
||||
onOpen: async (doc) => {
|
||||
const { _attachments } = doc;
|
||||
|
||||
const data = await JSONToSketchData(await blobToJSON(_attachments.sketch.data));
|
||||
dispatch(actions.sketcher.openSketch({ data }));
|
||||
dispatch(actions.router.push(''));
|
||||
}
|
||||
}))(AddImage);
|
52
src/js/containers/Pages/Donate.js
Normal file
@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import injectSheet from 'react-jss';
|
||||
import PropTypes from 'prop-types';
|
||||
import textMarkup from 'src/jss/textMarkup.js';
|
||||
import SignUpPay from 'src/js/components/SignUpPay.js';
|
||||
import { connect } from 'react-redux';
|
||||
import * as actions from 'src/js/actions/index.js';
|
||||
import imgDonate from 'img/paypal-donate-button-doodle3d-QR-code.png';
|
||||
import imgPayPal from 'img/btnPayPal.png';
|
||||
import imgMollie from 'img/btnMollie.png';
|
||||
import imgHeart from 'img/heart.jpg';
|
||||
|
||||
const styles = {
|
||||
...textMarkup,
|
||||
|
||||
floatRight: {
|
||||
float: "right",
|
||||
marginLeft: "20px"
|
||||
},
|
||||
|
||||
floatLeft: {
|
||||
float: "left",
|
||||
marginRight: "20px"
|
||||
}
|
||||
};
|
||||
|
||||
const Donate = ({ classes, onClose }) => (
|
||||
<SignUpPay onClose={onClose} medium >
|
||||
<div className={`${classes.text}`}>
|
||||
<h2>Love Doodle3D?</h2>
|
||||
<img src={imgHeart} width="200" className={classes.floatLeft}/>
|
||||
<a className={classes.floatRight} href="https://www.paypal.com/donate?hosted_button_id=EWJPZ9ZCJU4GE" target="_blank"><img src={imgDonate} height="150"/></a>
|
||||
<p>Do you also love Doodle3D Transform and want to help keeping it online?</p>
|
||||
<p>Your donation helps. Please click one of the buttons below or scan the QR-code.</p>
|
||||
<p>Thank you so much!</p>
|
||||
<br/>
|
||||
<div align="center">
|
||||
<a href="https://useplink.com/payment/FN2SAUblA50f1PlXG2W3w/" target="_blank"><img src={imgMollie} height="50" /></a>
|
||||
|
||||
<a href="https://www.paypal.com/donate?hosted_button_id=EWJPZ9ZCJU4GE" target="_blank"><img src={imgPayPal} height="50"/></a>
|
||||
</div>
|
||||
</div>
|
||||
</SignUpPay>
|
||||
);
|
||||
Donate.propTypes = {
|
||||
classes: PropTypes.objectOf(PropTypes.string),
|
||||
onClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(null, dispatch => ({
|
||||
onClose: () => dispatch(actions.router.push(`/`))
|
||||
}))(injectSheet(styles)(Donate));
|
38
src/js/containers/Pages/Help.js
Normal file
@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import injectSheet from 'react-jss';
|
||||
import PropTypes from 'prop-types';
|
||||
import textMarkup from 'src/jss/textMarkup.js';
|
||||
import SignUpPay from 'src/js/components/SignUpPay.js';
|
||||
import { connect } from 'react-redux';
|
||||
import * as actions from 'src/js/actions/index.js';
|
||||
|
||||
const VIDEO_SRC = 'https://www.youtube.com/embed/rkZNNzSJBps?modestbranding=1';
|
||||
|
||||
const styles = {
|
||||
...textMarkup,
|
||||
content: {
|
||||
gridColumn: '1 / 3'
|
||||
}
|
||||
};
|
||||
|
||||
const Help = ({ classes, onClose }) => (
|
||||
<SignUpPay onClose={onClose} medium >
|
||||
<div className={`${classes.text} ${classes.content}`}>
|
||||
<iframe
|
||||
width="840"
|
||||
height="410"
|
||||
src={VIDEO_SRC}
|
||||
frameBorder="0"
|
||||
allowFullScreen=""
|
||||
/>
|
||||
</div>
|
||||
</SignUpPay>
|
||||
);
|
||||
Help.propTypes = {
|
||||
classes: PropTypes.objectOf(PropTypes.string),
|
||||
onClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(null, dispatch => ({
|
||||
onClose: () => dispatch(actions.router.push(`/`))
|
||||
}))(injectSheet(styles)(Help));
|
65
src/js/containers/Pages/Licenses.js
Normal file
@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as actions from 'src/js/actions/index.js';
|
||||
import { connect } from 'react-redux';
|
||||
import injectSheet from 'react-jss';
|
||||
import textMarkup from 'src/jss/textMarkup.js';
|
||||
import dependencies from 'data/licenses.json';
|
||||
import _ from 'lodash';
|
||||
import SignUpPay from 'src/js/components/SignUpPay.js';
|
||||
// import createDebug from 'debug';
|
||||
// const debug = createDebug('d3d:popup:licenses');
|
||||
|
||||
const styles = {
|
||||
content: {
|
||||
gridColumn: '1 / 3'
|
||||
},
|
||||
...textMarkup
|
||||
};
|
||||
|
||||
class Licenses extends React.Component {
|
||||
static propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
routes: PropTypes.array.isRequired,
|
||||
route: PropTypes.object.isRequired,
|
||||
classes: PropTypes.object.isRequired
|
||||
};
|
||||
render() {
|
||||
const { classes, onClose } = this.props;
|
||||
|
||||
let licenses = dependencies.map(dep => dep.licenses);
|
||||
licenses = _.flatten(licenses); // some packages contain an array with multiple licenses
|
||||
licenses = licenses.map(license => license.replace(/\*$/, '')); // remove * from licenses, see footnote
|
||||
const uniqLicenses = _.uniq(licenses);
|
||||
|
||||
return (
|
||||
<SignUpPay onClose={onClose}>
|
||||
<div className={`${classes.text} ${classes.content}`}>
|
||||
<h2>Licenses Doodle3D</h2>
|
||||
<table>
|
||||
<tbody>
|
||||
{dependencies.map(dependency => (
|
||||
<tr key={dependency.name}>
|
||||
<td><a href={dependency.url}>{dependency.name}</a></td>
|
||||
<td className={classes.publisher}>{dependency.publisher || ''}</td>
|
||||
<td>{dependency.licenses}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
* License was deduced from an other file than package.json (README, LICENSE, COPYING, ...)
|
||||
</p>
|
||||
<p>Unique licenses found:</p>
|
||||
<ul>
|
||||
{uniqLicenses.map(license => <li>{license}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
</SignUpPay>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(null, dispatch => ({
|
||||
onClose: () => dispatch(actions.router.push(`/`))
|
||||
}))(injectSheet(styles)(Licenses));
|
196
src/js/containers/Pages/MyDoodles.js
Normal file
@ -0,0 +1,196 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
// import Gallery from 'doodle3d-user/components/Gallery.js';
|
||||
import * as actions from 'src/js/actions/index.js';
|
||||
import PropTypes from 'prop-types';
|
||||
import JSONToSketchData from '@doodle3d/doodle3d-core/lib/shape/JSONToSketchData';
|
||||
import { blobToJSON } from '@doodle3d/doodle3d-core/lib/utils/binaryUtils.js';
|
||||
import SignUpPay from 'src/js/components/SignUpPay.js';
|
||||
import Thumb from 'src/js/components/Thumb.js';
|
||||
import injectSheet from 'react-jss';
|
||||
import Popover from 'material-ui/Popover';
|
||||
import { forceResize } from '../../utils/utils.js';
|
||||
import CircularProgress from 'material-ui/CircularProgress';
|
||||
import Menu from 'material-ui/Menu';
|
||||
import MenuItem from 'material-ui/MenuItem';
|
||||
import RaisedButton from 'material-ui/RaisedButton';
|
||||
import Pagination from 'material-ui-pagination';
|
||||
|
||||
const orders = [
|
||||
{ id: 'DATE', text: 'Date New-Old', type: 'updatedOn', desc: true },
|
||||
{ id: 'DATE_REVERSE', text: 'Date Old-New', type: 'updatedOn', desc: false },
|
||||
{ id: 'ALPHABETICAL', text: 'Alphabetical A-Z', type: 'name', desc: false },
|
||||
{ id: 'ALPHABETICAL_REVERSE', text: 'Alphabetical Z-A', type: 'name', desc: true }
|
||||
];
|
||||
|
||||
const style = {
|
||||
grid: {
|
||||
'@supports (display: grid)': {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))',
|
||||
gridColumnGap: '10px',
|
||||
gridRowGap: '15px'
|
||||
},
|
||||
'@supports not (display: grid)': {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
'& > *': {
|
||||
width: '240px'
|
||||
}
|
||||
}
|
||||
},
|
||||
content: {
|
||||
gridColumn: '1 / 3'
|
||||
}
|
||||
};
|
||||
|
||||
class MyDoodles extends React.Component {
|
||||
static propTypes = {
|
||||
classes: PropTypes.objectOf(PropTypes.string),
|
||||
onOpen: PropTypes.func.isRequired,
|
||||
loadGallery: PropTypes.func.isRequired,
|
||||
openFileSelector: PropTypes.func.isRequired,
|
||||
downloadAllSketches: PropTypes.func.isRequired,
|
||||
numItems: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
state = {
|
||||
orderSelect: {
|
||||
open: false,
|
||||
element: null
|
||||
},
|
||||
page: 0,
|
||||
order: 'DATE'
|
||||
};
|
||||
|
||||
|
||||
static defaultProps = {
|
||||
numItems: 9
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
console.log("componentWillMount");
|
||||
|
||||
this.updatePage();
|
||||
}
|
||||
|
||||
componentDidUpdate(props, state) {
|
||||
if (this.state.page !== state.page || this.state.order !== state.order) this.updatePage();
|
||||
}
|
||||
|
||||
updatePage = () => {
|
||||
const { loadGallery, numItems } = this.props;
|
||||
const { page, order } = this.state;
|
||||
|
||||
const { type, desc } = orders.find(({ id }) => id === order);
|
||||
|
||||
forceResize();
|
||||
loadGallery(page, numItems, type, desc);
|
||||
//.then(forceResize);
|
||||
};
|
||||
|
||||
openOrderSelect = (event) => {
|
||||
event.preventDefault();
|
||||
this.setState({ orderSelect: { open: true, element: event.currentTarget } });
|
||||
};
|
||||
|
||||
closeOrderSelect = () => {
|
||||
this.setState({ orderSelect: { open: false, element: null } });
|
||||
};
|
||||
|
||||
changeOrder = (order) => {
|
||||
this.closeOrderSelect();
|
||||
this.setState({ order, page: 0 });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { classes, numItems, onClose, gallery, onOpen, downloadAllSketches, openFileSelector } = this.props;
|
||||
const { page, orderSelect } = this.state;
|
||||
|
||||
const _openFileSelector = async () => {
|
||||
await openFileSelector();
|
||||
this.updatePage();
|
||||
};
|
||||
|
||||
const changePage = (page) => this.setState({ page: page - 1 });
|
||||
|
||||
return (
|
||||
<SignUpPay onClose={onClose}>
|
||||
<div className={classes.content}>
|
||||
<h2>My Doodles</h2>
|
||||
{gallery.ready ? <div>
|
||||
<Popover
|
||||
open={orderSelect.open}
|
||||
anchorEl={orderSelect.element}
|
||||
onRequestClose={this.closeOrderSelect}
|
||||
>
|
||||
<Menu>
|
||||
{orders.map(({ id, text }) => <MenuItem
|
||||
key={id}
|
||||
onClick={() => this.changeOrder(id)}
|
||||
primaryText={text}
|
||||
/>)}
|
||||
</Menu>
|
||||
</Popover>
|
||||
{gallery.data.rows.length === 0 ?
|
||||
<div>
|
||||
<RaisedButton
|
||||
label="Import Doodles"
|
||||
onClick={_openFileSelector}
|
||||
style={{ margin: '10px 0', }}
|
||||
/>
|
||||
<p>No Doodles Found</p>
|
||||
</div> : <div>
|
||||
<RaisedButton
|
||||
label="Change Order"
|
||||
onClick={this.openOrderSelect}
|
||||
style={{ margin: '10px 0' }}
|
||||
/>
|
||||
<RaisedButton
|
||||
label="Import Doodles"
|
||||
onClick={_openFileSelector}
|
||||
style={{ margin: '10px 5px', }}
|
||||
/>
|
||||
<RaisedButton
|
||||
label="Backup as ZIP"
|
||||
onClick={downloadAllSketches}
|
||||
style={{ margin: '10px 0' }}
|
||||
/>
|
||||
<div className={classes.grid}>
|
||||
{gallery.data.rows.map(({ doc }) => <Thumb
|
||||
key={doc._id}
|
||||
onOpen={onOpen}
|
||||
onDeleted={this.updatePage}
|
||||
doc={doc}
|
||||
/>)}
|
||||
</div>
|
||||
<Pagination
|
||||
styleRoot={{ display: 'flex', justifyContent: 'center', margin: '10px 0' }}
|
||||
total={Math.ceil(gallery.data.total_rows / numItems)}
|
||||
current={page + 1}
|
||||
display={10}
|
||||
onChange={changePage}
|
||||
/>
|
||||
</div>}
|
||||
</div> : <CircularProgress style={{ margin: '0 auto', display: 'block' }} />}
|
||||
</div>
|
||||
</SignUpPay>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
gallery: state.files.gallery
|
||||
}), (dispatch) => ({
|
||||
openFileSelector: () => dispatch(actions.files.openFileSelector()),
|
||||
downloadAllSketches: () => dispatch(actions.files.downloadAllSketches()),
|
||||
loadGallery: (page, numItems, type, desc) => dispatch(actions.files.loadGallery(page, numItems, type, desc)),
|
||||
onClose: () => dispatch(actions.router.push(`/`)),
|
||||
onOpen: async (doc) => {
|
||||
const { _id, name, _attachments } = doc;
|
||||
dispatch(actions.sketcher.clear());
|
||||
const data = await JSONToSketchData(await blobToJSON(_attachments.sketch.data));
|
||||
dispatch(actions.files.openFile(data, name, _id));
|
||||
dispatch(actions.router.push(''));
|
||||
}
|
||||
}))(injectSheet(style)(MyDoodles));
|
45
src/js/containers/Pages/ReleaseNotes.js
Normal file
@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as actions from 'src/js/actions/index.js';
|
||||
import { connect } from 'react-redux';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import injectSheet from 'react-jss';
|
||||
import textMarkup from 'src/jss/textMarkup.js';
|
||||
import { processContent } from 'src/js/utils/contentUtils.js';
|
||||
import changelog from 'CHANGELOG.md';
|
||||
// import createDebug from 'debug';
|
||||
// const debug = createDebug('d3d:popup:ReleaseNotes');
|
||||
import SignUpPay from 'src/js/components/SignUpPay.js';
|
||||
|
||||
const styles = {
|
||||
...textMarkup,
|
||||
content: {
|
||||
gridColumn: '1 / 3'
|
||||
}
|
||||
};
|
||||
|
||||
class ReleaseNotes extends React.Component {
|
||||
static propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
routes: PropTypes.array.isRequired,
|
||||
route: PropTypes.object.isRequired,
|
||||
classes: PropTypes.object.isRequired
|
||||
};
|
||||
render() {
|
||||
const { classes, onClose } = this.props;
|
||||
const content = processContent(changelog || '');
|
||||
|
||||
return (
|
||||
<SignUpPay onClose={onClose}>
|
||||
<div className={`${classes.text} ${classes.content}`}>
|
||||
<h2>Release notes</h2>
|
||||
<ReactMarkdown source={content} />
|
||||
</div>
|
||||
</SignUpPay>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(null, dispatch => ({
|
||||
onClose: () => dispatch(actions.router.push(`/`))
|
||||
}))(injectSheet(styles)(ReleaseNotes));
|
92
src/js/containers/Pages/Save.js
Normal file
@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as actions from 'src/js/actions/index.js';
|
||||
import { connect } from 'react-redux';
|
||||
import { currentFileName } from 'src/js/reducers/index.js';
|
||||
import textMarkup from 'src/jss/textMarkup.js';
|
||||
import injectSheet from 'react-jss';
|
||||
import { reduxForm, Field, startSubmit, formValueSelector } from 'redux-form';
|
||||
import { formPromiseWrapper } from 'src/js/utils/formUtils.js';
|
||||
import { TextField } from 'redux-form-material-ui';
|
||||
import RaisedButton from 'material-ui/RaisedButton';
|
||||
import { AUTO_FOCUS_TEXT_FIELDS } from 'src/js/constants/general.js';
|
||||
import Dialog from 'material-ui/Dialog';
|
||||
import FlatButton from 'material-ui/FlatButton';
|
||||
|
||||
// import createDebug from 'debug';
|
||||
// const debug = createDebug('d3d:popup:save');
|
||||
|
||||
const style = {
|
||||
...textMarkup
|
||||
};
|
||||
|
||||
const onSubmit = async (values, dispatch, props) => {
|
||||
await formPromiseWrapper(props.save(values.fileName));
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
|
||||
class Save extends React.Component {
|
||||
static propTypes = {
|
||||
classes: PropTypes.objectOf(PropTypes.string),
|
||||
save: PropTypes.func.isRequired,
|
||||
submit: PropTypes.func.isRequired,
|
||||
handleSubmit: PropTypes.func,
|
||||
pristine: PropTypes.bool,
|
||||
downloadCurrentSketch: PropTypes.func.isRequired,
|
||||
fileName: PropTypes.string.isRequired,
|
||||
submitting: PropTypes.bool,
|
||||
invalid: PropTypes.bool,
|
||||
error: PropTypes.object,
|
||||
onClose: PropTypes.func.isRequired
|
||||
};
|
||||
render() {
|
||||
const { classes, handleSubmit, submitting, invalid, error, onClose, submit, save, downloadCurrentSketch, fileName } = this.props;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open
|
||||
contentStyle={{ maxWidth: '460px' }}
|
||||
actions={[
|
||||
<FlatButton label="Close" onClick={onClose} />,
|
||||
<FlatButton label="Download file" onClick={() => downloadCurrentSketch(fileName)} />,
|
||||
<RaisedButton label="Save in browser" onClick={handleSubmit} primary disabled={ submitting || invalid } />
|
||||
]}
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<div>
|
||||
<h2>Save</h2>
|
||||
<p>Your doodle will be saved in the <u>local storage of your web browser</u>.
|
||||
Make sure to make a <a>backup (see export menu)</a> of your doodles every once in a while.</p>
|
||||
{error && <p className={classes.error}>error</p>}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Field
|
||||
autoFocus={AUTO_FOCUS_TEXT_FIELDS}
|
||||
name="fileName"
|
||||
component={TextField}
|
||||
floatingLabelText="File name"
|
||||
fullWidth
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const formName = 'save';
|
||||
|
||||
const selector = formValueSelector(formName);
|
||||
|
||||
export default connect(state => ({
|
||||
fileName: selector(state, "fileName"),
|
||||
initialValues: { fileName: currentFileName(state) || '' }
|
||||
}), (dispatch) => ({
|
||||
downloadCurrentSketch: (fileName) => dispatch(actions.files.downloadCurrentSketch(fileName)),
|
||||
submit: () => dispatch(startSubmit(formName)),
|
||||
save: (fileName) => dispatch(actions.files.saveFile(fileName)),
|
||||
onClose: () => dispatch(actions.router.push('/'))
|
||||
}))(reduxForm({
|
||||
form: formName,
|
||||
onSubmit
|
||||
})(injectSheet(style)(Save)));
|
72
src/js/containers/Pages/Slicer.js
Normal file
@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import injectSheet from 'react-jss';
|
||||
import PropTypes from 'prop-types';
|
||||
import textMarkup from 'src/jss/textMarkup.js';
|
||||
import SignUpPay from 'src/js/components/SignUpPay.js';
|
||||
import { connect } from 'react-redux';
|
||||
import * as actions from 'src/js/actions/index.js';
|
||||
import { Interface } from '@doodle3d/doodle3d-slicer'
|
||||
import Dialog from 'material-ui/Dialog';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { generateExportMesh } from '@doodle3d/doodle3d-core/lib/utils/exportUtils.js';
|
||||
import { Matrix4 } from 'three';
|
||||
import { isEmpty } from '@doodle3d/doodle3d-core/lib/reducer';
|
||||
|
||||
const styles = {
|
||||
content: {
|
||||
height: '800px',
|
||||
'@media (max-height: 950px)': {
|
||||
height: '750px',
|
||||
},
|
||||
'@media (max-height: 750px)': {
|
||||
height: '400px',
|
||||
},
|
||||
'@media (max-height: 550px)': {
|
||||
height: '200px',
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
class Slicer extends React.Component {
|
||||
static = {
|
||||
classes: PropTypes.objectOf(PropTypes.string),
|
||||
onClose: PropTypes.func.isRequired,
|
||||
sketchData: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
let mesh = null;
|
||||
if (this.props.sketchData) {
|
||||
mesh = generateExportMesh(this.props.sketchData, {
|
||||
offsetSingleWalls: false,
|
||||
matrix: new Matrix4()
|
||||
});
|
||||
}
|
||||
this.setState({ mesh });
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Dialog
|
||||
open
|
||||
contentStyle={{ maxWidth: 'none' }}
|
||||
bodyStyle={{ padding: '0px' }}
|
||||
onRequestClose={this.props.onClose}
|
||||
>
|
||||
<div className={this.props.classes.content}>
|
||||
<Interface
|
||||
onCancel={this.props.onClose}
|
||||
onSliceSucces={({ gcode }) => saveAs(gcode, 'doodle.gcode')}
|
||||
mesh={this.state.mesh}
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
sketchData: isEmpty(state) ? null : state.sketcher.present
|
||||
}), dispatch => ({
|
||||
onClose: () => dispatch(actions.router.push(`/`))
|
||||
}))(injectSheet(styles)(Slicer));
|
151
src/js/index.js
Normal file
@ -0,0 +1,151 @@
|
||||
import * as envs from 'src/js/constants/envs.js';
|
||||
|
||||
// log version
|
||||
import { VERSION } from '@doodle3d/doodle3d-core/lib/constants/general.js';
|
||||
console.log(`#####################################################`); // eslint-disable-line no-console
|
||||
console.log(`############# Doodle3D Transform v${VERSION} #############`); // eslint-disable-line no-console
|
||||
|
||||
// polyfills
|
||||
import 'whatwg-fetch';
|
||||
import 'babel-polyfill';
|
||||
// import 'core-js';
|
||||
// import 'core-js/fn/object/assign';
|
||||
|
||||
// normalize css
|
||||
import jss from 'jss';
|
||||
import preset from 'jss-preset-default';
|
||||
import normalize from 'normalize-jss';
|
||||
jss.setup(preset());
|
||||
jss.createStyleSheet(normalize).attach();
|
||||
jss.createStyleSheet({
|
||||
'@global': {
|
||||
'*': { margin: 0, padding: 0 },
|
||||
'#app, body, html': { height: '100%', fontFamily: 'sans-serif' },
|
||||
body: { overflow: 'auto' },
|
||||
html: { overflow: 'hidden' },
|
||||
a: { color: '#358ED7' }
|
||||
}
|
||||
}).attach();
|
||||
|
||||
import createDebug from 'debug';
|
||||
const debug = createDebug('d3d:index');
|
||||
|
||||
// create store
|
||||
import reducer from './reducers/index.js';
|
||||
import configureStore from './store.js';
|
||||
const store = configureStore(reducer);
|
||||
debug('initial state: ', store.getState());
|
||||
|
||||
// load local (browser & device specific) data
|
||||
import * as actions from 'src/js/actions/index.js';
|
||||
store.dispatch(actions.localStore.read());
|
||||
|
||||
// For debugging purposes: allow dispatching actions from Web console
|
||||
import actionWrapper from 'redux-action-wrapper';
|
||||
window.actions = actionWrapper(actions, store.dispatch);
|
||||
|
||||
import * as CAL from 'cal';
|
||||
window.CAL = CAL;
|
||||
|
||||
// Create an enhanced history that syncs navigation events with the store
|
||||
import { browserHistory, hashHistory } from 'react-router';
|
||||
import { syncHistoryWithStore } from 'react-router-redux';
|
||||
const history = syncHistoryWithStore(envs.platform === 'ios-app' ? hashHistory : browserHistory, store);
|
||||
|
||||
window.onbeforeunload = event => {
|
||||
const state = store.getState();
|
||||
|
||||
if (state.files.current.unSavedData) {
|
||||
event.returnValue = 'You have unsaved work';
|
||||
return event.returnValue;
|
||||
}
|
||||
};
|
||||
|
||||
// add hotkeys
|
||||
let { pathname } = history.getCurrentLocation();
|
||||
history.listen(event => {
|
||||
pathname = event.pathname;
|
||||
});
|
||||
|
||||
const keyHandler = event => store.dispatch(actions.hotkeys.keyPress(event, pathname));
|
||||
window.addEventListener('keydown', keyHandler);
|
||||
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { AppContainer } from 'react-hot-loader';
|
||||
import { Provider } from 'react-redux';
|
||||
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
|
||||
import muiTheme from 'src/js/muiTheme.js';
|
||||
import Root from 'src/js/Root.js';
|
||||
import NotificationsWrapper from 'src/js/components/NotificationsWrapper.js';
|
||||
import BlockingSpinner from 'src/js/components/BlockingSpinner.js';
|
||||
import Prompt from 'redux-prompt/component';
|
||||
import { isWebGLAvailable } from '@doodle3d/doodle3d-core/lib/utils/webGLSupport.js';
|
||||
import bowser from 'bowser';
|
||||
|
||||
async function init() {
|
||||
store.dispatch(actions.files.init());
|
||||
|
||||
render(
|
||||
<AppContainer>
|
||||
<Provider store={store}>
|
||||
<MuiThemeProvider muiTheme={muiTheme}>
|
||||
<span>
|
||||
<Root />
|
||||
<BlockingSpinner />
|
||||
<NotificationsWrapper />
|
||||
</span>
|
||||
</MuiThemeProvider>
|
||||
</Provider>
|
||||
</AppContainer>,
|
||||
document.getElementById('app')
|
||||
);
|
||||
|
||||
if (!isWebGLAvailable) {
|
||||
store.dispatch(actions.notification.warning({
|
||||
title: `Oops… Doodle3D Transfrom is unable to use WebGL on your Device.`,
|
||||
autoDismiss: 0
|
||||
}));
|
||||
}
|
||||
|
||||
if (bowser.mobile) {
|
||||
store.dispatch(actions.notification.warning({
|
||||
title: `This app is optimized for tablet and desktop devices.`,
|
||||
autoDismiss: 0
|
||||
}));
|
||||
}
|
||||
|
||||
// if (module.hot) {
|
||||
// module.hot.accept('./Root.js', () => {
|
||||
// debug('Root changed, re-rendering');
|
||||
// const NewRoot = require('./Root.js').default;
|
||||
// render(
|
||||
// <AppContainer>
|
||||
// <Provider store={store}>
|
||||
// <MuiThemeProvider muiTheme={muiTheme}>
|
||||
// <span>
|
||||
// <NewRoot />
|
||||
// <BlockingSpinner />
|
||||
// <Prompt />
|
||||
// <NotificationsWrapper />
|
||||
// </span>
|
||||
// </MuiThemeProvider>
|
||||
// </Provider>
|
||||
// </AppContainer>,
|
||||
// document.getElementById('app')
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
}
|
||||
init();
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
export function __unload() {
|
||||
try {
|
||||
ReactDOM.unmountComponentAtNode(document.getElementById('app'));
|
||||
} catch (e) {
|
||||
// ignoring unmount error
|
||||
}
|
||||
window.removeEventListener('keydown', keyHandler);
|
||||
history.unsubscribe();
|
||||
}
|
18
src/js/muiTheme.js
Normal file
@ -0,0 +1,18 @@
|
||||
// import { grey100, cyan500, red50, darkBlack, white } from 'material-ui/styles/colors';
|
||||
import getMuiTheme from 'material-ui/styles/getMuiTheme';
|
||||
// import { fade } from 'material-ui/utils/colorManipulator';
|
||||
|
||||
const muiTheme = getMuiTheme({
|
||||
raisedButton: {
|
||||
primaryColor: '#358ed7',
|
||||
primaryTextColor: '#ffffff',
|
||||
textTransform: 'none',
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
flatButton: {
|
||||
textTransform: 'none',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
});
|
||||
|
||||
export default muiTheme;
|
8
src/js/preloader.js
Normal file
@ -0,0 +1,8 @@
|
||||
import 'core-js/fn/promise'; // IE 11 support
|
||||
import Spinner from 'spin.js';
|
||||
const target = document.getElementById('app');
|
||||
const preloader = new Spinner().spin(target);
|
||||
|
||||
import(/* webpackChunkName: "main" */ './index.js').then(() => {
|
||||
preloader.stop();
|
||||
});
|
22
src/js/reducers/blockingSpinnerReducer.js
Normal file
@ -0,0 +1,22 @@
|
||||
import * as actions from 'src/js/actions/blockingSpinner.js';
|
||||
|
||||
const initialState = {
|
||||
active: false
|
||||
};
|
||||
|
||||
export default function blockingSpinnerReducer(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.START:
|
||||
return {
|
||||
...state,
|
||||
active: true
|
||||
};
|
||||
case actions.STOP:
|
||||
return {
|
||||
...state,
|
||||
active: false
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
74
src/js/reducers/filesReducer.js
Normal file
@ -0,0 +1,74 @@
|
||||
import update from 'react-addons-update';
|
||||
import * as actions from 'src/js/actions/index.js';
|
||||
import * as sketcher from '@doodle3d/doodle3d-core/lib/actions';
|
||||
import { createPromiseReducer, initialState as initialPromiseState } from 'redux-promise-action';
|
||||
|
||||
const SKETCHER_ACTIONS = Object.keys(sketcher);
|
||||
export const initialState = {
|
||||
name: null,
|
||||
id: null,
|
||||
unSavedData: false,
|
||||
gallery: initialPromiseState,
|
||||
activeContextMenu: null,
|
||||
};
|
||||
|
||||
const galleryReducer = createPromiseReducer(actions.files.LOAD_GALLERY);
|
||||
|
||||
export default (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case actions.sketcher.CLEAR: {
|
||||
return update(state, {
|
||||
name: { $set: null },
|
||||
id: { $set: null },
|
||||
unSavedData: { $set: false },
|
||||
});
|
||||
}
|
||||
|
||||
case actions.files.OPEN_FILE: {
|
||||
const { name, id } = action;
|
||||
return update(state, {
|
||||
name: { $set: action.name },
|
||||
id: { $set: action.id },
|
||||
unSavedData: { $set: false },
|
||||
});
|
||||
}
|
||||
|
||||
case `${actions.files.SAVE_DOODLE}_FULFILLED`: {
|
||||
return update(state, {
|
||||
name: { $set: action.payload.name },
|
||||
id: { $set: action.payload.id },
|
||||
unSavedData: { $set: false },
|
||||
});
|
||||
}
|
||||
|
||||
case `${actions.files.REMOVE_DOODLE}_FULFILLED`:
|
||||
if (action.payload._id === state.id) {
|
||||
return update(state, {
|
||||
id: { $set: null },
|
||||
unSavedData: { $set: false },
|
||||
});
|
||||
}
|
||||
return state;
|
||||
|
||||
case actions.files.OPEN_CONTEXT_MENU:
|
||||
return update(state, { activeContextMenu: { $set: action.id } });
|
||||
case actions.files.CLOSE_CONTEXT_MENU:
|
||||
return update(state, { activeContextMenu: { $set: null } });
|
||||
|
||||
default:
|
||||
if (SKETCHER_ACTIONS.includes(action.type)) {
|
||||
return update(state, {
|
||||
unSavedData: { $set: true }
|
||||
});
|
||||
}
|
||||
|
||||
return update(state, {
|
||||
gallery: { $set: galleryReducer(state.gallery, action) }
|
||||
});
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export function currentName(state) {
|
||||
return state.name;
|
||||
}
|
21
src/js/reducers/index.js
Normal file
@ -0,0 +1,21 @@
|
||||
import 'src/js/actions/index.js'; // TODO find a way to remove this
|
||||
import { combineReducers } from 'redux';
|
||||
import sketcherReducer from '@doodle3d/doodle3d-core/lib/reducer';
|
||||
import { routerReducer } from 'react-router-redux';
|
||||
import filesReducer, { currentName } from './filesReducer.js';
|
||||
import { reducer as formReducer } from 'redux-form';
|
||||
import { reducer as notificationsReducer } from 'react-notification-system-redux';
|
||||
import blockingSpinnerReducer from './blockingSpinnerReducer.js';
|
||||
import promptReducer from 'redux-prompt/reducer';
|
||||
|
||||
export default combineReducers({
|
||||
sketcher: sketcherReducer,
|
||||
routing: routerReducer,
|
||||
form: formReducer,
|
||||
notifications: notificationsReducer,
|
||||
files: filesReducer,
|
||||
blockingSpinner: blockingSpinnerReducer,
|
||||
prompt: promptReducer,
|
||||
});
|
||||
|
||||
export const currentFileName = (state) => currentName(state.files);
|
42
src/js/services/config.js
Normal file
@ -0,0 +1,42 @@
|
||||
import createDebug from 'debug';
|
||||
const debug = createDebug('d3d:config');
|
||||
|
||||
const NAME = 'doodle3d_config';
|
||||
|
||||
const defaultConfig = {
|
||||
experimentalColorPicker: false,
|
||||
experimentalStampTool: false,
|
||||
experimentalColorUnionExport: false,
|
||||
d3ArrowHelpers: false,
|
||||
gaDebug: false,
|
||||
exportLineWidth: 2.0 // in mm
|
||||
};
|
||||
|
||||
let parsedConfig = {};
|
||||
try {
|
||||
const rawConfig = localStorage.getItem(NAME);
|
||||
if (rawConfig) {
|
||||
parsedConfig = JSON.parse(rawConfig);
|
||||
}
|
||||
} catch (error) {
|
||||
/* eslint-disable no-console */
|
||||
console.error('Parsing doodle3d config from localStorage failed, falling back to default');
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
|
||||
let config = {
|
||||
...defaultConfig,
|
||||
...parsedConfig
|
||||
};
|
||||
debug('config: ', config);
|
||||
|
||||
export const get = () => config;
|
||||
export const set = (newConfig) => {
|
||||
config = newConfig;
|
||||
localStorage.setItem(NAME, JSON.stringify(config));
|
||||
return config;
|
||||
};
|
||||
export const extend = (newConfig) => set({ ...config, ...newConfig });
|
||||
export const reset = () => set(defaultConfig);
|
||||
|
||||
window.config = { get, set, extend, reset };
|
44
src/js/services/localStore.js
Normal file
@ -0,0 +1,44 @@
|
||||
// import createDebug from 'debug';
|
||||
// const debug = createDebug('d3d:service:localStore');
|
||||
|
||||
const NAME = 'doodle3d';
|
||||
|
||||
export function read() {
|
||||
try {
|
||||
const raw = localStorage.getItem(NAME);
|
||||
if (raw) return JSON.parse(raw);
|
||||
} catch (error) {
|
||||
console.error('Reading localStorage failed, falling back to default'); // eslint-disable-line no-console
|
||||
}
|
||||
return null;
|
||||
}
|
||||
export function write(newData) {
|
||||
// debug('write: ', newData);
|
||||
try {
|
||||
localStorage.setItem(NAME, JSON.stringify(newData));
|
||||
} catch (error) {
|
||||
console.error('Storing data to localStorage failed'); // eslint-disable-line no-console
|
||||
}
|
||||
return newData;
|
||||
}
|
||||
|
||||
// Middleware
|
||||
// Make a subsection of the state persistant
|
||||
// Will start writing to storage after a initializingAction
|
||||
export function makePersistantMiddleware(selector, initializingAction) {
|
||||
let prevState;
|
||||
let initialized = false;
|
||||
return store => next => action => {
|
||||
let result = next(action);
|
||||
// retrieve state to persist using selector
|
||||
const state = selector(store.getState());
|
||||
if (state !== prevState) { // if subsection changed
|
||||
if (initialized) write(state); // store state
|
||||
prevState = state;
|
||||
}
|
||||
if (action.type && action.type === initializingAction) {
|
||||
initialized = true;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
64
src/js/store.js
Normal file
@ -0,0 +1,64 @@
|
||||
import { createStore, compose, applyMiddleware } from 'redux';
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
import promiseMiddleware from 'redux-promise-middleware';
|
||||
import { browserHistory, hashHistory } from 'react-router';
|
||||
import { routerMiddleware } from 'react-router-redux';
|
||||
import createLogger from 'redux-logger';
|
||||
import { makePersistantMiddleware } from 'src/js/services/localStore.js';
|
||||
import { LOCAL_STORE_READ } from 'src/js/actions/localStore.js';
|
||||
import { platform } from 'src/js/constants/envs.js';
|
||||
import promptMiddleware from 'redux-prompt/middleware';
|
||||
|
||||
import debugOverlappingDispatches from './utils/debugOverlappingDispatches.js';
|
||||
|
||||
import createDebug from 'debug';
|
||||
// in dev mode when finds debug key in localStorage
|
||||
// (which is also used by debug module)
|
||||
const devMode = createDebug.load() !== undefined;
|
||||
const debug = createDebug('d3d:store');
|
||||
|
||||
// create router middleware that handles route actions
|
||||
const reduxRouterMiddleware = routerMiddleware(platform === 'ios-app' ? hashHistory : browserHistory);
|
||||
|
||||
export default function configureStore(reducers, initialState) {
|
||||
let store;
|
||||
|
||||
let middleware = [
|
||||
reduxRouterMiddleware,
|
||||
thunkMiddleware,
|
||||
promiseMiddleware(),
|
||||
promptMiddleware(),
|
||||
makePersistantMiddleware(state => state.localStore, LOCAL_STORE_READ),
|
||||
debugOverlappingDispatches()
|
||||
];
|
||||
|
||||
if (devMode) {
|
||||
// add development middleware
|
||||
const logger = createLogger({
|
||||
predicate: (_, action) => action.log !== false,
|
||||
collapsed: true,
|
||||
logErrors: false
|
||||
});
|
||||
middleware = [
|
||||
...middleware,
|
||||
logger
|
||||
];
|
||||
}
|
||||
store = createStore(
|
||||
reducers,
|
||||
initialState,
|
||||
compose( // compose store enhancers
|
||||
applyMiddleware(...middleware)
|
||||
)
|
||||
);
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.accept('./reducers/index.js', () => {
|
||||
const reducer = require('./reducers/index.js').default;
|
||||
debug('Reducers changed, replacing reducers of store');
|
||||
store.replaceReducer(reducer);
|
||||
});
|
||||
}
|
||||
|
||||
return store;
|
||||
}
|
134
src/js/utils/asyncActionUtils.js
Normal file
@ -0,0 +1,134 @@
|
||||
|
||||
import createDebug from 'debug';
|
||||
const debug = createDebug('d3d:util:asyncActions');
|
||||
|
||||
const defaultSuffixes = ['PENDING', 'FULFILLED', 'REJECTED'];
|
||||
|
||||
/*
|
||||
* Some utils below are quite similar to the following util:
|
||||
* https://github.com/rjbma/redux-promise-reducer
|
||||
*/
|
||||
|
||||
export function createPromiseActionCreators(
|
||||
instance,
|
||||
fnNames,
|
||||
promiseWrapper,
|
||||
actionPrefix = '',
|
||||
suffixes = defaultSuffixes
|
||||
) {
|
||||
let actionCreators = {};
|
||||
for (const fnName of fnNames) {
|
||||
actionCreators = {
|
||||
...actionCreators,
|
||||
...createPromiseActionCreator(instance, fnName, promiseWrapper, actionPrefix, suffixes)
|
||||
};
|
||||
}
|
||||
return actionCreators;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an actionCreator that returns an
|
||||
* redux-promise-middleware compatible action with:
|
||||
* - type based on (prefix+) function name and
|
||||
* - the promise the function returns
|
||||
*/
|
||||
export function createPromiseActionCreator(
|
||||
instance,
|
||||
fnName,
|
||||
promiseWrapper,
|
||||
actionPrefix = '',
|
||||
suffixes = defaultSuffixes
|
||||
) {
|
||||
const fn = instance[fnName];
|
||||
if (fn === undefined) {
|
||||
throw new Error(`Given instance doesn't have a function named '${fnName}'`);
|
||||
}
|
||||
actionPrefix = actionPrefix ? `${actionPrefix}_` : '';
|
||||
const fnNameUpper = fnName.toUpperCase();
|
||||
|
||||
const pendingType = actionPrefix + fnNameUpper + '_' + suffixes[0];
|
||||
const fulfilledType = actionPrefix + fnNameUpper + '_' + suffixes[1];
|
||||
const rejectedType = actionPrefix + fnNameUpper + '_' + suffixes[2];
|
||||
|
||||
return {
|
||||
[fnName]: function actionCreator() {
|
||||
debug('actionCreator called: ', fnName, arguments);
|
||||
const promise = fn.apply(instance, arguments);
|
||||
return {
|
||||
type: actionPrefix + fnNameUpper,
|
||||
payload: {
|
||||
promise: promiseWrapper ? promiseWrapper(promise, fnName, arguments) : promise,
|
||||
data: arguments // send as payload with pending action
|
||||
},
|
||||
// include arguments to meta so it's also included in
|
||||
// the fulfilled and rejected action types
|
||||
meta: {
|
||||
args: arguments
|
||||
}
|
||||
};
|
||||
},
|
||||
[pendingType]: pendingType,
|
||||
[fulfilledType]: fulfilledType,
|
||||
[rejectedType]: rejectedType
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a reducer that manages state for async actions
|
||||
*/
|
||||
export function createAsyncActionsReducer(pendingType, fulfilledType, rejectedType) {
|
||||
return function reducer(
|
||||
state = {
|
||||
pending: false,
|
||||
error: null,
|
||||
data: null
|
||||
},
|
||||
action
|
||||
) {
|
||||
switch (action.type) {
|
||||
case pendingType:
|
||||
return {
|
||||
pending: true,
|
||||
data: null,
|
||||
error: null
|
||||
};
|
||||
case fulfilledType:
|
||||
return {
|
||||
pending: false,
|
||||
data: action.payload,
|
||||
error: null
|
||||
};
|
||||
case rejectedType:
|
||||
return {
|
||||
pending: false,
|
||||
data: null,
|
||||
error: action.payload
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* creates a reducers for a list of names
|
||||
* result is usable for combineReducers()
|
||||
*/
|
||||
export function createAsyncActionsReducers(
|
||||
prefix,
|
||||
names,
|
||||
// default suffixes from redux-promise-middleware:
|
||||
suffixes = defaultSuffixes
|
||||
) {
|
||||
const reducers = {};
|
||||
prefix = prefix ? `${prefix}_` : '';
|
||||
|
||||
for (const name of names) {
|
||||
const nameUpper = name.toUpperCase();
|
||||
const pendingType = prefix + nameUpper + '_' + suffixes[0];
|
||||
const fulfilledType = prefix + nameUpper + '_' + suffixes[1];
|
||||
const rejectedType = prefix + nameUpper + '_' + suffixes[2];
|
||||
reducers[name] = createAsyncActionsReducer(pendingType, fulfilledType, rejectedType);
|
||||
}
|
||||
return reducers;
|
||||
}
|
11
src/js/utils/contentUtils.js
Normal file
@ -0,0 +1,11 @@
|
||||
import { platform } from 'src/js/constants/envs.js';
|
||||
|
||||
export function processContent(text) {
|
||||
if (platform === 'ios-app') text = makeImagesRelative(text);
|
||||
return text;
|
||||
}
|
||||
|
||||
const MAKE_IMAGES_RELATIVE_REG_EXP = /src="\/([^"]+)"/g;
|
||||
function makeImagesRelative(text) {
|
||||
return text.replace(MAKE_IMAGES_RELATIVE_REG_EXP, 'src="./$1"');
|
||||
}
|
16
src/js/utils/debugOverlappingDispatches.js
Normal file
@ -0,0 +1,16 @@
|
||||
export default function debugOverlappingDispatches() {
|
||||
let isDispatching = false;
|
||||
let dispatchingAction;
|
||||
return function middleware() {
|
||||
return next => action => {
|
||||
if (isDispatching) {
|
||||
console.log(`Overlapping dispatch: ${action.type} during ${dispatchingAction.type}`);
|
||||
}
|
||||
isDispatching = true;
|
||||
dispatchingAction = action;
|
||||
next(action);
|
||||
isDispatching = false;
|
||||
dispatchingAction = null;
|
||||
};
|
||||
};
|
||||
}
|
36
src/js/utils/formUtils.js
Normal file
@ -0,0 +1,36 @@
|
||||
import { SubmissionError } from 'redux-form';
|
||||
// import createDebug from 'debug';
|
||||
// const debug = createDebug('d3d:utils:form');
|
||||
|
||||
// redux form submit promise wrapper
|
||||
// - turns all errors into SubmissionError
|
||||
// - fills _error field, enables showing main error message in form
|
||||
// - includes superlogin's validationErrors
|
||||
export function formPromiseWrapper(promise) {
|
||||
return new Promise((resolve, reject) => {
|
||||
promise
|
||||
.then(resolve)
|
||||
.catch(err => {
|
||||
// axius (used by superlogin http utils) puts response data in response.data
|
||||
if (err.response && err.response.data) {
|
||||
err = err.response.data;
|
||||
}
|
||||
|
||||
// promise middleware puts actual error in reason
|
||||
if (err.action && err.reason) {
|
||||
err = err.reason;
|
||||
}
|
||||
const submissionErrors = {
|
||||
_error: err.message || err.error
|
||||
};
|
||||
// add superlogin validation errors
|
||||
// joined because superlogin creates arrays per field
|
||||
for (const prop in err.validationErrors) {
|
||||
submissionErrors[prop] = err.validationErrors[prop].join(', ');
|
||||
}
|
||||
const submissionError = new SubmissionError(submissionErrors);
|
||||
|
||||
reject(submissionError);
|
||||
});
|
||||
});
|
||||
}
|
11
src/js/utils/getHotReloadStore.js
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Utility to store data over hot reloads.
|
||||
*/
|
||||
|
||||
const globalStore = {};
|
||||
export default function getHotReloadStore(key) {
|
||||
if (globalStore[key] === undefined) {
|
||||
globalStore[key] = {};
|
||||
}
|
||||
return globalStore[key];
|
||||
}
|
20
src/js/utils/rapidActionFilter.js
Normal file
@ -0,0 +1,20 @@
|
||||
export default function createRapidActionFilter(threshold) {
|
||||
// ignore rapid action types that are the same
|
||||
let ignoreRapid = false;
|
||||
let prevActionType;
|
||||
return function rapidActionFilter(action) {
|
||||
if (action.type !== prevActionType) {
|
||||
ignoreRapid = false;
|
||||
prevActionType = action.type;
|
||||
return true;
|
||||
}
|
||||
if (ignoreRapid) {
|
||||
return false;
|
||||
}
|
||||
ignoreRapid = true;
|
||||
setTimeout(() => {
|
||||
ignoreRapid = false;
|
||||
}, threshold);
|
||||
return true;
|
||||
};
|
||||
}
|
26
src/js/utils/reactRouterUtils.js
Normal file
@ -0,0 +1,26 @@
|
||||
// import createDebug from 'debug';
|
||||
// const debug = createDebug('d3d:util:react-router');
|
||||
|
||||
// Get parent pathname of component (not current full url)
|
||||
// This enables going to the parent of an component,
|
||||
// even when a sub route is open
|
||||
// routes and route are properties given to all
|
||||
// react-router's Router children
|
||||
export function getParentPathName(routes, route) {
|
||||
const parentRoutes = [];
|
||||
for (const r of routes) {
|
||||
if (r === route) break;
|
||||
else parentRoutes.push(r);
|
||||
}
|
||||
return getPathName(parentRoutes);
|
||||
}
|
||||
|
||||
// transform routes object into pathName
|
||||
export function getPathName(routes) {
|
||||
const path = routes.map(r => r.path);
|
||||
let pathName = path.join('/');
|
||||
// replace 2 or more /'s with one /'
|
||||
pathName = pathName.replace(/\/{2,}/, '/');
|
||||
if (pathName === '') pathName = '/';
|
||||
return pathName;
|
||||
}
|
32
src/js/utils/saveUtils.js
Normal file
@ -0,0 +1,32 @@
|
||||
import { generateThumb } from '@doodle3d/doodle3d-core/lib/utils/generateThumb.js';
|
||||
import sketchDataToJSON from '@doodle3d/doodle3d-core/lib/shape/sketchDataToJSON';
|
||||
import { VERSION } from '@doodle3d/doodle3d-core/lib/constants/general.js';
|
||||
|
||||
export const THUMBNAIL_WIDTH = 240 * 2; // multiply times 2 because retina
|
||||
export const THUMBNAIL_HEIGHT = 200 * 2;
|
||||
export async function sketchDataToDoc(name, sketcherState) {
|
||||
const imgBlob = await generateThumb(sketcherState, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, 'blob');
|
||||
const sketchData = JSON.stringify(sketchDataToJSON(sketcherState));
|
||||
const sketchBlob = new Blob([sketchData], { type: 'application/json' });
|
||||
|
||||
return {
|
||||
name,
|
||||
appVersion: VERSION,
|
||||
_attachments: {
|
||||
img: { content_type: imgBlob.type, data: imgBlob },
|
||||
sketch: { content_type: sketchBlob.type, data: sketchBlob }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createUniqueName(name, names) {
|
||||
names = names.map(str => str.toUpperCase());
|
||||
|
||||
if (!names.includes(name.toUpperCase())) return name;
|
||||
|
||||
let counter = 1;
|
||||
while (names.includes(`${name.toUpperCase()} (${counter})`)) {
|
||||
counter ++;
|
||||
}
|
||||
return `${name} (${counter})`;
|
||||
}
|
86
src/js/utils/utils.js
Normal file
@ -0,0 +1,86 @@
|
||||
import createDebug from 'debug';
|
||||
// const debug = createDebug('d3d:util:util');
|
||||
|
||||
export function getDiff(a, b, path = '/', result = '') {
|
||||
if (a !== b) {
|
||||
if (path.length === 1) {
|
||||
result += 'diff:';
|
||||
}
|
||||
result += `\n ${path}: ${a}, ${b}`;
|
||||
}
|
||||
if (a && b && typeof a === 'object' && typeof b === 'object') {
|
||||
const allKeys = uniq(Object.keys(a).concat(Object.keys(b)));
|
||||
for (let key of allKeys) {
|
||||
const seperator = (path.length > 1) ? '.' : '';
|
||||
const subPath = `${path}${seperator}${key}`;
|
||||
result += getDiff(a[key], b[key], subPath);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function uniq(array) {
|
||||
const seen = {};
|
||||
const out = [];
|
||||
const len = array.length;
|
||||
let j = 0;
|
||||
for (let i = 0; i < len; i++) {
|
||||
const item = array[i];
|
||||
if (seen[item] !== 1) {
|
||||
seen[item] = 1;
|
||||
out[j++] = item;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function loggedReducer(reducer) {
|
||||
const reducerDebug = createDebug(`d3d:reducer:${reducer.name}:`);
|
||||
return (state, action) => {
|
||||
if (action.log !== false && action.type) reducerDebug(action.type);
|
||||
return logResult(reducerDebug, ' newState: ', reducer(state, action));
|
||||
};
|
||||
}
|
||||
|
||||
export function logResult(debugInstance, text, value) {
|
||||
debugInstance(text, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
export function createQuery(properties, joinString = '&', identifierString = '=') {
|
||||
return Object.entries(properties)
|
||||
.map(([key, value]) => `${key}${identifierString}${String(value)}`)
|
||||
.join(joinString);
|
||||
}
|
||||
|
||||
export function extractQuery(queryString, joinString = '&', identifierString = '=') {
|
||||
return queryString.split('?')[1].split(joinString)
|
||||
.map(entry => entry.split(identifierString))
|
||||
.reduce((query, [key, value]) => {
|
||||
query[key] = decodeURIComponent(value);
|
||||
return query;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export const isValidNumber = (num) => typeof num === 'number' && !isNaN(num);
|
||||
|
||||
// remove inline authentication from url
|
||||
export function removeAuthFromURL(url) {
|
||||
return url.replace(/(https?:\/\/)[^@]*@/, '$1');
|
||||
}
|
||||
|
||||
export function getErrorMessage(err) {
|
||||
if (typeof err.reason === 'string') {
|
||||
return err.reason;
|
||||
} else {
|
||||
const { error, message } = err.reason;
|
||||
const parts = [];
|
||||
if (error) parts.push(error);
|
||||
if (message) parts.push(message);
|
||||
return parts.join(': ');
|
||||
}
|
||||
}
|
||||
|
||||
export function forceResize() {
|
||||
requestAnimationFrame(() => window.dispatchEvent(new Event('resize')));
|
||||
}
|
27
src/jss/textMarkup.js
Normal file
@ -0,0 +1,27 @@
|
||||
export default {
|
||||
text: {
|
||||
lineHeight: '1.5',
|
||||
fontWeight: 'normal',
|
||||
'& h1, & h2, & h3, & p, & ol, & ul': {
|
||||
fontWeight: 'inherit',
|
||||
margin: '0.5em 0'
|
||||
},
|
||||
'& h1': {
|
||||
lineHeight: 1.2
|
||||
},
|
||||
'& ol, & ul': {
|
||||
padding: '0 0 0 1em'
|
||||
},
|
||||
'& img, & iframe': {
|
||||
maxWidth: '100%'
|
||||
},
|
||||
'& table': {
|
||||
wordWrap: 'break-word',
|
||||
width: '100%',
|
||||
tableLayout: 'fixed'
|
||||
},
|
||||
'& small': {
|
||||
fontWeight: 'lighter'
|
||||
}
|
||||
}
|
||||
};
|
206
webpack.config.js
Normal file
@ -0,0 +1,206 @@
|
||||
/* eslint no-console: 0, quote-props: 0*/
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const HTMLWebpackPlugin = require('html-webpack-plugin');
|
||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
|
||||
const devMode = process.env.NODE_ENV !== 'production';
|
||||
const analyzeBundle = process.env.ANALYZE_BUNDLE;
|
||||
// console.log(`Starting Webpack (devmode: ${devMode})`);
|
||||
|
||||
let devtool;
|
||||
if (devMode) {
|
||||
devtool = 'eval-source-map';
|
||||
} else {
|
||||
devtool = 'nosources-source-map';
|
||||
}
|
||||
|
||||
const babelLoader = {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [
|
||||
[require('babel-preset-env'), {
|
||||
targets: {
|
||||
browsers: devMode ?
|
||||
['last 1 Chrome versions', 'last 1 Firefox versions'] :
|
||||
['last 2 versions', 'safari >= 7', 'not ie < 11']
|
||||
},
|
||||
modules: false, // keeping the esm module syntax enables the minifier to go through the modules
|
||||
loose: true,
|
||||
debug: !devMode // log targets when creating dist
|
||||
}],
|
||||
require('babel-preset-react'),
|
||||
],
|
||||
plugins: [
|
||||
require('babel-plugin-transform-object-rest-spread'), // transpile spread operator for objects
|
||||
require('babel-plugin-transform-class-properties'), // transpile class properties
|
||||
require('babel-plugin-transform-es2015-classes'), // react-hot-loader always needs this, see: https://github.com/gaearon/react-hot-loader/issues/313
|
||||
require('babel-plugin-syntax-dynamic-import'), // enable dynamic imports (lazy loading)
|
||||
require('babel-plugin-transform-runtime'),
|
||||
...(devMode ? [
|
||||
require('react-hot-loader/babel')
|
||||
] : [
|
||||
// require('babel-plugin-ramda'), // improve dead code elimination for ramda
|
||||
require('babel-plugin-lodash'), // improve dead code elimination for lodash
|
||||
[require('babel-plugin-transform-imports'), {
|
||||
// improve dead code elimination for material-ui
|
||||
'material-ui': { transform: 'material-ui/${member}', preventFullImport: true }
|
||||
}]
|
||||
])
|
||||
],
|
||||
babelrc: false
|
||||
}
|
||||
};
|
||||
|
||||
const cssModuleLoader = {
|
||||
loader: 'css-loader',
|
||||
query: {
|
||||
modules: true,
|
||||
localIdentName: '[name]__[local]___[hash:base64:5]'
|
||||
}
|
||||
};
|
||||
|
||||
const workerLoader = {
|
||||
loader: 'worker-loader',
|
||||
options: {
|
||||
inline: false, // sepererate files will still be created as fallback
|
||||
name: '[name].worker.js'
|
||||
}
|
||||
};
|
||||
|
||||
const imgLoader = [{
|
||||
loader: 'file-loader',
|
||||
options: { name: '[path][name].[ext]' }
|
||||
}];
|
||||
|
||||
if (!devMode) {
|
||||
const imageCompressor = {
|
||||
loader: 'image-webpack-loader',
|
||||
options: {
|
||||
mozjpeg: { progressive: true, quality: 65 },
|
||||
optipng: { enabled: false },
|
||||
pngquant: { quality: '65-90', speed: 4 }
|
||||
}
|
||||
};
|
||||
|
||||
imgLoader.push(imageCompressor);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
app: [
|
||||
...(devMode ? [
|
||||
'webpack-hot-middleware/client?reload=true', // hot middleware client
|
||||
'react-hot-loader/patch'
|
||||
] : []),
|
||||
'./src/js/preloader.js'
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
'NODE_ENV': JSON.stringify(process.env.NODE_ENV),
|
||||
'PLATFORM': JSON.stringify(process.env.PLATFORM),
|
||||
}
|
||||
}),
|
||||
...(analyzeBundle ? [new BundleAnalyzerPlugin()] : [
|
||||
new HTMLWebpackPlugin({
|
||||
template: require('html-webpack-template'),
|
||||
filename: 'index.html',
|
||||
title: 'Doodle3D Transform',
|
||||
appMountId: 'app',
|
||||
inject: false,
|
||||
mobile: true,
|
||||
minify: !devMode && { html5: true, collapseWhitespace: true },
|
||||
hash: !devMode,
|
||||
favicon: 'favicon.ico',
|
||||
chunks: ['app'],
|
||||
meta: [
|
||||
{ 'http-equiv': 'Content-Type', content: 'text/html; charset=utf-8' },
|
||||
{ name: 'apple-mobile-web-app-capable', content: 'yes' },
|
||||
{ name: 'apple-mobile-web-app-status-bar-style', content: 'black' },
|
||||
{ name: 'mobile-web-app-capable', content: 'yes' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, minimal-ui, user-scalable=no' }
|
||||
]
|
||||
}),
|
||||
new HTMLWebpackPlugin({
|
||||
template: require('html-webpack-template'),
|
||||
filename: '404.html',
|
||||
title: 'Page not found',
|
||||
inject: false,
|
||||
mobile: true,
|
||||
bodyHtmlSnippet: 'Not found',
|
||||
chunks: []
|
||||
})
|
||||
]),
|
||||
...(devMode ? [
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new webpack.NamedModulesPlugin()
|
||||
// new BundleAnalyzerPlugin()
|
||||
] : [])
|
||||
],
|
||||
output: {
|
||||
filename: '[name].bundle.js',
|
||||
chunkFilename: '[name].chunk.js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
publicPath: '/'
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'src': path.resolve(__dirname, 'src/'),
|
||||
'data': path.resolve(__dirname, 'data/'),
|
||||
'img': path.resolve(__dirname, 'img/'),
|
||||
'workers': path.resolve(__dirname, 'workers/'),
|
||||
'server': path.resolve(__dirname, 'server/'),
|
||||
'CHANGELOG.md': path.resolve(__dirname, 'CHANGELOG.md'),
|
||||
'superlogin-client': '@doodle3d/superlogin-client',
|
||||
'cal': '@doodle3d/cal',
|
||||
'threejs-export-stl': '@doodle3d/threejs-export-stl',
|
||||
'threejs-export-obj': '@doodle3d/threejs-export-obj',
|
||||
'redux-batched-subscribe': '@doodle3d/redux-batched-subscribe',
|
||||
'redux-prompt': `@doodle3d/redux-prompt/lib`
|
||||
}
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
use: [babelLoader]
|
||||
}, {
|
||||
test: /\.css$/,
|
||||
exclude: /src\/css\/.+\.css$/,
|
||||
use: ['style-loader', 'css-loader']
|
||||
}, { // css modules
|
||||
test: /src\/css\/.+\.css$/,
|
||||
use: ['style-loader', cssModuleLoader]
|
||||
}, {
|
||||
test: /\.(png|jpeg|jpg|gif)$/,
|
||||
use: imgLoader
|
||||
}, {
|
||||
test: /\.(woff)$/,
|
||||
use: {
|
||||
loader: 'file-loader'
|
||||
}
|
||||
}, {
|
||||
test: /\.(svg|glsl|txt|md)$/,
|
||||
use: 'raw-loader'
|
||||
}, {
|
||||
test: /\.json$/,
|
||||
use: 'json-loader'
|
||||
}, {
|
||||
test: /\.yml$/,
|
||||
use: 'yml-loader'
|
||||
}, { // web workers
|
||||
test: /\.worker.js$/,
|
||||
use: [workerLoader, babelLoader]
|
||||
}, { // make THREE global available to three.js examples
|
||||
test: /three\/examples\/.+\.js/,
|
||||
use: 'imports-loader?THREE=three'
|
||||
}
|
||||
]
|
||||
},
|
||||
// Source map creation
|
||||
// https://webpack.js.org/configuration/devtool/
|
||||
devtool
|
||||
};
|