Open-sourcing Doodle3D Transform. Enjoy!

This commit is contained in:
Rick Companje 2021-05-30 22:32:59 +02:00
parent 49a5b6658e
commit b45e3ccae7
78 changed files with 37697 additions and 2 deletions

33
.eslintrc Normal file
View 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
View 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
View File

@ -0,0 +1,13 @@
{
"ecmaVersion": 6,
"libs": [
"browser"
],
"loadEagerly": [
"js/app.js"
],
"plugins": {
"modules": {},
"es_modules": {}
}
}

123
ADD_OBJECT.md Normal file
View 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

File diff suppressed because it is too large Load Diff

22
LICENSE.md Normal file
View 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.

View File

@ -1,2 +1,73 @@
# Doodle3D-Transform
Official repository of the Doodle3D Transform web-app
# Doodle3D-App
Doodle3D Transform is a free and open-source web-app that makes designing in 3D easy and fun! Created with love by Casper, Peter, Rick, Nico, Jeroen, Simon, Donna and Arne. With the support of 1,626 Kickstarter backers.
As of 2021-05-26 Doodle3D Transform is distributed under the MIT License. This gives everyone the freedoms to use Doodle3D Transform in any context: commercial or non-commercial, public or private, open or closed source.
![Screenshot](screenshot.png)
## Prerequisites
- Install [Node.js](https://nodejs.org/en/) v6 LTS (v8 is not supported yet).
- Update NPM to at least v5.
`$ npm install npm@latest -g`
## Install
- Clone our repo.
`$ git clone git@github.com:Doodle3D/Doodle3D-App.git`
- Go into it.
`$ cd Doodle3D-App`
- Install all dependencies
`$ npm install`
- Setup environment variables, see below.
## Run
Start the server, you can visit it at http://localhost:8080
```
npm start
```
## Create distribution
To create a optimized version of our app that's ready for deployment run:
```
npm run dist
```
To test this distribution start our app in production mode by running:
```
npm run start-production
```
## Build & Run as App
[Cordova](https://cordova.apache.org/) is used to build the app. Before building with Cordova, it needs to be installed.
```
npm run cordova:install
```
- **Run iOS** (mac only)
Build app and deploy to ios device:
`npm run ios`
- **Emulate iOS** (mac only)
Build app and start iOS emulator:
`npm run ios-emulate [-- -{platform}]`
- **Run Android**
Build app and deploy on device or emulator:
`npm run android`
- **Run Windows** (win only)
Build app and deploy on device:
`npm run windows`
## Import / Export dev tool
In `Developer tools` > `Console`:
- `files.exportFile(name)`
- `name`: name of file to export
```
files.exportFile('star')
```
- `files.importFile(data, name)`
- `data`: result of `exportFile` between single quotes
- `name`: override name of file (optionally)
```
files.importFile('{"data":[{"height":40,"transform":{"metadata":{"library":"CAL","type":"Matrix"},"matrix":[1,0,-7.73286467486821,0,1,18.980667838312854]},"sculpt":[1,1,1],"cut":[true,true],"twist":0,"type":"STAR","star":{"rays":5,"innerRadius":25,"outerRadius":53}}],"_attachments":{"img":{"content_type":"image/jpeg","digest":"md5-NOT2LxdCerpIrG/7vEczDw==","length":7442,"stub":true}},"_id":"star","_rev":"3-6ea2bc9e62628a25078d9d8d90aff151"}','imported star')
```
- `files.loadAllNames()`
Get a list of the names all saves sketches

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 B

BIN
favicon_alt.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

BIN
img/btnMollie.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
img/btnPayPal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
img/heart.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
img/menu/btnExport.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
img/menu/btnExportSmall.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
img/menu/btnHelp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
img/menu/btnHelpSmall.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
img/menu/btnLove.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
img/menu/btnLoveSmall.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
img/menu/btnMenu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
img/menu/btnMenuSmall.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
img/menu/btnNew.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
img/menu/btnNewSmall.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
img/menu/btnOpen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

BIN
img/menu/btnOpenSmall.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
img/menu/btnSave.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

BIN
img/menu/btnSaveSmall.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

46
licenses-to-json.js Normal file
View 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

File diff suppressed because it is too large Load Diff

130
package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

30
src/js/Root.js Normal file
View 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>
);

View 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
View 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
View 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
View 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';

View 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
View 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}`
})
});

View 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
View 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);

View File

@ -0,0 +1,4 @@
import { connect } from 'react-redux';
import Notifications from 'react-notification-system-redux';
export default connect(({ notifications }) => ({ notifications }))(Notifications);

View 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
View 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));

View 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
View 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;

View 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
View 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));

View 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> | &nbsp;
<Link to={'/licenses'}>Licenses</Link> | &nbsp;
<Link to={'/help'}>Help</Link> | &nbsp;
<Link to={'/donate'}>Donate</Link> | &nbsp;
<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));

View 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);

View 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>
&nbsp;&nbsp;
<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));

View 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));

View 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));

View 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));

View 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));

View 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)));

View 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
View 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
View 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
View 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();
});

View 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;
}
}

View 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
View 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
View 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 };

View 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
View 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;
}

View 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;
}

View 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"');
}

View 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
View 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);
});
});
}

View 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];
}

View 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;
};
}

View 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
View 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
View 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
View 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
View 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
};