From d25c3499ad7d26e8eca157031f0e3aaa91ffe38f Mon Sep 17 00:00:00 2001 From: Mark Moissette Date: Mon, 15 Jan 2018 13:22:00 +0100 Subject: [PATCH] feat(Updates): (#12) * chore(pkg): updated pkg.json with more build & general settings * refactor(ui): moved main html output out to seperate module * refactor(actions): moved actions to seperate folder * refactor(state): extracted export file path determination to simple function * initial output format now determined correctly * feat(error display): added basic display of errors & updated internal plumbing accordingly * fix(design loading): * seperated setting design path from design loading * moved file system interaction to fsWrapper side effect * modified fileWatcher to return the file contents * added potential fix for fileWatcher issues thanks to the above * modified actions & state accordingly * refactor(loadScript): moved out fs.readFileSync, now takes script content & path as input * chore(pkg): added dev launch command for window * chore(ui): modified export message * chore(experiments): added experiments for other useful side effects * closes #14 * closes #13 * closes #11 * closes #10 --- index.html | 7 +++ main.js | 20 +++++++-- package.json | 18 ++++++-- src/{ => actions}/actions.js | 28 +++++------- src/actions/types.js | 0 src/app.js | 81 ++++++---------------------------- src/core/scripLoading.js | 3 +- src/sideEffects/fileWatcher.js | 8 +++- src/sideEffects/fsWrapper.js | 6 +-- src/sideEffects/i8n.js | 18 ++++++++ src/sideEffects/ipc.js | 12 +++++ src/state.js | 68 +++++++++++++++++----------- src/ui/main.js | 68 ++++++++++++++++++++++++++++ 13 files changed, 214 insertions(+), 123 deletions(-) rename src/{ => actions}/actions.js (89%) create mode 100644 src/actions/types.js create mode 100644 src/sideEffects/i8n.js create mode 100644 src/sideEffects/ipc.js create mode 100644 src/ui/main.js diff --git a/index.html b/index.html index 473a3ee..33d2293 100644 --- a/index.html +++ b/index.html @@ -51,9 +51,16 @@ user-select: none; } #busy{ + position: absolute; + color: black; + top: 50px; + } + #status{ position: absolute; color: red; top: 50px; + left: 100px; + z-index: 100; } #container{ z-index: 100; diff --git a/main.js b/main.js index 6d555ac..2f0a287 100644 --- a/main.js +++ b/main.js @@ -27,7 +27,7 @@ function createWindow () { })) // Open the DevTools. - // mainWindow.webContents.openDevTools() + mainWindow.webContents.openDevTools() // Emitted when the window is closed. mainWindow.on('closed', function () { @@ -47,9 +47,7 @@ app.on('ready', createWindow) app.on('window-all-closed', function () { // On OS X it is common for applications and their menu bar // to stay active until the user quits explicitly with Cmd + Q - // if (process.platform !== 'darwin') { app.quit() - // } }) app.on('activate', function () { @@ -62,3 +60,19 @@ app.on('activate', function () { // In this file you can include the rest of your app's specific main process // code. You can also put them in separate files and require them here. +electron.ipcMain.on('get-file-data', function (event) { + let data = null + if (process.platform === 'win32' && process.argv.length >= 2) { + var openFilePath = process.argv[1] + data = openFilePath + } + console.log('here', data, event, process.argv) + event.sender.send('asynchronous-reply', {data, event, args: process.argv}) + event.returnValue = data +}) +console.log('foo') + +electron.ipcMain.on('asynchronous-message', (event, arg) => { + console.log(arg) // prints "ping" + event.sender.send('asynchronous-reply', 'pong') +}) diff --git a/package.json b/package.json index 2a425a5..6d9f889 100644 --- a/package.json +++ b/package.json @@ -2,15 +2,17 @@ "name": "jscad-desktop", "version": "0.0.1", "description": "jscad desktop application", + "author": "jscad core team/ Mark Moissette", + "license": "MIT", + "repository": "", "main": "main.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "dev": "node node_modules/.bin/electron .", + "dev-win": "node_modules/.bin/electron.cmd .", "pack": "electron-builder --dir", "dist": "electron-builder" }, - "author": "Mark 'kaosat-dev' Moissette", - "license": "MIT", "dependencies": { "@jscad/amf-serializer": "^0.1.1", "@jscad/csg": "^0.3.7", @@ -40,7 +42,15 @@ "build": { "appId": "jscad.id", "mac": { - "category": "jscad.3dmodeling.type" - } + "category": "public.app-category.graphics-design" + }, + "linux":{ + "target": "AppImage", + "category": "Graphics" + }, + "win":{ + "target": "nsis" + }, + "fileAssociations": [{"ext": "jscad", "name":"jscad","role":"editor"}] } } diff --git a/src/actions.js b/src/actions/actions.js similarity index 89% rename from src/actions.js rename to src/actions/actions.js index 543a113..2787b05 100644 --- a/src/actions.js +++ b/src/actions/actions.js @@ -2,8 +2,8 @@ const path = require('path') const most = require('most') const {remote} = require('electron') const {dialog} = remote -const {getScriptFile} = require('./core/scripLoading') -const {head} = require('./utils') +const {getScriptFile} = require('../core/scripLoading') +const {head} = require('../utils') function compositeKeyFromKeyEvent (event) { const ctrl = event.ctrlKey ? 'ctrl+' : '' @@ -108,7 +108,7 @@ const makeActions = (sources) => { const filePath = dialog.showSaveDialog({properties: ['saveFile'], title: 'export design to', defaultPath: defaultExportFilePath})//, function (filePath) { // console.log('saving', filePath) if (filePath !== undefined) { - const saveDataToFs = require('./io/saveDataToFs') + const saveDataToFs = require('../io/saveDataToFs') saveDataToFs(data, exportFormat, filePath) } }) @@ -126,35 +126,31 @@ const makeActions = (sources) => { .map(data => [data]), sources.drops .filter(drop => drop.type === 'fileOrFolder' && drop.data.length > 0) - .map(drop => drop.data.map(fileOrFolder => fileOrFolder.path)), - sources.watcher - .map(path => [path]) + .map(drop => drop.data.map(fileOrFolder => fileOrFolder.path)) ]) .filter(data => data !== undefined) - .debounce(300) + .debounce(50) .multicast() - const designLoadRequested$ = designPath$ - .map(data => ({type: 'designLoadRequested', data})) - const setDesignPath$ = designPath$ .map(data => ({type: 'setDesignPath', data})) .delay(1) - const setDesignScriptContent$ = most.mergeArray([ - sources.fs.filter() + const setDesignContent$ = most.mergeArray([ + sources.fs.filter(data => data.operation === 'read').map(raw => raw.data), + sources.watcher// .map(content => ) ]) - .map(data => ({type: 'setDesignScriptContent', data})) + .map(data => ({type: 'setDesignContent', data})) // design parameter change actions const updateDesignFromParams$ = most.mergeArray([ sources.dom.select('#updateDesignFromParams').events('click') .map(function () { const controls = Array.from(document.getElementById('paramsMain').getElementsByTagName('input')) - return {paramValues: require('./core/getParamValues')(controls), origin: 'manualUpdate'} + return {paramValues: require('../core/getParamValues')(controls), origin: 'manualUpdate'} }), sources.paramChanges.map(function (controls) { - return {paramValues: require('./core/getParamValues')(controls), origin: 'instantUpdate'} + return {paramValues: require('../core/getParamValues')(controls), origin: 'instantUpdate'} }) ]) .map(data => ({type: 'updateDesignFromParams', data})) @@ -172,7 +168,7 @@ const makeActions = (sources) => { toggleInstantUpdate$, // design setDesignPath$, - designLoadRequested$, + setDesignContent$, updateDesignFromParams$, // exports changeExportFormat$, diff --git a/src/actions/types.js b/src/actions/types.js new file mode 100644 index 0000000..e69de29 diff --git a/src/app.js b/src/app.js index a1e2965..88b7f62 100644 --- a/src/app.js +++ b/src/app.js @@ -19,7 +19,7 @@ const paramsCallbacktoStream = require('./observable-utils/callbackToObservable' const { attach, stream } = proxy() const state$ = stream // -const actions$ = require('./actions')({ +const actions$ = require('./actions/actions')({ store: storeSource$, drops: dragAndDropSource$, watcher: watcherSource(), @@ -58,15 +58,16 @@ electronStoreSink(state$ watcherSink( state$ .filter(state => state.design.mainPath !== '') // FIXME: disable watch if autoreload is set to false - .map(state => ({filePath: state.design.mainPath, enabled: state.autoReload})) .skipRepeats() + .map(state => ({filePath: state.design.mainPath, enabled: state.autoReload})) ) -/* fsSink( +fsSink( state$ .filter(state => state.design.mainPath !== '') - .map(state => ({operation: 'read', id: 'loadScript', path: state.design.mainPath})) + .map(state => state.design.mainPath) .skipRepeats() -) */ + .map(path => ({operation: 'read', id: 'loadScript', path})) +) // viewer data state$ @@ -90,66 +91,6 @@ state$ }) // ui updates, exports -const html = require('bel') - -function dom (state) { - const formatsList = state.availableExportFormats - .map(function ({name, displayName}) { - return html`` - }) - - const {createParamControls} = require('./ui/createParameterControls2') - const {paramValues, paramDefinitions} = state.design - const {controls} = createParamControls(paramValues, paramDefinitions, true, paramsCallbacktoStream.callback) - - const output = html` -
- -
- - - - - - - - - - - - - - - - - - ${state.busy ? 'processing, please wait' : ''} -
- - - - - ${controls} -
-
- - - - - -
- - - -
- ` - return output -} const outToDom$ = state$ .skipRepeatsWith(function (state, previousState) { @@ -159,13 +100,17 @@ const outToDom$ = state$ const sameExportFormats = state.exportFormat === previousState.exportFormat && state.availableExportFormats === previousState.availableExportFormats - const sameStatus = state.busy === previousState.busy const sameStyling = state.themeName === previousState.themeName const sameAutoreload = state.autoReload === previousState.autoReload - return sameParamDefinitions && sameExportFormats && sameStatus && sameStyling && sameAutoreload && sameInstantUpdate + + const sameError = JSON.stringify(state.error) === JSON.stringify(previousState.error) + const sameStatus = state.busy === previousState.busy + + return sameParamDefinitions && sameExportFormats && sameStatus && sameStyling && + sameAutoreload && sameInstantUpdate && sameError }) - .map(state => dom(state)) + .map(state => require('./ui/main')(state, paramsCallbacktoStream)) domSink(outToDom$) diff --git a/src/core/scripLoading.js b/src/core/scripLoading.js index 5007fe8..061b313 100644 --- a/src/core/scripLoading.js +++ b/src/core/scripLoading.js @@ -72,9 +72,8 @@ const getScriptFile = paths => { * @param {} filePath * @param {} csgBasePath='../../../core/' : relative path or './node_modules/@jscad' */ -function loadScript (filePath, csgBasePath = './node_modules/@jscad') { +function loadScript (scriptAsText, filePath, csgBasePath = './node_modules/@jscad') { console.log('loading script using jscad/csg base path at:', csgBasePath) - const scriptAsText = fs.readFileSync(filePath, 'utf8') let jscadScript // && !scriptAsText.includes('require(') if ((!scriptAsText.includes('module.exports')) && scriptAsText.includes('main')) { diff --git a/src/sideEffects/fileWatcher.js b/src/sideEffects/fileWatcher.js index b8bc6dc..965bfa0 100644 --- a/src/sideEffects/fileWatcher.js +++ b/src/sideEffects/fileWatcher.js @@ -35,7 +35,8 @@ function watcherSink (toWatch$) { watched = filename // clear require() cache to force reload the file , otherwise it keeps reloading the cached version requireUncached(filePath) - scriptDataFromCB.callback(filePath) + const contents = fs.readFileSync(filePath, 'utf8') + scriptDataFromCB.callback(contents) } }) } @@ -44,7 +45,10 @@ function watcherSink (toWatch$) { } function watcherSource () { - return scriptDataFromCB.stream.multicast().debounce(1000)// debounce is very important as fs.Watch is unstable + return scriptDataFromCB.stream + .debounce(400)// debounce is very important as fs.Watch is unstable + .skipRepeats() + .multicast() } module.exports = {watcherSink, watcherSource} diff --git a/src/sideEffects/fsWrapper.js b/src/sideEffects/fsWrapper.js index 2512783..8765b6f 100644 --- a/src/sideEffects/fsWrapper.js +++ b/src/sideEffects/fsWrapper.js @@ -5,14 +5,14 @@ const callBackToStream = require('../observable-utils/callbackToObservable') const readFileToCB = callBackToStream() function fsSink (out$) { - out$.forEach(function ({operation, path}) { + out$.forEach(function ({path, operation}) { console.log('read/writing to', path, operation) if (operation === 'read') { fs.readFile(path, 'utf8', function (error, data) { if (error) { - readFileToCB.callback({error}) + readFileToCB.callback({path, operation, error}) } else { - readFileToCB.callback({path, data, operation}) + readFileToCB.callback({path, operation, data}) } }) } diff --git a/src/sideEffects/i8n.js b/src/sideEffects/i8n.js new file mode 100644 index 0000000..4099556 --- /dev/null +++ b/src/sideEffects/i8n.js @@ -0,0 +1,18 @@ +const i18next = require('i18next') + +const {create} = require('@most/create') + +module.exports = getTranslations = (translationPaths) => { + return create((add, end, error) => { + i18next.init({ + lng: 'en', + resources: translationPaths + }, (err, t) => { + if (err) { + error(err) + } else { + add(t) + } + }) + }) +} \ No newline at end of file diff --git a/src/sideEffects/ipc.js b/src/sideEffects/ipc.js new file mode 100644 index 0000000..e9f8d90 --- /dev/null +++ b/src/sideEffects/ipc.js @@ -0,0 +1,12 @@ +const {ipcRenderer} = require('electron') +var data = ipcRenderer.sendSync('get-file-data') +if (data === null) { + console.log('There is no file') +} else { + // Do something with the file. + console.log(data) +} +ipcRenderer.send('asynchronous-message', 'ping') +ipcRenderer.on('asynchronous-reply', (event, arg) => { + console.log(arg) // prints "pong" +}) \ No newline at end of file diff --git a/src/state.js b/src/state.js index eeb5043..079201a 100644 --- a/src/state.js +++ b/src/state.js @@ -11,6 +11,8 @@ const themes = { const initialState = { appTitle: `${packageMetadata.name} v ${packageMetadata.version}`, + // for possible errors + error: undefined, // design data design: { name: '', @@ -34,6 +36,7 @@ const initialState = { themeName: 'light', mainTextColor: '#FFF', viewer: {// ridiculous shadowing of viewer state ?? or actually logical + // camera: {position: [150, 150, 250]}, rendering: { background: [0.211, 0.2, 0.207, 1], // [1, 1, 1, 1],//54, 51, 53 meshColor: [0.4, 0.6, 0.5, 1] // nice orange : [1, 0.4, 0, 1] @@ -96,23 +99,7 @@ function makeState (actions) { return Object.assign({}, state, {instantUpdate}) }, changeExportFormat: (state, exportFormat) => { - // console.log('changeExportFormat', exportFormat) - const path = require('path') - const {formats} = require('./io/formats') - const design = state.design - - const extension = formats[exportFormat].extension - const defaultFileName = `${design.name}.${extension}` - const exportFilePath = path.join(design.path, defaultFileName) - - return Object.assign({}, state, {exportFormat, exportFilePath}) - }, - designLoadRequested: (state, _) => { - // console.log('designLoadRequested') - // FIXME: UGHH so goddam verbose ! - // we want the viewer to focus on new entities for our 'session' (until design change) - const viewer = Object.assign({}, state.viewer, {behaviours: {resetViewOn: ['new-entities']}}) - return Object.assign({}, state, {busy: true, viewer}) + return Object.assign({}, state, exportFilePathFromFormatAndDesign(state.design, exportFormat)) }, setDesignPath: (state, paths) => { // console.log('setDesignPath') @@ -122,23 +109,33 @@ function makeState (actions) { const designName = path.parse(path.basename(filePath)).name const designPath = path.dirname(filePath) + const design = Object.assign({}, state.design, { + name: designName, + path: designPath, + mainPath + }) + + // we want the viewer to focus on new entities for our 'session' (until design change) + const viewer = Object.assign({}, state.viewer, {behaviours: {resetViewOn: ['new-entities']}}) + return Object.assign({}, state, {busy: true, viewer, design}) + }, + setDesignContent: (state, scriptAsText) => { + console.log('setDesignContent') + const {mainPath} = state.design // load script - const {jscadScript, paramDefinitions, params} = loadScript(mainPath) + const {jscadScript, paramDefinitions, params} = loadScript(scriptAsText, mainPath) // console.log('paramDefinitions', paramDefinitions, 'params', params) let solids = toArray(jscadScript(params)) /* func(paramDefinitions) => paramsUI func(paramsUI + interaction) => params */ - const design = { - name: designName, - path: designPath, - mainPath, + const design = Object.assign({}, state.design, { script: jscadScript, paramDefinitions, paramValues: params, solids - } + }) const {supportedFormatsForObjects, formats} = require('./io/formats') const formatsToIgnore = ['jscad', 'js'] @@ -152,7 +149,16 @@ function makeState (actions) { console.log('done updating design from path') // FIXME: UGHH so goddam verbose ! const viewer = Object.assign({}, state.viewer, {behaviours: {resetViewOn: [''], zoomToFitOn: ['new-entities']}}) - return Object.assign({}, state, {design, viewer}, {availableExportFormats, exportFormat, busy: false}) + const exportInfos = exportFilePathFromFormatAndDesign(design, exportFormat) + const appTitle = `${packageMetadata.name} v ${packageMetadata.version}: ${state.design.path}` + return Object.assign({}, state, {design, viewer}, { + availableExportFormats, + exportFormat, + appTitle, + busy: false, + error: undefined + }, + exportInfos) }, updateDesignFromParams: (state, {paramValues, origin}) => { // console.log('updateDesignFromParams') @@ -177,7 +183,7 @@ function makeState (actions) { return newState } catch (error) { console.error('caught error', error) - return merge({}, state, {error}) + return Object.assign({}, state, {error}) } // const newState = merge({}, state, updatedData) // console.log('SCAAAN', action, newState) @@ -189,3 +195,15 @@ function makeState (actions) { } module.exports = {makeState, initialState} + +// state utilities , extract at some point +const exportFilePathFromFormatAndDesign = (design, exportFormat) => { + const path = require('path') + const {formats} = require('./io/formats') + + const extension = formats[exportFormat].extension + const defaultFileName = `${design.name}.${extension}` + const exportFilePath = path.join(design.path, defaultFileName) + + return {exportFormat, exportFilePath} +} diff --git a/src/ui/main.js b/src/ui/main.js new file mode 100644 index 0000000..f5b1a8b --- /dev/null +++ b/src/ui/main.js @@ -0,0 +1,68 @@ +const html = require('bel') + +function dom (state, paramsCallbacktoStream) { + const formatsList = state.availableExportFormats + .map(function ({name, displayName}) { + return html`` + }) + + const {createParamControls} = require('./createParameterControls2') + const {paramValues, paramDefinitions} = state.design + const {controls} = createParamControls(paramValues, paramDefinitions, true, paramsCallbacktoStream.callback) + + const statusMessage = state.error !== undefined + ? `Error: ${state.error.message} details: ${state.error.stack}` : '' + + const exportButtonText = `export design to ${state.exportFormat}` + + const output = html` +
+ + ${state.busy ? 'processing, please wait' : ''} + ${statusMessage} + +
+ + + + + + + + + + + + + + + + +
+ + + + + ${controls} +
+
+ + + + + +
+ + + +
+ ` + return output +} + +module.exports = dom