implement local storage

This commit is contained in:
casperlamboo 2018-01-16 17:57:34 +01:00
parent 9d47e8dc23
commit 7b59ba1108
7 changed files with 512 additions and 629 deletions

674
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -21,12 +21,16 @@
"file-saver": "^1.3.3",
"lodash": "^4.17.4",
"material-ui": "^0.19.4",
"material-ui-icons": "^1.0.0-beta.17",
"material-ui-textfield-icon": "^0.2.2-1",
"proptypes": "^1.1.0",
"query-string": "^5.0.1",
"react": "^16.0.0",
"react-addons-update": "^15.6.2",
"react-dom": "^16.0.0",
"react-jss": "^7.2.0",
"react-resize-detector": "^1.1.0",
"shortid": "^2.2.8",
"three": "^0.88.0"
},
"devDependencies": {

View File

@ -1,2 +1,3 @@
export const PRECISION = 0.01;
export const VERSION = '0.0.18';
export const LOCAL_STORAGE_KEY = 'PRINTER_SETTINGS';

View File

@ -4,36 +4,58 @@ import _ from 'lodash';
import injectSheet from 'react-jss';
import MaterialUISelectField from 'material-ui/SelectField'
import MaterialUICheckbox from 'material-ui/Checkbox';
import MaterialUITextField from 'material-ui/TextField';
import { blue500, grey500 } from 'material-ui/styles/colors';
import TextFieldIcon from 'material-ui-textfield-icon';
import Clear from 'material-ui-icons/Clear';
const contextTypes = { state: PropTypes.object, onChange: PropTypes.func, disabled: PropTypes.bool };
const contextTypes = {
settings: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
disabled: PropTypes.bool.isRequired,
addPrinter: PropTypes.object.isRequired,
advancedFields: PropTypes.array.isRequired,
activePrinter: PropTypes.string
};
const propTypes = {
name: PropTypes.string.isRequired
};
export const SelectField = (props, context) => (
<MaterialUISelectField
{ ...props }
{...props}
disabled={context.disabled}
value={_.get(context.state, props.name)}
value={_.get(context, props.name)}
onChange={(event, index, value) => context.onChange(props.name, value)}
/>
);
SelectField.contextTypes = contextTypes;
SelectField.propTypes = propTypes;
export const TextField = (props, context) => (
<MaterialUITextField
{ ...props }
<TextFieldIcon
{...props}
icon={context.advancedFields.includes(props.name) && <Clear onTouchTap={() => context.onChange(props.name, null)} />}
floatingLabelStyle={{ color: context.advancedFields.includes(props.name) ? blue500 : grey500 }}
disabled={context.disabled}
value={_.get(context.state, props.name)}
onChange={(event, value) => context.onChange(props.name, value)}
value={_.get(context, props.name)}
onChange={(event, value) => context.onChange(props.name, props.type === 'number' ? parseFloat(value) : value)}
/>
);
TextField.contextTypes = contextTypes;
TextField.propTypes = propTypes;
export const Checkbox = (props, context) => (
<span style={{ display: 'flex' }}>
<MaterialUICheckbox
{ ...props }
{...props}
style={{ display: 'block' }}
iconStyle={{ fill: context.advancedFields.includes(props.name) ? blue500 : grey500 }}
disabled={context.disabled}
checked={_.get(context.state, props.name)}
checked={_.get(context, props.name)}
onCheck={(event, value) => context.onChange(props.name, value)}
/>
{context.advancedFields.includes(props.name) && <Clear onTouchTap={() => context.onChange(props.name, null)} />}
</span>
);
Checkbox.contextTypes = contextTypes;
Checkbox.propTypes = propTypes;

View File

@ -5,7 +5,17 @@ import { Tabs, Tab } from 'material-ui/Tabs';
import MenuItem from 'material-ui/MenuItem';
import injectSheet from 'react-jss';
import { SelectField, TextField, Checkbox } from './FormComponents.js';
import { grey800, cyan500 } from 'material-ui/styles/colors';
import { grey800, cyan500, red500 } from 'material-ui/styles/colors';
import Divider from 'material-ui/Divider';
import Dialog from 'material-ui/Dialog';
import FlatButton from 'material-ui/FlatButton';
import { LOCAL_STORAGE_KEY } from '../constants.js';
import shortid from 'shortid';
import defaultSettings from '../settings/default.yml';
import printerSettings from '../settings/printer.yml';
import materialSettings from '../settings/material.yml';
import qualitySettings from '../settings/quality.yml';
import update from 'react-addons-update';
const styles = {
textFieldRow: {
@ -19,74 +29,232 @@ const styles = {
fontWeight: 'bold',
margin: '30px 0 0 0'
}
},
error: {
color: red500
}
};
const getLocalStorage = () => {
let localStorage = window.localStorage.getItem(LOCAL_STORAGE_KEY);
if (!localStorage) {
localStorage = { printers: {}, active: null };
updateLocalStorage(localStorage);
} else {
localStorage = JSON.parse(localStorage);
}
return localStorage;
};
const updateLocalStorage = (localStorage) => {
window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(localStorage));
};
class Settings extends React.Component {
static childContextTypes = { state: PropTypes.object, onChange: PropTypes.func, disabled: PropTypes.bool };
static defaultProps: {
disabled: false
};
static propTypes = {
classes: PropTypes.objectOf(PropTypes.string),
onChange: PropTypes.func,
printers: PropTypes.object.isRequired,
defaultPrinter: PropTypes.string,
quality: PropTypes.object.isRequired,
defaultQuality: PropTypes.string.isRequired,
material: PropTypes.object.isRequired,
defaultMaterial: PropTypes.string.isRequired,
initialSettings: PropTypes.object.isRequired,
disabled: PropTypes.bool.isRequired
};
constructor(props) {
super();
this.state = {
settings: props.initialSettings,
printers: props.defaultPrinter,
quality: props.defaultQuality,
material: props.defaultMaterial
static defaultProps: {
disabled: false
};
static childContextTypes = {
settings: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
disabled: PropTypes.bool.isRequired,
addPrinter: PropTypes.object.isRequired,
activePrinter: PropTypes.string,
advancedFields: PropTypes.array.isRequired
};
state = {
localStorage: getLocalStorage(),
addPrinter: {
open: false,
name: '',
printer: '',
error: null
}
};
componentDidMount() {
const { onChange } = this.props;
const { localStorage } = this.state;
if (localStorage.active) {
if (onChange) onChange(this.constructSettings(localStorage));
} else {
this.openAddPrinterDialog();
}
}
changeSettings = (fieldName, value) => {
const { onChange } = this.props;
const { localStorage } = this.state;
let state;
switch (fieldName) {
case 'printers':
case 'quality':
case 'material':
state = {
[fieldName]: value,
settings: _.merge({}, this.state.settings, this.props[fieldName][value])
let state = _.cloneDeep(this.state);
const removeAddPrinterError = () => {
state = update(state, { addPrinter: { error: { $set: null } } });
};
switch (fieldName) {
case 'addPrinter.printer':
state = update(state, { addPrinter: { printer: { $set: value } } });
state = update(state, { addPrinter: { name: { $set: printerSettings[value].title } } });
removeAddPrinterError();
break;
case 'addPrinter.name':
state = update(state, { addPrinter: { name: { $set: value } } });
removeAddPrinterError();
break;
case 'activePrinter':
if (value !== 'add_printer') state = update(state, { localStorage: { active: { $set: value } } });
break;
case 'settings.quality':
case 'settings.material':
if (!localStorage.active) return this.openAddPrinterDialog();
state = _.set(state, `localStorage.printers[${localStorage.active}].${fieldName}`, value);
break;
case 'settings.layerHeight':
case 'settings.dimensions.x':
case 'settings.dimensions.y':
case 'settings.dimensions.z':
case 'settings.nozzleDiameter':
case 'settings.bedTemperature':
case 'settings.heatedBed':
case 'settings.filamentThickness':
case 'settings.temperature':
case 'settings.thickness.top':
case 'settings.thickness.bottom':
case 'settings.thickness.shell':
case 'settings.retraction.enabled':
case 'settings.retraction.amount':
case 'settings.retraction.speed':
case 'settings.retraction.minDistance':
case 'settings.travel.speed':
case 'settings.combing':
case 'settings.innerShell.speed':
case 'settings.innerShell.flowRate':
case 'settings.outerShell.speed':
case 'settings.outerShell.flowRate':
case 'settings.innerInfill.gridSize':
case 'settings.innerInfill.speed':
case 'settings.innerInfill.flowRate':
case 'settings.outerInfill.speed':
case 'settings.outerInfill.flowRate':
case 'settings.brim.size':
case 'settings.brim.speed':
case 'settings.brim.flowRate':
case 'settings.firstLayer.speed':
case 'settings.firstLayer.flowRate':
if (!localStorage.active) return this.openAddPrinterDialog();
if (value === null) {
const advanced = { ...state.localStorage.printers[localStorage.active].settings.advanced };
delete advanced[fieldName];
state = update(state, { localStorage: { printers: { [localStorage.active]: { settings: { advanced: { $set: advanced } } } } } });
} else {
state = _.set(state, `localStorage.printers[${localStorage.active}].settings.advanced[${JSON.stringify(fieldName)}]`, value);
}
break;
default:
state = _.set(_.cloneDeep(this.state), fieldName, value);
break;
}
if (onChange) onChange(state);
if (state) this.setState(state);
};
getChildContext() {
return { state: this.state, onChange: this.changeSettings, disabled: this.props.disabled };
this.setState(state);
if (localStorage.active) {
if (onChange) onChange(this.constructSettings(state.localStorage));
updateLocalStorage(state.localStorage);
}
}
getChildContext() {
const { localStorage, addPrinter } = this.state;
return {
addPrinter,
activePrinter: localStorage.active,
advancedFields: localStorage.active ? Object.keys(localStorage.printers[localStorage.active].settings.advanced) : [],
settings: this.constructSettings(localStorage),
onChange: this.changeSettings,
disabled: this.props.disabled
};
}
constructSettings(localStorage) {
if (!localStorage.active) return defaultSettings;
const { printer, material, quality, advanced } = localStorage.printers[localStorage.active].settings;
let settings = {
...defaultSettings,
printer,
material,
quality
};
settings = _.merge({}, settings, printerSettings[printer]);
settings = _.merge({}, settings, qualitySettings[quality]);
settings = _.merge({}, settings, materialSettings[material]);
for (const key in advanced) {
const value = advanced[key];
settings = _.set(_.cloneDeep(settings), key.replace('settings.', ''), value);
}
return settings;
}
addPrinter = () => {
const { name, printer } = this.state.addPrinter;
if (!name || !printer) {
this.setState({ addPrinter: { ...this.state.addPrinter, error: 'Please enter a name and printer' } });
return;
}
const id = shortid.generate();
const localStorage = {
active: id,
printers: {
...this.state.localStorage.printers,
[id]: { name, settings: { printer, material: 'pla', quality: 'medium', advanced: {} } }
}
};
this.setState({ localStorage });
updateLocalStorage(localStorage);
this.closeAddPrinterDialog();
const { onChange } = this.props;
if (onChange) onChange(this.constructSettings(localStorage));
}
closeAddPrinterDialog = () => this.setAddPrinterDialog(false);
openAddPrinterDialog = () => this.setAddPrinterDialog(true);
setAddPrinterDialog = (open) => this.setState({ addPrinter: { name: '', printer: '', error: null, open } });
render() {
const { classes, printers, quality, material, disabled } = this.props;
const { addPrinter, localStorage } = this.state;
const { classes, disabled } = this.props;
return (
<div className={classes.container}>
<SelectField name="printers" floatingLabelText="Printer" fullWidth>
{Object.entries(printers).map(([value, { title }]) => (
<MenuItem key={value} value={value} primaryText={title} />
<SelectField name="activePrinter" floatingLabelText="Printer" fullWidth>
{Object.entries(localStorage.printers).map(([id, { name }]) => (
<MenuItem key={id} value={id} primaryText={name} />
))}
<Divider />
<MenuItem onTouchTap={this.openAddPrinterDialog} value="add_printer" primaryText="Add Printer" />
</SelectField>
<SelectField name="material" floatingLabelText="Material" fullWidth>
{Object.entries(material).map(([value, { title }]) => (
<SelectField name="settings.material" floatingLabelText="Material" fullWidth>
{Object.entries(materialSettings).map(([value, { title }]) => (
<MenuItem key={value} value={value} primaryText={title} />
))}
</SelectField>
@ -94,8 +262,8 @@ class Settings extends React.Component {
<Tabs inkBarStyle={{ backgroundColor: cyan500 }}>
<Tab buttonStyle={{ color: grey800, backgroundColor: 'white' }} label="Basic">
<div>
<SelectField name="quality" floatingLabelText="Quality" fullWidth>
{Object.entries(quality).map(([value, { title }]) => (
<SelectField name="settings.quality" floatingLabelText="Quality" fullWidth>
{Object.entries(qualitySettings).map(([value, { title }]) => (
<MenuItem key={value} value={value} primaryText={title} />
))}
</SelectField>
@ -123,8 +291,6 @@ class Settings extends React.Component {
<TextField name="settings.thickness.top" fullWidth floatingLabelText="top" type="number" />
<TextField name="settings.thickness.bottom" fullWidth floatingLabelText="bottom" type="number" />
<TextField name="settings.thickness.shell" fullWidth floatingLabelText="shell" type="number" />
<p>Combing</p>
<Checkbox name="settings.combing" label="Enabled" />
<p>Retraction</p>
<Checkbox name="settings.retraction.enabled" label="Enabled" />
<TextField name="settings.retraction.amount" fullWidth floatingLabelText="Amount" type="number" />
@ -156,6 +322,30 @@ class Settings extends React.Component {
</div>
</Tab>
</Tabs>
<Dialog
title="Add Printer"
open={addPrinter.open}
onRequestClose={this.closeAddPrinterDialog}
contentStyle={{ maxWidth: '400px' }}
actions={[
<FlatButton
label="Cancel"
onTouchTap={this.closeAddPrinterDialog}
/>,
<FlatButton
label="Add"
primary
onTouchTap={this.addPrinter}
/>
]}
>
<SelectField name="addPrinter.printer" floatingLabelText="Printer" fullWidth>
{Object.entries(printerSettings).map(([value, { title }]) => (
<MenuItem key={value} value={value} primaryText={title} /> ))}
</SelectField>
<TextField name="addPrinter.name" floatingLabelText="Name" fullWidth />
{addPrinter.error && <p className={classes.error}>{addPrinter.error}</p>}
</Dialog>
</div>
);
}

View File

@ -16,10 +16,6 @@ import Menu from 'material-ui/Menu';
import MenuItem from 'material-ui/MenuItem';
import { Tabs, Tab } from 'material-ui/Tabs';
import Settings from './Settings.js';
import defaultSettings from '../settings/default.yml';
import printerSettings from '../settings/printer.yml';
import materialSettings from '../settings/material.yml';
import qualitySettings from '../settings/quality.yml';
import ReactResizeDetector from 'react-resize-detector';
import JSONToSketchData from 'doodle3d-core/shape/JSONToSketchData';
import createSceneData from 'doodle3d-core/d3/createSceneData.js';
@ -91,63 +87,40 @@ class Interface extends React.Component {
PropTypes.string
]).isRequired,
classes: PropTypes.objectOf(PropTypes.string),
defaultSettings: PropTypes.object.isRequired,
printers: PropTypes.object.isRequired,
defaultPrinter: PropTypes.string,
quality: PropTypes.object.isRequired,
defaultQuality: PropTypes.string.isRequired,
material: PropTypes.object.isRequired,
defaultMaterial: PropTypes.string.isRequired,
pixelRatio: PropTypes.number.isRequired,
onCancel: PropTypes.func,
name: PropTypes.string.isRequired
};
static defaultProps = {
defaultSettings: defaultSettings,
printers: printerSettings,
quality: qualitySettings,
defaultQuality: 'medium',
material: materialSettings,
defaultMaterial: 'pla',
pixelRatio: 1,
name: 'Doodle3D'
};
constructor(props) {
super(props);
const { defaultPrinter, defaultQuality, defaultMaterial, printers, quality, material, defaultSettings } = props;
const scene = createScene(this.props);
this.state = {
scene,
settings: null,
showFullScreen: false,
isSlicing: false,
isLoading: true,
error: null,
printers: defaultPrinter,
quality: defaultQuality,
material: defaultMaterial,
popover: {
element: null,
open: false
},
settings: _.merge(
{},
defaultSettings,
printers[defaultPrinter],
quality[defaultQuality],
material[defaultMaterial]
)
}
};
}
componentDidMount() {
const { canvas } = this.refs;
const scene = createScene(canvas, this.props, this.state);
this.setState({ scene });
const { scene } = this.state;
scene.updateCanvas(canvas);
const { file } = this.props;
if (!file) {
throw new Error('no file provided');
} if (typeof file === 'string') {
@ -220,22 +193,20 @@ class Interface extends React.Component {
};
slice = async (target) => {
const { isSlicing, isLoading, settings, printers, quality, mesh, scene: { material, mesh: { matrix } } } = this.state;
const { isSlicing, isLoading, settings, mesh, scene: { material, mesh: { matrix } } } = this.state;
const { name } = this.props;
if (isSlicing || isLoading) return;
this.closePopover();
this.setState({ isSlicing: true, progress: { action: '', percentage: 0, step: 0 }, error: null });
const exportMesh = new Mesh(mesh.geometry, mesh.material);
exportMesh.applyMatrix(matrix);
try {
await slice(target, name, exportMesh, settings, printers, quality, material, progress => {
this.setState({ progress: { ...this.state.progress, ...progress } });
});
const updateProgres = progress => this.setState({ progress: { ...this.state.progress, ...progress } });
await slice(target, name, exportMesh, settings, updateProgres);
} catch (error) {
this.setState({ error: error.message });
throw error;
@ -263,23 +234,6 @@ class Interface extends React.Component {
});
};
onChangeSettings = (settings) => {
this.setState(settings);
};
componentWillUpdate(nextProps, nextState) {
if (!this.state.scene) return;
const { scene: { box, render, setSize } } = this.state;
let changed = false;
if (box && nextState.settings.dimensions !== this.state.settings.dimensions) {
const { dimensions } = nextState.settings;
box.scale.set(dimensions.y, dimensions.z, dimensions.x);
box.updateMatrix();
changed = true;
}
if (changed) render();
}
componentDidUpdate() {
const { scene: { updateCanvas } } = this.state;
const { canvas } = this.refs;
@ -298,9 +252,23 @@ class Interface extends React.Component {
this.setState({ showFullScreen: width > MAX_FULLSCREEN_WIDTH });
};
onChangeSettings = (settings) => {
const { scene: { box, render } } = this.state;
let changed = false;
if (!this.state.settings || this.state.settings.dimensions !== settings.dimensions) {
box.scale.set(settings.dimensions.y, settings.dimensions.z, settings.dimensions.x);
box.updateMatrix();
changed = true;
}
if (changed) render();
this.setState({ settings, error: null });
};
render() {
const { classes, defaultPrinter, defaultQuality, defaultMaterial, onCancel } = this.props;
const { isSlicing, isLoading, progress, settings, printers, quality, material, showFullScreen, error } = this.state;
const { classes, onCancel } = this.props;
const { isSlicing, isLoading, progress, showFullScreen, error } = this.state;
const disableUI = isSlicing || isLoading;
const style = { ...(showFullScreen ? {} : { maxWidth: 'inherit', width: '100%', height: '100%' }) };
@ -309,13 +277,6 @@ class Interface extends React.Component {
<div className={classes.settingsBar} style={style}>
<Settings
disabled={disableUI}
printers={printerSettings}
defaultPrinter={defaultPrinter}
quality={qualitySettings}
defaultQuality={defaultQuality}
material={materialSettings}
defaultMaterial={defaultMaterial}
initialSettings={settings}
onChange={this.onChangeSettings}
/>
<div className={classes.sliceActions}>

View File

@ -1,6 +1,7 @@
import * as THREE from 'three';
import { Box3 } from 'three/src/math/Box3.js';
import { Matrix4 } from 'three/src/math/Matrix4.js';
import { Vector3 } from 'three/src/math/Vector3.js';
import { Scene } from 'three/src/scenes/Scene.js';
import { PerspectiveCamera } from 'three/src/cameras/PerspectiveCamera.js';
import { AmbientLight } from 'three/src/lights/AmbientLight.js';
@ -34,14 +35,12 @@ export function centerGeometry(mesh) {
mesh.geometry.applyMatrix(new Matrix4().makeTranslation(-center.x, -center.y, -center.z));
}
export function createScene(canvas, props, state) {
const { pixelRatio } = props;
const { settings } = state;
export function createScene({ pixelRatio }) {
const scene = new Scene();
const camera = new PerspectiveCamera(50, 1, 1, 10000);
camera.position.set(0, 400, 300);
camera.lookAt(new Vector3(0, 0, 0));
const directionalLightA = new DirectionalLight(0xa2a2a2);
directionalLightA.position.set(1, 1, 1);
@ -61,8 +60,10 @@ export function createScene(canvas, props, state) {
const box = new BoxHelper(new Mesh(new BoxGeometry(1, 1, 1).applyMatrix(new Matrix4().makeTranslation(0, 0.5, 0))), 0x72bcd4);
scene.add(box);
const { dimensions } = settings;
box.scale.set(dimensions.y, dimensions.z, dimensions.x);
let renderer = new WebGLRenderer({ alpha: true, antialias: true });
let editorControls = new THREE.EditorControls(camera, renderer.domElement);
box.scale.set(1., 1., 1.);
box.updateMatrix();
const render = () => renderer.render(scene, camera);
@ -75,8 +76,6 @@ export function createScene(canvas, props, state) {
render();
};
let editorControls;
let renderer;
const updateCanvas = (canvas) => {
if (!renderer || renderer.domElement !== canvas) {
if (renderer) renderer.dispose();
@ -86,13 +85,11 @@ export function createScene(canvas, props, state) {
if (!editorControls || editorControls.domElement !== canvas) {
if (editorControls) editorControls.dispose();
editorControls = new THREE.EditorControls(camera, canvas);
editorControls.focus(mesh);
editorControls.addEventListener('change', render);
}
render();
};
updateCanvas(canvas);
const focus = () => editorControls.focus(mesh);
@ -119,8 +116,8 @@ export function fetchProgress(url, { method = 'get', headers = {}, body = {} } =
const GCODE_SERVER_URL = 'https://gcodeserver.doodle3d.com';
const CONNECT_URL = 'http://connect.doodle3d.com/';
export async function slice(target, name, mesh, settings, printers, quality, material, updateProgress) {
if (!printers) throw new Error('Please select a printer');
export async function slice(target, name, mesh, settings, updateProgress) {
if (!settings) throw new Error('please select a printer first');
let steps;
let currentStep = 0;
@ -167,22 +164,13 @@ export async function slice(target, name, mesh, settings, printers, quality, mat
body.append(key, fields[key]);
}
const file = ';' + JSON.stringify({
name: `${name}.gcode`,
const file = `;${JSON.stringify({
...settings,
printer: {
type: printers,
title: printerSettings[printers].title
},
material: {
type: material,
title: materialSettings[material].title
},
quality: {
type: quality,
title: qualitySettings[quality].title
}
}).trim() + '\n' + gcode;
name: `${name}.gcode`,
printer: { type: settings.printers, title: printerSettings[settings.printer].title },
material: { type: settings.material, title: materialSettings[settings.material].title },
quality: { type: settings.quality, title: qualitySettings[settings.quality].title }
}).trim()}\n${gcode}`;
body.append('file', file);
await fetchProgress(reservation.url, { method: 'POST', body }, (progess) => {
@ -195,6 +183,7 @@ export async function slice(target, name, mesh, settings, printers, quality, mat
const popup = window.open(`${CONNECT_URL}?uuid=${id}`, '_blank');
if (!popup) throw new Error('popup was blocked by browser');
break;
}
default: