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
|
# Doodle3D-App
|
||||||
Official repository of the Doodle3D Transform web-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
|
||||||
|
};
|