diff --git a/.eslintrc.js b/.eslintrc.js
deleted file mode 100644
index 054ec9e8..00000000
--- a/.eslintrc.js
+++ /dev/null
@@ -1,18 +0,0 @@
-module.exports = {
- "extends": "airbnb-base",
- "rules": {
- "no-restricted-syntax": "off",
- "no-underscore-dangle": "off",
- "no-plusplus": "off",
- "max-len": ["error", { "code": 90 }],
- "comma-dangle": ["error", {
- "exports": "never",
- "functions": "never",
- "arrays": "always-multiline",
- "objects": "always-multiline"
- }],
- "class-methods-use-this": "off",
- "no-console": "off", // TODO: Move to some other logging method.
- "no-multi-str": "off",
- }
-};
diff --git a/.gitignore b/.gitignore
index 62840d6a..4074d5e9 100755
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,8 @@ npm-debug.log
config.ini
+test_*
+
.DS_Store
/nbproject/*
diff --git a/.nvmrc b/.nvmrc
index 48082f72..b6a7d89c 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-12
+16
diff --git a/machine_types/watercolorbot.ini b/machine_types/watercolorbot.ini
index 7b1b4703..7ea12944 100755
--- a/machine_types/watercolorbot.ini
+++ b/machine_types/watercolorbot.ini
@@ -53,6 +53,7 @@ height = 206.25
[park]
x = 0
y = 0
+z = 0
[workArea]
; Also measured in steps
@@ -93,182 +94,43 @@ max = 1023
min = 0
[tools]
+[tools.pan]
+x = 22
+y = -2.5
+width = 38.4
+height = 214
+position = topleft
+radius = 4
+type = swappable
+group = Placeholder
+
[tools.water0]
-x = 0
-y = 0
-wiggleAxis = y
-wiggleTravel = 300
-wiggleIterations = 4
+x = -12.2
+y = 14.8
+width = 57.85
+height = 57.85
+position = center
+radius = 35
group = Water
-[tools.water0dip]
-x = 0
-y = 0
-wiggleAxis = y
-wiggleTravel = 5
-wiggleIterations = 2
-group = Water Dip
-
[tools.water1]
-x = 0
-y = 1650
-wiggleAxis = y
-wiggleTravel = 300
-wiggleIterations = 4
+x = -12.2
+y = 90
+width = 57.85
+height = 57.85
+position = center
+radius = 35
group = Water
-[tools.water1dip]
-x = 0
-y = 1650
-wiggleAxis = y
-wiggleTravel = 5
-wiggleIterations = 2
-group = Water Dip
-
[tools.water2]
-x = 0
-y = 3000
-wiggleAxis = y
-wiggleTravel = 300
-wiggleIterations = 4
+x = -12.2
+y = 168
+width = 57.85
+height = 57.85
+position = center
+radius = 35
group = Water
-[tools.water2dip]
-x = 0
-y = 3000
-wiggleAxis = y
-wiggleTravel = 5
-wiggleIterations = 2
-group = Water Dip
-
-[tools.color0]
-x = 775
-y = 250
-wiggleAxis = xy
-wiggleTravel = 300
-wiggleIterations = 8
-group = Colors
-
-[tools.color1]
-x = 775
-y = 715
-wiggleAxis = xy
-wiggleTravel = 300
-wiggleIterations = 8
-group = Colors
-
-[tools.color2]
-x = 775
-y = 1135
-wiggleAxis = xy
-wiggleTravel = 300
-wiggleIterations = 8
-group = Colors
-
-[tools.color3]
-x = 775
-y = 1625
-wiggleAxis = xy
-wiggleTravel = 300
-wiggleIterations = 8
-group = Colors
-
-[tools.color4]
-x = 775
-y = 2035
-wiggleAxis = xy
-wiggleTravel = 300
-wiggleIterations = 8
-group = Colors
-
-[tools.color5]
-x = 775
-y = 2502
-wiggleAxis = xy
-wiggleTravel = 300
-wiggleIterations = 8
-group = Colors
-
-[tools.color6]
-x = 775
-y = 2955
-wiggleAxis = xy
-wiggleTravel = 300
-wiggleIterations = 8
-group = Colors
-
-[tools.color7]
-x = 775
-y = 3405
-wiggleAxis = xy
-wiggleTravel = 300
-wiggleIterations = 8
-group = Colors
-
-[tools.color0dip]
-x = 775
-y = 250
-wiggleAxis = xy
-wiggleTravel = 5
-wiggleIterations = 2
-group = Colors Dip
-
-[tools.color1dip]
-x = 775
-y = 715
-wiggleAxis = xy
-wiggleTravel = 5
-wiggleIterations = 2
-group = Colors Dip
-
-[tools.color2dip]
-x = 775
-y = 1135
-wiggleAxis = xy
-wiggleTravel = 5
-wiggleIterations = 2
-group = Colors Dip
-
-[tools.color3dip]
-x = 775
-y = 1625
-wiggleAxis = xy
-wiggleTravel = 5
-wiggleIterations = 2
-group = Colors Dip
-
-[tools.color4dip]
-x = 775
-y = 2035
-wiggleAxis = xy
-wiggleTravel = 5
-wiggleIterations = 2
-group = Colors Dip
-
-[tools.color5dip]
-x = 775
-y = 2502
-wiggleAxis = xy
-wiggleTravel = 5
-wiggleIterations = 2
-group = Colors Dip
-
-[tools.color6dip]
-x = 775
-y = 2955
-wiggleAxis = xy
-wiggleTravel = 5
-wiggleIterations = 2
-group = Colors Dip
-
-[tools.color7dip]
-x = 775
-y = 3405
-wiggleAxis = xy
-wiggleTravel = 5
-wiggleIterations = 2
-group = Colors Dip
-
[tools.manualswap]
x = 0
y = 0
diff --git a/node.importmap b/node.importmap
new file mode 100644
index 00000000..b524579f
--- /dev/null
+++ b/node.importmap
@@ -0,0 +1,73 @@
+{
+ "imports": {
+ "cs/unify": "./src/components/core/utils/cncserver.unify.js",
+ "cs/utils": "./src/components/core/utils/cncserver.utils.js",
+ "cs/binder": "./src/components/core/utils/cncserver.binder.js",
+ "cs/settings": "./src/components/core/utils/cncserver.settings.js",
+ "cs/i18n": "./src/components/core/utils/cncserver.i18n.js",
+ "cs/server": "./src/components/core/comms/cncserver.server.js",
+ "cs/rest": "./src/components/core/comms/cncserver.rest.js",
+ "cs/ipc": "./src/components/core/comms/cncserver.ipc.js",
+ "cs/serial": "./src/components/core/comms/cncserver.serial.js",
+ "cs/sockets": "./src/components/core/comms/cncserver.sockets.js",
+ "cs/pen": "./src/components/core/control/cncserver.pen.js",
+ "cs/actualPen": "./src/components/core/control/cncserver.actualpen.js",
+ "cs/control": "./src/components/core/control/cncserver.control.js",
+ "cs/tools": "./src/components/core/control/cncserver.tools.js",
+ "cs/buffer": "./src/components/core/utils/cncserver.buffer.js",
+ "cs/run": "./src/components/core/utils/cncserver.run.js",
+ "cs/scratch": "./src/components/third_party/scratch/cncserver.scratch.js",
+ "cs/drawing": "./src/components/core/drawing.js",
+ "cs/projects": "./src/components/core/control/cncserver.projects.js",
+ "cs/print": "./src/components/core/control/cncserver.print.js",
+ "cs/content": "./src/components/core/control/cncserver.content.js",
+ "cs/schemas": "./src/components/core/schemas/cncserver.schemas.js",
+ "cs/api": "./src/components/core/comms/cncserver.api.js",
+ "cs/api/handlers": "./src/components/core/comms/api/index.js",
+ "cs/api/handlers/settings": "./src/components/core/comms/api/cncserver.api.settings.js",
+ "cs/api/handlers/pen": "./src/components/core/comms/api/cncserver.api.pen.js",
+ "cs/api/handlers/motors": "./src/components/core/comms/api/cncserver.api.motors.js",
+ "cs/api/handlers/buffer": "./src/components/core/comms/api/cncserver.api.buffer.js",
+ "cs/api/handlers/tools": "./src/components/core/comms/api/cncserver.api.tools.js",
+ "cs/api/handlers/colors": "./src/components/core/comms/api/cncserver.api.colors.js",
+ "cs/api/handlers/implements": "./src/components/core/comms/api/cncserver.api.implements.js",
+ "cs/api/handlers/projects": "./src/components/core/comms/api/cncserver.api.projects.js",
+ "cs/api/handlers/content": "./src/components/core/comms/api/cncserver.api.content.js",
+ "cs/api/handlers/print": "./src/components/core/comms/api/cncserver.api.print.js",
+ "cs/api/handlers/serial": "./src/components/core/comms/api/cncserver.api.serial.js",
+ "cs/bots": "./src/components/machine_support/index.js",
+ "cs/bots/base": "./src/components/machine_support/cncserver.bots.base.js",
+ "cs/bots/ebb": "./src/components/machine_support/cncserver.bots.ebb.js",
+ "cs/bots/watercolorbot": "./src/components/machine_support/cncserver.bots.watercolorbot.js",
+ "cs/drawing": "./src/components/core/drawing/index.js",
+ "cs/drawing/base": "./src/components/core/drawing/cncserver.drawing.base.js",
+ "cs/drawing/occlusion": "./src/components/core/drawing/cncserver.drawing.occlusion.js",
+ "cs/drawing/trace": "./src/components/core/drawing/cncserver.drawing.trace.js",
+ "cs/drawing/spawner": "./src/components/core/drawing/cncserver.drawing.spawner.js",
+ "cs/drawing/fill": "./src/components/core/drawing/cncserver.drawing.fill.js",
+ "cs/drawing/vectorize": "./src/components/core/drawing/cncserver.drawing.vectorize.js",
+ "cs/drawing/text": "./src/components/core/drawing/cncserver.drawing.text.js",
+ "cs/drawing/accell": "./src/components/core/drawing/cncserver.drawing.accell.js",
+ "cs/drawing/colors": "./src/components/core/drawing/cncserver.drawing.colors.js",
+ "cs/drawing/colors/matcher": "./src/interface/modules/utils/colorset-matcher.mjs",
+ "cs/drawing/implements": "./src/components/core/drawing/cncserver.drawing.implements.js",
+ "cs/drawing/stage": "./src/components/core/drawing/cncserver.drawing.stage.js",
+ "cs/drawing/preview": "./src/components/core/drawing/cncserver.drawing.preview.js",
+ "cs/drawing/temp": "./src/components/core/drawing/cncserver.drawing.temp.js",
+ "cs/schemas": "./src/components/core/schemas/cncserver.schemas.js",
+ "cs/schemas/index": "./src/components/core/schemas/index.js",
+ "cs/schemas/projects": "./src/components/core/schemas/cncserver.schemas.projects.js",
+ "cs/schemas/content": "./src/components/core/schemas/cncserver.schemas.content.js",
+ "cs/schemas/content/settings": "./src/components/core/schemas/cncserver.schemas.content.settings.js",
+ "cs/schemas/fill": "./src/components/core/schemas/cncserver.schemas.fill.js",
+ "cs/schemas/stroke": "./src/components/core/schemas/cncserver.schemas.stroke.js",
+ "cs/schemas/text": "./src/components/core/schemas/cncserver.schemas.text.js",
+ "cs/schemas/vectorize": "./src/components/core/schemas/cncserver.schemas.vectorize.js",
+ "cs/schemas/path": "./src/components/core/schemas/cncserver.schemas.path.js",
+ "cs/schemas/color": "./src/components/core/schemas/cncserver.schemas.color.js",
+ "cs/schemas/colors": "./src/components/core/schemas/cncserver.schemas.colors.js",
+ "cs/schemas/tools": "./src/components/core/schemas/cncserver.schemas.tools.js",
+ "cs/schemas/implements": "./src/components/core/schemas/cncserver.schemas.implements.js",
+ "cs/schemas/toolsets": "./src/components/core/schemas/cncserver.schemas.toolsets.js"
+ }
+}
diff --git a/package.json b/package.json
index f8968e4b..497e2e5e 100644
--- a/package.json
+++ b/package.json
@@ -1,27 +1,30 @@
{
"name": "cncserver",
- "version": "3.0.0-beta1",
+ "version": "3.0.0-beta.1",
"description": "A web based a web based CNC Server/Controller to drive @makersylvia's WaterColorBot and beyond",
"main": "cncserver.js",
"author": "techninja",
+ "type": "module",
"license": "MIT",
"dependencies": {
"@fortawesome/fontawesome-free": "^5.9.0",
- "@json-editor/json-editor": "^2.2.0",
+ "@json-editor/json-editor": "https://github.com/techninja/json-editor.git#no-undefined-data",
+ "@node-loader/import-maps": "^1.0.3",
"ajv": "^6.12.0",
"axios": "^0.19.0",
"body-parser": "^1.17.1",
"bootstrap": "^4.4.1",
"bulma": "^0.7.5",
"canvas": "^2.6.0",
+ "chroma-js": "^2.1.0",
"connect-slashes": "techninja/connect-slashes#ignore-files",
"data-uri-to-buffer": "^3.0.0",
"datauri": "^2.0.0",
"express": "~3.0.0",
"font-list": "^1.2.9",
"glob": "^7.1.6",
- "hersheytext": "1",
- "hybrids": "^4.0.3",
+ "hersheytext": "2",
+ "hybrids": "^4.2.1",
"i18next": "^19.0.1",
"i18next-express-middleware": "^1.8.2",
"i18next-node-fs-backend": "^2.1.3",
@@ -30,30 +33,34 @@
"jquery": "^3.4.1",
"js-angusj-clipper": "^1.0.3",
"jsonform": "^2.1.6",
+ "jsonpath": "^1.0.2",
"merge-deep": "^3.0.2",
"nconf": "0.8.4",
- "nearest-color": "^0.4.4",
"node-ipc": "8.9.2",
"paper": "^0.12.3",
"paper-jsdom": "^0.12.3",
"path-to-regexp": "^1.7.0",
+ "png-metadata": "^1.0.2",
"request": "*",
"select2": "^4.0.12",
"semver": "^6.3.0",
- "serialport": "7.1.5",
+ "serialport": "^9",
"snowpack": "^1.4.0",
"socket.io": "^2.3.0",
"underscore": "^1.9.2",
"vectorize-text": "^3.2.1",
- "weightless": "^0.0.37",
"zodiac-ts": "^1.0.3"
},
"devDependencies": {
+ "@jsenv/importmap-eslint-resolver": "^2.4.0",
+ "babel-eslint": "^10.1.0",
"chai": "*",
- "eslint": "^6.0.1",
- "eslint-config-airbnb-base": "^13.2.0",
- "eslint-plugin-import": "^2.18.0",
- "mocha": "*"
+ "eslint": "^7.2.0",
+ "eslint-config-airbnb-base": "^14.2.0",
+ "eslint-plugin-import": "^2.22.0",
+ "mocha": "*",
+ "prettier": "^2.2.1",
+ "prettier-eslint": "^12.0.0"
},
"engines": {
"node": ">= 10.0.0"
@@ -64,7 +71,68 @@
},
"scripts": {
"lint": "eslint \"src/**/*.js\"",
- "start": "node ./src/cncserver",
+ "start": "node --experimental-loader @node-loader/import-maps ./src/cncserver.js",
+ "test": "node --experimental-loader @node-loader/import-maps ./src/test.js",
"prepare": "snowpack"
+ },
+ "eslintConfig": {
+ "parser": "babel-eslint",
+ "parserOptions": {
+ "ecmaVersion": 2017
+ },
+ "extends": "airbnb-base",
+ "plugins": [
+ "import"
+ ],
+ "rules": {
+ "no-restricted-syntax": "off",
+ "no-underscore-dangle": "off",
+ "no-plusplus": "off",
+ "import/extensions": "off",
+ "import/prefer-default-export": [
+ 0
+ ],
+ "import/no-unresolved": [
+ 2,
+ {
+ "ignore": [
+ "cs/*"
+ ]
+ }
+ ],
+ "max-len": [
+ "error",
+ {
+ "code": 90
+ }
+ ],
+ "comma-dangle": [
+ "error",
+ {
+ "exports": "never",
+ "functions": "never",
+ "arrays": "always-multiline",
+ "objects": "always-multiline"
+ }
+ ],
+ "object-property-newline": [
+ 2,
+ {
+ "allowAllPropertiesOnSameLine": true
+ }
+ ],
+ "arrow-parens": [
+ 2,
+ "as-needed"
+ ],
+ "class-methods-use-this": "off",
+ "no-console": "off",
+ "no-multi-str": "off"
+ }
+ },
+ "prettier": {
+ "singleQuote": true,
+ "trailingComma": "es5",
+ "arrowParens": "avoid"
}
}
diff --git a/prettier.config.js b/prettier.config.js
deleted file mode 100644
index a425d3f7..00000000
--- a/prettier.config.js
+++ /dev/null
@@ -1,4 +0,0 @@
-module.exports = {
- singleQuote: true,
- trailingComma: 'es5',
-};
diff --git a/src/cncserver.js b/src/cncserver.js
index 6ea5a322..245448d8 100644
--- a/src/cncserver.js
+++ b/src/cncserver.js
@@ -18,57 +18,18 @@
* cncserver.start({
* error: function callback(err){ // ERROR! },
* success: function callback(){ // SUCCESS! },
- * disconnect: function callback(){ //BOT DISCONNECTED! }
+ * disconnect: function callback(){ // BOT DISCONNECTED! }
* };
*
*/
-// CONFIGURATION ===============================================================
-const cncserver = { // Master object for holding/passing stuff!
- bot: {}, // Holds clean rendered settings set after botConfig is loaded
- exports: {}, // This entire object will be added to the module.exports,
-};
-
-global.__basedir = __dirname;
-
-// Global Defaults (also used to write the initial config.ini)
-cncserver.globalConfigDefaults = {
- httpPort: 4242,
- httpLocalOnly: true,
- swapMotors: false, // Global setting for bots that don't have it configured
- invertAxis: {
- x: false,
- y: false,
- },
- maximumBlockingCallStack: 100, // Limit for the # blocking sequential calls
- showSerial: false, // Specific debug to show serial data.
- serialPath: '{auto}', // Empty for auto-config
- bufferLatencyOffset: 30, // Number of ms to move each command closer together
- corsDomain: '*', // Start as open to CORs enabled browser clients
- debug: false,
- botType: 'watercolorbot',
- scratchSupport: true,
- flipZToggleBit: false,
- botOverride: {
- info: 'Override bot settings E.G. > [botOverride.eggbot] servo:max = 1234',
- },
-};
-
-// COMPONENT REQUIRES ==========================================================
+import 'cs/i18n';
+import { loadGlobalConfig, loadBotConfig } from 'cs/settings';
+import { initServer } from 'cs/ipc';
+import { connect, localTrigger } from 'cs/serial';
+import 'cs/bots';
-// Load all components.
-const components = require('./components');
-
-// TODO: Find a better way to do this.
-for (const [key, path] of Object.entries(components)) {
- // eslint-disable-next-line global-require, import/no-dynamic-require
- const component = require(path)(cncserver);
- cncserver[key] = component;
- if (component && component.exports) {
- cncserver.exports = { ...cncserver.exports, ...component.exports };
- delete component.exports;
- }
-}
+// CONFIGURATION ===============================================================
// TODO: Convert to promises instaed of endless callbacks and error management.
@@ -94,29 +55,28 @@ for (const [key, path] of Object.entries(components)) {
// INTIAL SETUP ================================================================
// Load the Global Configuration (from config, defaults & CL vars)
-cncserver.settings.loadGlobalConfig(() => {
- // Only if we're running standalone... try to start the server immediately!
- if (!module.parent) {
- // Load the bot specific configuration, defaulting to gConf bot type
- cncserver.settings.loadBotConfig(() => {
- cncserver.ipc.initServer({ localRunner: true }, () => {
- // Runner is ready! Attempt Initial Serial Connection.
- cncserver.serial.connect({
- error: () => {
- console.error('CONNECTSERIAL ERROR!');
- cncserver.serial.localTrigger('simulationStart');
- cncserver.serial.localTrigger('serialReady');
- },
- connect: () => {
- cncserver.serial.localTrigger('serialReady');
- },
- disconnect: () => {
- cncserver.serial.localTrigger('serialClose');
- },
- });
+loadGlobalConfig(() => {
+ // Try to start the server immediately!
+ // Load the bot specific configuration, defaulting to gConf bot type
+ loadBotConfig(() => {
+ initServer({ localRunner: true }, () => {
+ // Runner is ready! Attempt Initial Serial Connection.
+ connect({
+ error: () => {
+ console.error('CONNECTSERIAL ERROR!');
+ localTrigger('simulationStart');
+ localTrigger('serialReady');
+ },
+ connect: () => {
+ localTrigger('serialReady');
+ },
+ disconnect: () => {
+ localTrigger('serialClose');
+ },
});
});
- } else { // Export the module's useful API functions! =================
+ });
+ /* } else { // Export the module's useful API functions! =================
// Enherit all module added exports.
module.exports = cncserver.exports;
@@ -131,7 +91,7 @@ cncserver.settings.loadGlobalConfig(() => {
module.exports.penUpdateTrigger = options.penUpdate;
}
- cncserver.settings.loadBotConfig(() => {
+ loadBotConfig(() => {
// Before we can attempt to connect to the serialport, we must ensure
// The IPC runner is connected...
@@ -175,5 +135,5 @@ cncserver.settings.loadGlobalConfig(() => {
module.exports.serialReadyInit = () => {
cncserver.serial.localTrigger('serialReady');
};
- }
+ } */
});
diff --git a/src/components/core/comms/api/cncserver.api.buffer.js b/src/components/core/comms/api/cncserver.api.buffer.js
index 3b9330cc..13af4ab1 100644
--- a/src/components/core/comms/api/cncserver.api.buffer.js
+++ b/src/components/core/comms/api/cncserver.api.buffer.js
@@ -1,147 +1,156 @@
/**
* @file CNCServer ReSTful API endpoint module for pen state management.
*/
-const handlers = {};
-
-module.exports = (cncserver) => {
- handlers['/v2/buffer'] = function bufferMain(req, res) {
- const { buffer } = cncserver;
- if (req.route.method === 'get' || req.route.method === 'put') {
- // Pause/resume (normalize input)
- if (typeof req.body.paused === 'string') {
- req.body.paused = req.body.paused === 'true';
- }
+import {
+ state as bufferState,
+ pause,
+ resume,
+ clear
+} from 'cs/buffer';
+import { setHeight } from 'cs/pen';
+import { state as actualPenState } from 'cs/actualPen';
+import { actuallyMove, actuallyMoveHeight } from 'cs/control';
+import { gConf } from 'cs/settings';
+import { sendBufferVars } from 'cs/sockets';
+import run from 'cs/run';
+import { trigger } from 'cs/binder';
+
+export const handlers = {};
+
+handlers['/v2/buffer'] = function bufferMain(req, res) {
+ if (req.route.method === 'get' || req.route.method === 'put') {
+ // Pause/resume (normalize input)
+ if (typeof req.body.paused === 'string') {
+ req.body.paused = req.body.paused === 'true';
+ }
- if (typeof req.body.paused === 'boolean') {
- if (req.body.paused !== buffer.paused) {
- // If pausing, trigger immediately.
- // Resuming can't do this if returning from another position.
- if (req.body.paused) {
- buffer.pause();
- cncserver.pen.setHeight('up', null, true); // Pen up for safety!
- console.log('Run buffer paused!');
- } else {
- console.log('Resume to begin shortly...');
- }
-
- // Changed to paused!
- buffer.newlyPaused = req.body.paused;
+ if (typeof req.body.paused === 'boolean') {
+ if (req.body.paused !== bufferState.paused) {
+ // If pausing, trigger immediately.
+ // Resuming can't do this if returning from another position.
+ if (req.body.paused) {
+ pause();
+ setHeight('up', null, true); // Pen up for safety!
+ console.log('Run buffer paused!');
+ } else {
+ console.log('Resume to begin shortly...');
}
+
+ // Changed to paused!
+ bufferState.newlyPaused = req.body.paused;
}
+ }
- // Did we actually change position since pausing?
- let changedSincePause = false;
- if (buffer.pausePen) {
- if (buffer.pausePen.x !== cncserver.actualPen.state.x
- || buffer.pausePen.y !== cncserver.actualPen.state.y
- || buffer.pausePen.height !== cncserver.actualPen.state.height) {
- changedSincePause = true;
- console.log('CHANGED SINCE PAUSE');
- } else if (!req.body.paused) {
- // If we're resuming, and there's no change... clear the pause pen
- console.log('RESUMING NO CHANGE!');
- }
+ // Did we actually change position since pausing?
+ let changedSincePause = false;
+ if (bufferState.pausePen && req.route.method === 'put') {
+ if (bufferState.pausePen.x !== actualPenState.x
+ || bufferState.pausePen.y !== actualPenState.y
+ || bufferState.pausePen.height !== actualPenState.height) {
+ changedSincePause = true;
+ console.log('CHANGED SINCE PAUSE');
+ } else if (!req.body.paused) {
+ // If we're resuming, and there's no change... clear the pause pen
+ console.log('RESUMING NO CHANGE!');
}
+ }
- // Resuming?
- if (!req.body.paused) {
- // Move back to position we paused at (if changed).
- if (changedSincePause) {
- // Remain paused until we've finished...
- console.log('Moving back to pre-pause position...');
-
- // Set the pen up before moving to resume position
- cncserver.pen.setHeight('up', () => {
- cncserver.control.actuallyMove(buffer.pausePen, () => {
- // Set the height back to what it was AFTER moving
- cncserver.control.actuallyMoveHeight(
- buffer.pausePen.height,
- buffer.pausePen.state,
- () => {
- console.log('Resuming buffer!');
- buffer.resume();
-
- res.status(200).send(JSON.stringify({
- running: buffer.running,
- paused: buffer.paused,
- count: buffer.data.length,
- buffer: "This isn't a great idea...", // TODO: FIX <<
- }));
-
- if (cncserver.settings.gConf.get('debug')) {
- console.log('>RESP', req.route.path, '200');
- }
+ // Resuming?
+ if (!req.body.paused) {
+ // Move back to position we paused at (if changed).
+ if (changedSincePause) {
+ // Remain paused until we've finished...
+ console.log('Moving back to pre-pause position...');
+
+ // Set the pen up before moving to resume position
+ setHeight('up', () => {
+ actuallyMove(bufferState.pausePen, () => {
+ // Set the height back to what it was AFTER moving
+ actuallyMoveHeight(
+ bufferState.pausePen.height,
+ bufferState.pausePen.state,
+ () => {
+ console.log('Resuming buffer!');
+ resume();
+
+ res.status(200).send(JSON.stringify({
+ running: bufferState.running,
+ paused: bufferState.paused,
+ count: bufferState.data.length,
+ buffer: "This isn't a great idea...", // TODO: FIX <<
+ }));
+
+ if (gConf.get('debug')) {
+ console.log('>RESP', req.route.path, '200');
}
- );
- });
- }, true); // Skipbuffer on setheight!
-
- return true; // Don't finish the response till after move back ^^^
- }
+ }
+ );
+ });
+ }, true); // Skipbuffer on setheight!
- // Plain resume.
- buffer.resume();
+ return true; // Don't finish the response till after move back ^^^
}
- // In case paused with 0 items in buffer...
- if (!buffer.newlyPaused || buffer.data.length === 0) {
- buffer.newlyPaused = false;
- cncserver.sockets.sendBufferVars();
- return {
- code: 200,
- body: {
- running: buffer.running,
- paused: buffer.paused,
- count: buffer.data.length,
- },
- };
- }
+ // Plain resume.
+ resume();
+ }
- // Buffer isn't empty and we're newly paused
- // Wait until last item has finished before returning
- console.log('Waiting for last item to finish...');
-
- buffer.pauseCallback = () => {
- res.status(200).send(JSON.stringify({
- running: buffer.running,
- paused: buffer.paused,
- count: buffer.length,
- }));
- cncserver.sockets.sendBufferVars();
- buffer.newlyPaused = false;
-
- if (cncserver.settings.gConf.get('debug')) {
- console.log('>RESP', req.route.path, 200);
- }
+ // In case paused with 0 items in buffer...
+ if (!bufferState.newlyPaused || bufferState.data.length === 0) {
+ bufferState.newlyPaused = false;
+ sendBufferVars();
+ return {
+ code: 200,
+ body: {
+ running: bufferState.running,
+ paused: bufferState.paused,
+ count: bufferState.data.length,
+ },
};
-
- return true; // Don't finish the response till later
}
- if (req.route.method === 'post') {
- // Create a status message/callback and shuck it into the buffer
- if (typeof req.body.message === 'string') {
- cncserver.run('message', req.body.message);
- return [200, 'Message added to buffer'];
+ // Buffer isn't empty and we're newly paused
+ // Wait until last item has finished before returning
+ console.log('Waiting for last item to finish...');
+
+ bufferState.pauseCallback = () => {
+ res.status(200).send(JSON.stringify({
+ running: bufferState.running,
+ paused: bufferState.paused,
+ count: bufferState.length,
+ }));
+ sendBufferVars();
+ bufferState.newlyPaused = false;
+
+ if (gConf.get('debug')) {
+ console.log('>RESP', req.route.path, 200);
}
+ };
- if (typeof req.body.callback === 'string') {
- cncserver.run('callbackname', req.body.callback);
- return [200, 'Callback name added to buffer'];
- }
+ return true; // Don't finish the response till later
+ }
- return [400, '/v2/buffer POST only accepts "message" or "callback"'];
+ if (req.route.method === 'post') {
+ // Create a status message/callback and shuck it into the buffer
+ if (typeof req.body.message === 'string') {
+ run('message', req.body.message);
+ return [200, 'Message added to buffer'];
}
- if (req.route.method === 'delete') {
- cncserver.binder.trigger('buffer.clear');
- buffer.clear();
- return [200, 'Buffer Cleared'];
+ if (typeof req.body.callback === 'string') {
+ run('callbackname', req.body.callback);
+ return [200, 'Callback name added to buffer'];
}
- // Error to client for unsupported request types.
- return false;
- };
+ return [400, '/v2/buffer POST only accepts "message" or "callback"'];
+ }
+
+ if (req.route.method === 'delete') {
+ trigger('buffer.clear');
+ clear();
+ return [200, 'Buffer Cleared'];
+ }
- return handlers;
+ // Error to client for unsupported request types.
+ return false;
};
diff --git a/src/components/core/comms/api/cncserver.api.colors.js b/src/components/core/comms/api/cncserver.api.colors.js
index cdce1fca..bc391d7a 100644
--- a/src/components/core/comms/api/cncserver.api.colors.js
+++ b/src/components/core/comms/api/cncserver.api.colors.js
@@ -1,92 +1,159 @@
/**
* @file CNCServer ReSTful API endpoint module for colorset management.
*/
-const handlers = {};
+import * as colors from 'cs/drawing/colors';
+import { validateData } from 'cs/schemas';
+import { err } from 'cs/rest';
+import {
+ deletePreset,
+ isValidPreset,
+ merge,
+ singleLineString
+} from 'cs/utils';
-module.exports = (cncserver) => {
- handlers['/v2/colors'] = (req) => {
- const { drawing: { colors } } = cncserver;
+export const handlers = {};
- if (req.route.method === 'get') { // Get current colorset and
- return {
- code: 200,
- body: {
- set: colors.set,
- presets: colors.listPresets(req.t),
- },
- };
+// Primary Colorset group handler.
+handlers['/v2/colors'] = (req, res) => {
+ // Standard post resolve for colors requests.
+ function postResolve() {
+ res.status(200).send({
+ set: colors.getCurrentSet(req.t), // The current colorset.
+ presets: colors.listPresets(req.t), // List all presets.
+ customs: colors.customKeys(), // List of custom preset machine names.
+ internals: colors.internalKeys(), // List of internal preset machine names.
+ invalidSets: colors.invalidPresets(), // Keys of invalid colorsets.
+ });
+ }
+
+ // Get current colorset and presets.
+ if (req.route.method === 'get') {
+ postResolve();
+ return true; // Tell endpoint wrapper we'll handle the GET response.
+ }
+
+ // Add color, or replace set from preset.
+ if (req.route.method === 'post') {
+ // Set via preset, only err here is 404 preset not found.
+ if (req.body.preset) {
+ colors.applyPreset(req.body.preset, req.t)
+ .then(postResolve)
+ .catch(err(res, 404));
+ } else {
+ // Validate data and add color item, or error out.
+ validateData('color', req.body, true)
+ .then(isValidPreset('implement', true))
+ .then(colors.add)
+ .then(postResolve)
+ .catch(err(res));
}
- if (req.route.method === 'post') { // Set color/preset
- if (req.body.preset) {
- if (!colors.applyPreset(req.body.preset)) {
- return {
- code: 404,
- body: {
- status: `Preset with id of '${req.body.preset}' not found in preset list.`,
- validOptions: Object.keys(colors.presets),
- },
- };
- }
- } else if (!req.body.id || !req.body.name || !req.body.color) {
- return [
- 406,
- 'Must include valid id, name and color, see color documentation API docs',
- ];
- } else if (!colors.add(req.body)) {
- return [
- 406,
- `Color with id ${req.body.id} already exists, update it directly or change id`,
- ];
- }
+ return true; // Tell endpoint wrapper we'll handle the POST response.
+ }
+ // Allow deleting of custom presets.
+ if (req.route.method === 'delete' && req.body.preset) {
+ const customKeys = colors.customKeys();
+ if (!customKeys.includes(req.body.preset)) {
return {
- code: 200,
+ code: 406,
body: {
- set: colors.set,
- presets: colors.presets,
+ status: 'Only custom or overridden presets can be deleted.',
+ allowedValues: customKeys,
},
};
}
- return false;
- };
+ // Preset found, delete it!
+ deletePreset('colorsets', req.body.preset);
+ postResolve();
+ return true; // Tell endpoint wrapper we'll handle the GET response.
+ }
+
+ // Change set options directly.
+ if (req.route.method === 'patch') {
+ // If item data is attempted to be changed here, give a specific message for it.
+ if (req.body.items) {
+ err(res, 406)(
+ new Error(singleLineString`Patching the colors endpoint can only edit the
+ current set details, not individual colorset items.
+ Patch to /v2/colors/[ID] to edit a colorset item.`)
+ );
+ }
+
+ // Merge with current set, validate data, then edit.
+ const set = colors.getCurrentSet();
+ delete set.items;
+ const mergedItem = merge(set, req.body);
+ validateData('colors', mergedItem, true)
+ .then(isValidPreset('implement'))
+ .then(isValidPreset('toolset'))
+ .then(colors.editSet)
+ .then(postResolve)
+ .catch(err(res));
- handlers['/v2/colors/:colorID'] = (req) => {
- // Sanity check color ID
- const { colorID } = req.params;
- const { drawing: { colors } } = cncserver;
- const color = colors.getColor(colorID);
+ return true; // Tell endpoint wrapper we'll handle the PATCH response.
+ }
- if (!color) {
+ return false;
+};
+
+// Colorset item handler.
+handlers['/v2/colors/:colorID'] = (req, res) => {
+ // Sanity check color ID
+ const { colorID } = req.params;
+
+ // TODO: Apply translation here?...
+ const color = colors.getColor(colorID);
+
+ if (!color) {
+ return {
+ code: 404,
+ body: {
+ status: `Color with id of '${colorID}' not found in color set.`,
+ allowedValues: colors.getIDs(),
+ },
+ };
+ }
+
+ // Display the color info.
+ if (req.route.method === 'get') {
+ return { code: 200, body: color };
+ }
+
+ // Patch item.
+ if (req.route.method === 'patch') {
+ // Error out if client is trying to change the ID in the request.
+ if (req.body.id && req.body.id !== colorID) {
return {
- code: 404,
+ code: 406,
body: {
- status: `Color with id of '${colorID}' not found in color set.`,
- validOptions: colors.getIDs(),
+ status: 'error',
+ message: singleLineString`You cannot rewrite a colorset ID in a patch.
+ Delete item and recreate.`,
},
};
}
- // Display the color info.
- if (req.route.method === 'get') {
- return { code: 200, body: color };
- }
+ // Merge the incoming data with the existing object as we don't need delta.
+ const mergedItem = merge(color, req.body);
- // Update color info
- if (req.route.method === 'put') {
- return { code: 200, body: colors.update(colorID, req.body) };
- }
+ // Validate the request data against the schema before continuing.
+ validateData('color', mergedItem, true)
+ .then(isValidPreset('implement', true))
+ .then(colors.edit)
+ .then(finalItem => { res.status(200).send(finalItem); })
+ .catch(err(res));
- // Delete color
- if (req.route.method === 'delete') {
- colors.delete(color);
- return { code: 200, body: { set: colors.set, presets: colors.presets } };
- }
+ return true; // Tell endpoint wrapper we'll handle the PATCH response.
+ }
- // Error to client for unsupported request types.
- return false;
- };
+ // Delete color
+ if (req.route.method === 'delete') {
+ colors.deleteColor(colorID);
+ return { code: 200, body: { status: 'success' } };
+ }
- return handlers;
+ // Error to client for unsupported request types.
+ return false;
};
diff --git a/src/components/core/comms/api/cncserver.api.content.js b/src/components/core/comms/api/cncserver.api.content.js
index 052024e7..2e529a62 100644
--- a/src/components/core/comms/api/cncserver.api.content.js
+++ b/src/components/core/comms/api/cncserver.api.content.js
@@ -1,101 +1,82 @@
/**
* @file CNCServer ReSTful API endpoint for high level project content.
*/
-const handlers = {};
-
-module.exports = (cncserver) => {
- handlers['/v2/content'] = (req, res) => {
- const { content } = cncserver;
-
- // Enumerate content.
- if (req.route.method === 'get') {
- return {
- code: 200,
- body: {
- items: content.getItems(),
- },
- };
- }
-
- // Create a piece of content.
- if (req.route.method === 'post') {
- // Validate the request data against the schema before continuing.
- cncserver.schemas.validateData('content', req.body)
- .then(body => content.normalizeInput(body))
- .then(content.addItem)
- .then((item) => { res.status(200).send(item); })
- .catch((err) => {
- const errBody = {
- status: 'error',
- message: err.message || err,
- };
-
- if (err.stack) errBody.stack = err.stack.split('\n');
- res.status(406).send(errBody);
- });
-
- return true; // Tell endpoint wrapper we'll handle the response
- }
-
- // Error to client for unsupported request types.
- return false;
- };
-
-
- // Individual content management.
- handlers['/v2/content/:hash'] = (req, res) => {
- const { hash } = req.params;
- const { content, utils } = cncserver;
- const item = content.getResponseItem(hash);
-
- // Sanity check hash lookup.
- if (!item) {
- return [404, `Content with hash ID "${hash}" not found`];
- }
+import * as content from 'cs/content';
+import { merge } from 'cs/utils';
+import { validateData } from 'cs/schemas';
+import { err } from 'cs/rest';
+
+export const handlers = {};
+
+handlers['/v2/content'] = (req, res) => {
+ // Enumerate content.
+ if (req.route.method === 'get') {
+ return {
+ code: 200,
+ body: {
+ items: content.getItems(),
+ },
+ };
+ }
+
+ // Create a piece of content.
+ if (req.route.method === 'post') {
+ // Validate the request data against the schema before continuing.
+ validateData('content', req.body)
+ .then(body => content.normalizeInput(body))
+ .then(content.addItem)
+ .then(item => { res.status(200).send(item); })
+ .catch(err(res));
+
+ return true; // Tell endpoint wrapper we'll handle the response
+ }
+
+ // Error to client for unsupported request types.
+ return false;
+};
- // Display item.
- if (req.route.method === 'get') {
- return {
- code: 200,
- body: item,
- };
+// Individual content management.
+handlers['/v2/content/:hash'] = (req, res) => {
+ const { hash } = req.params;
+ const item = content.getResponseItem(hash);
+
+ // Sanity check hash lookup.
+ if (!item) {
+ return [404, `Content with hash ID "${hash}" not found`];
+ }
+
+ // Display item.
+ if (req.route.method === 'get') {
+ return {
+ code: 200,
+ body: item,
+ };
+ }
+
+ // Patch item.
+ if (req.route.method === 'patch') {
+ // Validate the request data against the schema before continuing.
+ const mergedItem = merge(item, req.body);
+
+ // If a new source is given, replace the entire item in merged.
+ if (req.body.source) {
+ mergedItem.source = req.body.source;
}
- // Patch item.
- if (req.route.method === 'patch') {
- // Validate the request data against the schema before continuing.
- const mergedItem = utils.merge(item, req.body);
-
- // If a new source is given, replace the entire item in merged.
- if (req.body.source) {
- mergedItem.source = req.body.source;
- }
-
- cncserver.schemas.validateData('content', mergedItem)
- .then(() => content.editItem(item, req.body, mergedItem))
- .then((finalItem) => { res.status(200).send(finalItem); })
- .catch((err) => {
- const errBody = {
- status: 'error',
- message: err.message || err,
- };
+ validateData('content', mergedItem)
+ .then(() => content.editItem(item, req.body, mergedItem))
+ .then(finalItem => { res.status(200).send(finalItem); })
+ .catch(err(res));
- if (err.stack) errBody.stack = err.stack.split('\n');
- res.status(406).send(errBody);
- });
-
- return true; // Tell endpoint wrapper we'll handle the response
- }
-
- // Remove item.
- if (req.route.method === 'delete') {
- content.removeItem(hash);
- return [200, `Content identified by hash "${hash}" removed`];
- }
+ return true; // Tell endpoint wrapper we'll handle the response
+ }
- // Error to client for unsupported request types.
- return false;
- };
+ // Remove item.
+ if (req.route.method === 'delete') {
+ content.removeItem(hash);
+ return [200, `Content identified by hash "${hash}" removed`];
+ }
- return handlers;
+ // Error to client for unsupported request types.
+ return false;
};
diff --git a/src/components/core/comms/api/cncserver.api.implements.js b/src/components/core/comms/api/cncserver.api.implements.js
new file mode 100644
index 00000000..5ac2812a
--- /dev/null
+++ b/src/components/core/comms/api/cncserver.api.implements.js
@@ -0,0 +1,125 @@
+/**
+ * @file CNCServer ReSTful API endpoint module for implement management.
+ */
+import {
+ listPresets,
+ customKeys,
+ internalKeys,
+ edit,
+ get,
+ add,
+ deleteImplement,
+ IMPLEMENT_PARENT
+} from 'cs/drawing/implements';
+import { colors } from 'cs/drawing';
+import { validateData } from 'cs/schemas';
+import { singleLineString, merge } from 'cs/utils';
+import { err } from 'cs/rest';
+
+export const handlers = {};
+
+// Primary group handler.
+handlers['/v2/implements'] = (req, res) => {
+ // Standard post resolve for colors requests.
+ function postResolve() {
+ res.status(200).send({
+ presets: listPresets(req.t),
+ customs: customKeys(), // List of custom preset machine names.
+ internals: internalKeys(), // List of internal preset machine names.
+ });
+ }
+
+ // Get presets.
+ if (req.route.method === 'get') {
+ postResolve();
+ return true; // Tell endpoint wrapper we'll handle the GET response.
+ }
+
+ // Add implement.
+ if (req.route.method === 'post') {
+ // Validate data and add implement, or error out.
+ validateData('drawing.implements', req.body, true)
+ .then(add)
+ .then(postResolve)
+ .catch(err(res));
+
+ return true; // Tell endpoint wrapper we'll handle the POST response.
+ }
+
+ // Error to client for unsupported request types.
+ return false;
+};
+
+// Item handler.
+handlers['/v2/implements/:implementName'] = (req, res) => {
+ // Sanity check color ID
+ let { implementName } = req.params;
+
+ // Allow inherit to redirect to colorset parent default.
+ if (implementName === IMPLEMENT_PARENT) {
+ implementName = colors.set.implement;
+ }
+
+ const implement = get(implementName);
+
+ if (!implement) {
+ return {
+ code: 404,
+ body: {
+ status: `Implement with name of '${implementName}' not found.`,
+ validOptions: Object.keys(listPresets(req.t)),
+ },
+ };
+ }
+
+ // Display the item.
+ if (req.route.method === 'get') {
+ return { code: 200, body: implement };
+ }
+
+ // Patch item. Editing internal presets saves a new custom with override data.
+ if (req.route.method === 'patch') {
+ if (req.body.name) {
+ return {
+ code: 406,
+ body: {
+ status: 'error',
+ message: singleLineString`You cannot rewrite an implement name in a patch.
+ Delete item and recreate.`,
+ },
+ };
+ }
+
+ // Merge the incoming data with the existing object as we don't need delta.
+ const mergedItem = merge(implement, req.body);
+
+ // Validate the request data against the schema before continuing.
+ validateData('implements', mergedItem, true)
+ .then(edit)
+ .then(finalItem => { res.status(200).send(finalItem); })
+ .catch(err(res));
+
+ return true; // Tell endpoint wrapper we'll handle the PATCH response.
+ }
+
+ // Delete item.
+ if (req.route.method === 'delete') {
+ // Notify client that they cannot delete internal presets.
+ if (!get(implementName, true)) {
+ return {
+ code: 406,
+ body: {
+ status: 'You cannot delete internal presets.',
+ validOptions: Object.keys(listPresets(req.t, true)),
+ },
+ };
+ }
+
+ // Actually delete.
+ deleteImplement(implementName);
+ return { code: 200, body: { status: 'success' } };
+ }
+
+ // Error to client for unsupported request types.
+ return false;
+};
diff --git a/src/components/core/comms/api/cncserver.api.motors.js b/src/components/core/comms/api/cncserver.api.motors.js
index f8ac395f..64ab98c5 100644
--- a/src/components/core/comms/api/cncserver.api.motors.js
+++ b/src/components/core/comms/api/cncserver.api.motors.js
@@ -1,62 +1,63 @@
/**
* @file CNCServer ReSTful API endpoint module for pen state management.
*/
-const handlers = {};
-
-module.exports = (cncserver) => {
- handlers['/v2/motors'] = function motorsMain(req) {
- // Disable/unlock motors
- if (req.route.method === 'delete') {
- cncserver.run('custom', cncserver.buffer.cmdstr('disablemotors'));
- return [201, 'Disable Queued'];
- }
-
- if (req.route.method === 'put') {
- if (parseInt(req.body.reset, 10) === 1) {
- // ZERO motor position to park position
- const park = cncserver.utils.centToSteps(cncserver.settings.bot.park, true);
- // It is at this point assumed that one would *never* want to do this as
- // a buffered operation as it implies *manually* moving the bot to the
- // parking location, so we're going to man-handle the variables a bit.
- // completely not repecting the buffer (as really, it should be empty)
-
- // EDIT: There are plenty of queued operations that don't involve moving
- // the pen that make sense to have in the buffer after a zero operation,
- // not to mention if there are items in the queue during a pause, we
- // should still want the ability to do this.
-
- // Set tip of buffer to current
- cncserver.pen.forceState({
+import { forceState } from 'cs/pen';
+import * as actualPen from 'cs/actualPen';
+import { bot, gConf } from 'cs/settings';
+import { cmdstr } from 'cs/buffer';
+import { centToSteps } from 'cs/utils';
+import run from 'cs/run';
+
+export const handlers = {};
+
+handlers['/v2/motors'] = function motorsMain(req) {
+ // Disable/unlock motors
+ if (req.route.method === 'delete') {
+ run('custom', cmdstr('disablemotors'));
+ return [201, 'Disable Queued'];
+ }
+
+ if (req.route.method === 'put') {
+ if (parseInt(req.body.reset, 10) === 1) {
+ // ZERO motor position to park position
+ const park = centToSteps(bot.park, true);
+ // It is at this point assumed that one would *never* want to do this as
+ // a buffered operation as it implies *manually* moving the bot to the
+ // parking location, so we're going to man-handle the variables a bit.
+ // completely not repecting the buffer (as really, it should be empty)
+
+ // EDIT: There are plenty of queued operations that don't involve moving
+ // the pen that make sense to have in the buffer after a zero operation,
+ // not to mention if there are items in the queue during a pause, we
+ // should still want the ability to do this.
+
+ // Set tip of buffer to current
+ forceState({
+ x: park.x,
+ y: park.y,
+ });
+
+ run('callback', () => {
+ // Set actualPen position. This is the ONLY place we set this value
+ // without a movement, because it's assumed to have been moved there
+ // physically by a user. Also we're assuming they did it instantly!
+ actualPen.forceState({
x: park.x,
y: park.y,
+ lastDuration: 0,
});
- cncserver.run('callback', () => {
- // Set actualPen position. This is the ONLY place we set this value
- // without a movement, because it's assumed to have been moved there
- // physically by a user. Also we're assuming they did it instantly!
- cncserver.actualPen.forceState({
- x: park.x,
- y: park.y,
- lastDuration: 0,
- });
-
- cncserver.sockets.sendPenUpdate();
-
- if (cncserver.settings.gConf.get('debug')) {
- console.log('Motor offset reset to park position');
- }
- });
-
- return [201, 'Motor offset reset to park position queued'];
- }
+ if (gConf.get('debug')) {
+ console.log('Motor offset reset to park position');
+ }
+ });
- return [406, 'Input not acceptable, see API spec for details.'];
+ return [201, 'Motor offset reset to park position queued'];
}
- // Error to client for unsupported request types.
- return false;
- };
+ return [406, 'Input not acceptable, see API spec for details.'];
+ }
- return handlers;
+ // Error to client for unsupported request types.
+ return false;
};
diff --git a/src/components/core/comms/api/cncserver.api.pen.js b/src/components/core/comms/api/cncserver.api.pen.js
index d1e8a972..0de33b4a 100644
--- a/src/components/core/comms/api/cncserver.api.pen.js
+++ b/src/components/core/comms/api/cncserver.api.pen.js
@@ -1,90 +1,90 @@
/**
* @file CNCServer ReSTful API endpoint module for pen state management.
*/
-const handlers = {};
-
-module.exports = (cncserver) => {
- handlers['/v2/pen'] = function penMain(req, res) {
- if (req.route.method === 'put') {
- // Verify absolute measurement input.
- if (req.body.abs) {
- if (!['in', 'mm'].includes(req.body.abs)) {
- return [
- 406,
- 'Input not acceptable, absolute measurement must be: in, mm',
- ];
- }
-
- if (!cncserver.settings.bot.maxAreaMM) {
- return [
- 406,
- 'Input not acceptable, bot does not support absolute position.',
- ];
- }
+import { gConf, bot } from 'cs/settings';
+import { setPen, state, park } from 'cs/pen';
+import * as actualPen from 'cs/actualPen';
+
+export const handlers = {};
+
+handlers['/v2/pen'] = function penMain(req, res) {
+ if (req.route.method === 'put') {
+ // Verify absolute measurement input.
+ if (req.body.abs) {
+ if (!['in', 'mm'].includes(req.body.abs)) {
+ return [
+ 406,
+ 'Input not acceptable, absolute measurement must be: in, mm',
+ ];
}
- // SET/UPDATE pen status
- cncserver.pen.setPen(req.body, (stat) => {
- let code = 202;
- let body = {};
-
- if (!stat) {
- code = 500;
- body.status = 'Error setting pen!';
- } else {
- // Wait return.
- if (req.body.waitForCompletion) {
- code = 200;
- }
- body = cncserver.pen.state;
- }
-
- body = JSON.stringify(body);
- res.status(code).send(body);
-
- if (cncserver.settings.gConf.get('debug')) {
- console.log('>RESP', req.route.path, code, body);
- }
- });
-
- return true; // Tell endpoint wrapper we'll handle the response
+ if (!bot.maxAreaMM) {
+ return [
+ 406,
+ 'Input not acceptable, bot does not support absolute position.',
+ ];
+ }
}
- if (req.route.method === 'delete') {
- // Reset pen to defaults (park)
- cncserver.pen.park(req.body.skipBuffer, (stat) => {
- let code = 200;
- let body = {};
-
- if (!stat) {
- code = 500;
- body.status = 'Error parking pen!';
- } else {
- body = cncserver.pen.state;
+ // SET/UPDATE pen status
+ setPen(req.body, stat => {
+ let code = 202;
+ let body = {};
+
+ if (!stat) {
+ code = 500;
+ body.status = 'Error setting pen!';
+ } else {
+ // Wait return.
+ if (req.body.waitForCompletion) {
+ code = 200;
}
+ body = state;
+ }
- body = JSON.stringify(body);
- res.status(code).send(body);
+ body = JSON.stringify(body);
+ res.status(code).send(body);
- if (cncserver.settings.gConf.get('debug')) {
- console.log('>RESP', req.route.path, code, body);
- }
- });
+ if (gConf.get('debug')) {
+ console.log('>RESP', req.route.path, code, body);
+ }
+ });
+
+ return true; // Tell endpoint wrapper we'll handle the response
+ }
+
+ if (req.route.method === 'delete') {
+ // Reset pen to defaults (park)
+ park(req.body.skipBuffer, stat => {
+ let code = 200;
+ let body = {};
+
+ if (!stat) {
+ code = 500;
+ body.status = 'Error parking pen!';
+ } else {
+ body = state;
+ }
- return true; // Tell endpoint wrapper we'll handle the response
- }
+ body = JSON.stringify(body);
+ res.status(code).send(body);
- if (req.route.method === 'get') {
- if (req.query.actual) {
- return { code: 200, body: cncserver.actualPen.state };
+ if (gConf.get('debug')) {
+ console.log('>RESP', req.route.path, code, body);
}
+ });
+
+ return true; // Tell endpoint wrapper we'll handle the response
+ }
- return { code: 200, body: cncserver.pen.state };
+ if (req.route.method === 'get') {
+ if (req.query.actual) {
+ return { code: 200, body: actualPen.state };
}
- // Error to client for unsupported request types.
- return false;
- };
+ return { code: 200, body: state };
+ }
- return handlers;
+ // Error to client for unsupported request types.
+ return false;
};
diff --git a/src/components/core/comms/api/cncserver.api.print.js b/src/components/core/comms/api/cncserver.api.print.js
new file mode 100644
index 00000000..f90cfc1b
--- /dev/null
+++ b/src/components/core/comms/api/cncserver.api.print.js
@@ -0,0 +1,23 @@
+/**
+ * @file CNCServer ReSTful API endpoint module for pen state management.
+ */
+import * as print from 'cs/print';
+
+export const handlers = {};
+
+// TODO:
+// - GET /: List all prints for all projects
+// - GET /[PROJECT HASH]: All prints for project
+// - GET /[PROJECT HASH]/[PRINT_HASH]: All data for specific print
+// - PUT /[PROJECT HASH]/[PRINT_HASH]: Edit only title, desc for a print.
+// - POST /[PROJECT-HASH] or /: Create print render with given settings.
+// - DELETE /[PROJECT HASH]/[PRINT_HASH]: Delete a given print
+handlers['/v2/print'] = function printMain(req) {
+ if (req.route.method === 'get') {
+ print.getPrintData();
+ return [200, 'Check the file'];
+ }
+
+ // Error to client for unsupported request types.
+ return false;
+};
diff --git a/src/components/core/comms/api/cncserver.api.projects.js b/src/components/core/comms/api/cncserver.api.projects.js
index 7bd0f9ca..23d4625a 100644
--- a/src/components/core/comms/api/cncserver.api.projects.js
+++ b/src/components/core/comms/api/cncserver.api.projects.js
@@ -1,143 +1,120 @@
/**
* @file CNCServer ReSTful API endpoint for high level project management.
*/
-const handlers = {};
-
-module.exports = (cncserver) => {
- handlers['/v2/projects'] = (req, res) => {
- const { projects } = cncserver;
-
- // Standard projects api return.
- const projectsBody = () => ({
- current: projects.getCurrentHash(),
- rendering: projects.getRenderingState(),
- printing: projects.getPrintingState(),
- items: projects.getItems(),
- });
-
- // Enumerate projects.
- if (req.route.method === 'get') {
- return { code: 200, body: projectsBody() };
- }
-
- // Create a project.
- if (req.route.method === 'post') {
- // Validate the request data against the schema before continuing.
- cncserver.schemas.validateData('projects', req.body, true)
- .then(body => projects.addItem(body))
- .then((item) => { res.status(200).send(item); })
- .catch((err) => {
- console.error('Error on Projects request:', err);
- const errBody = {
- status: 'error',
- message: err,
- };
-
- if (err.stack) errBody.stack = err.stack;
- res.status(406).send(errBody);
- });
-
- return true; // Tell endpoint wrapper we'll handle the response
- }
+import * as projects from 'cs/projects';
+import { validateData } from 'cs/schemas';
+import { err } from 'cs/rest';
+
+export const handlers = {};
+
+handlers['/v2/projects'] = (req, res) => {
+ // Standard projects api return.
+ const projectsBody = () => ({
+ current: projects.getCurrentHash(),
+ rendering: projects.getRenderingState(),
+ printing: projects.getPrintingState(),
+ items: projects.getItems(),
+ });
+
+ // Enumerate projects.
+ if (req.route.method === 'get') {
+ return { code: 200, body: projectsBody() };
+ }
+
+ // Create a project.
+ if (req.route.method === 'post') {
+ // Validate the request data against the schema before continuing.
+ validateData('projects', req.body, true)
+ .then(projects.addItem)
+ .then(item => { res.status(200).send(item); })
+ .catch(err(res));
+
+ return true; // Tell endpoint wrapper we'll handle the response
+ }
+
+ // Allow modification of general project management state:
+ // "current": Open a project by patching "current" key with project hash.
+ // "rendering": Start rending by setting to true.
+ // "printing": Start printing render by setting to true.
+ if (req.route.method === 'patch') {
+ const { current, rendering, printing } = req.body || {};
- // Allow modification of general project management state:
- // "current": Open a project by patching "current" key with project hash.
- // "rendering": Start rending by setting to true.
- // "printing": Start printing render by setting to true.
- if (req.route.method === 'patch') {
- const { current, rendering, printing } = req.body || {};
-
- // Sanity check hash lookup.
- if (current) {
- if (!projects.items.has(current)) {
- return [404, `Project with hash ID "${current}" not found`];
- }
- if (projects.getCurrentHash() === current) {
- return [302, 'Project already loaded.'];
- }
-
- // Actually open the project.
- projects.open(current);
- } else if (rendering !== undefined) {
- projects.setRenderingState(rendering);
- } else if (printing !== undefined) {
- projects.setPrintingState(printing);
- } else {
- // Nothing sent right, let em know.
- return [406, 'Patch either "current" hash to open, or new "rendering" or "printing" state.'];
+ // Sanity check hash lookup.
+ if (current) {
+ if (!projects.items.has(current)) {
+ return [404, `Project with hash ID "${current}" not found`];
+ }
+ if (projects.getCurrentHash() === current) {
+ return [302, 'Project already loaded.'];
}
- // Return the full new projects return body.
- return { code: 200, body: projectsBody() };
- }
-
- // Error to client for unsupported request types.
- return false;
- };
-
-
- // Individual project management.
- handlers['/v2/projects/:hash'] = (req, res) => {
- const { projects } = cncserver;
-
- // Shortcut "current" hash lookup.
- const hash = req.params.hash === 'current'
- ? projects.getCurrentHash()
- : req.params.hash;
-
- // Sanity check hash lookup.
- if (!projects.items.has(hash)) {
- return [404, `Project with hash ID "${hash}" not found`];
+ // Actually open the project.
+ projects.openProject(current);
+ } else if (rendering !== undefined) {
+ projects.setRenderingState(rendering);
+ } else if (printing !== undefined) {
+ projects.setPrintingState(printing);
+ } else {
+ // Nothing sent right, let em know.
+ return [
+ 406,
+ 'Patch either "current" hash to open, or new "rendering" or "printing" state.',
+ ];
}
- // Get the project via validated hash.
- const project = projects.getResponseItem(hash);
+ // Return the full new projects return body.
+ return { code: 200, body: projectsBody() };
+ }
- // Display item.
- if (req.route.method === 'get') {
- return { code: 200, body: project };
- }
+ // Error to client for unsupported request types.
+ return false;
+};
- // Patch item.
- if (req.route.method === 'patch') {
- // Validate the request data against the schema before continuing.
- const mergedProject = { ...project, ...req.body };
- cncserver.schemas.validateData('projects', mergedProject)
- .then(() => projects.editItem(project, req.body))
- .then((item) => { res.status(200).send(item); })
- .catch((err) => {
- const errBody = {
- status: 'error',
- message: err,
- };
-
- if (err.stack) errBody.stack = err.stack;
- res.status(406).send(errBody);
+// Individual project management.
+handlers['/v2/projects/:hash'] = (req, res) => {
+ // Shortcut "current" hash lookup.
+ const hash = req.params.hash === 'current'
+ ? projects.getCurrentHash()
+ : req.params.hash;
+
+ // Sanity check hash lookup.
+ if (!projects.items.has(hash)) {
+ return [404, `Project with hash ID "${hash}" not found`];
+ }
+
+ // Get the project via validated hash.
+ const project = projects.getResponseItem(hash);
+
+ // Display item.
+ if (req.route.method === 'get') {
+ return { code: 200, body: project };
+ }
+
+ // Patch item.
+ if (req.route.method === 'patch') {
+ // Validate the request data against the schema before continuing.
+ const mergedProject = { ...project, ...req.body };
+ validateData('projects', mergedProject)
+ .then(() => projects.editItem(project, req.body))
+ .then(item => { res.status(200).send(item); })
+ .catch(err(res));
+
+ return true; // Tell endpoint wrapper we'll handle the response
+ }
+
+ // Remove item.
+ if (req.route.method === 'delete') {
+ projects.removeItem(hash).then(() => {
+ res
+ .status(200)
+ .send({
+ status: `Project identified by "${hash}" moved to trash directory`,
});
+ }).catch(err(res, 500));
- return true; // Tell endpoint wrapper we'll handle the response
- }
-
- // Remove item.
- if (req.route.method === 'delete') {
- projects.removeItem(hash).then(() => {
- res.status(200).send({ status: `Project identified by "${hash}" moved to trash directory` });
- }).catch((err) => {
- const errBody = {
- status: 'error',
- message: err,
- };
-
- if (err.stack) errBody.stack = err.stack;
- res.status(500).send(errBody);
- });
-
- return true; // Tell endpoint wrapper we'll handle the response
- }
-
- // Error to client for unsupported request types.
- return false;
- };
+ return true; // Tell endpoint wrapper we'll handle the response
+ }
- return handlers;
+ // Error to client for unsupported request types.
+ return false;
};
diff --git a/src/components/core/comms/api/cncserver.api.serial.js b/src/components/core/comms/api/cncserver.api.serial.js
new file mode 100644
index 00000000..0c77c25e
--- /dev/null
+++ b/src/components/core/comms/api/cncserver.api.serial.js
@@ -0,0 +1,19 @@
+/**
+ * @file CNCServer ReSTful API endpoint module for direct serial execution.
+ */
+import { getSerialValueRaw } from 'cs/ipc';
+
+export const handlers = {};
+
+handlers['/v2/serial'] = function serialMain(req, res) {
+ if (req.route.method === 'post') {
+ console.log('GOT SERIAL', req.body);
+ getSerialValueRaw(req.body.command).then(message => {
+ res.status(200).send(message);
+ });
+ return true; // Tell endpoint wrapper we'll handle the response
+ }
+
+ // Error to client for unsupported request types.
+ return false;
+};
diff --git a/src/components/core/comms/api/cncserver.api.settings.js b/src/components/core/comms/api/cncserver.api.settings.js
index dbea2dcb..eedeb320 100644
--- a/src/components/core/comms/api/cncserver.api.settings.js
+++ b/src/components/core/comms/api/cncserver.api.settings.js
@@ -1,69 +1,67 @@
/**
* @file CNCServer ReSTful API endpoint module for settings management.
*/
-const handlers = {};
+import { botConf, gConf } from 'cs/settings';
-module.exports = (cncserver) => {
- handlers['/v2/settings'] = function settingsGet(req) {
- if (req.route.method === 'get') { // Get list of tools
- return {
- code: 200,
- body: {
- global: '/v2/settings/global',
- bot: '/v2/settings/bot',
- },
- };
- }
+export const handlers = {};
- return false;
- };
+handlers['/v2/settings'] = function settingsGet(req) {
+ if (req.route.method === 'get') { // Get list of tools
+ return {
+ code: 200,
+ body: {
+ global: '/v2/settings/global',
+ bot: '/v2/settings/bot',
+ },
+ };
+ }
- handlers['/v2/settings/:type'] = function settingsMain(req) {
- // TODO: Refactor most/all of this to be more consistent, and pull useful
- // cleaned up config from cnccserver.settings.[x]
+ return false;
+};
- // Sanity check type
- const setType = req.params.type;
- if (!['global', 'bot'].includes(setType)) {
- return [404, 'Settings group not found'];
- }
+handlers['/v2/settings/:type'] = function settingsMain(req) {
+ // TODO: Refactor most/all of this to be more consistent, and pull useful
+ // cleaned up config from cnccserver.settings.[x]
- let conf = cncserver.settings.botConf;
- if (setType === 'global') {
- conf = cncserver.settings.gConf;
- }
+ // Sanity check type
+ const setType = req.params.type;
+ if (!['global', 'bot'].includes(setType)) {
+ return [404, 'Settings group not found'];
+ }
- function getSettings() {
- let out = {};
- // Clean the output for global as it contains all commandline env vars!
- if (setType === 'global') {
- const g = conf.get();
- for (const [key, value] of Object.entries(g)) {
- if (key === 'botOverride') {
- break;
- }
- out[key] = value;
+ let conf = botConf;
+ if (setType === 'global') {
+ conf = gConf;
+ }
+
+ function getSettings() {
+ let out = {};
+ // Clean the output for global as it contains all commandline env vars!
+ if (setType === 'global') {
+ const g = conf.get();
+ for (const [key, value] of Object.entries(g)) {
+ if (key === 'botOverride') {
+ break;
}
- } else {
- out = conf.get();
+ out[key] = value;
}
- return out;
+ } else {
+ out = conf.get();
}
+ return out;
+ }
- // Get the full list for the type
- if (req.route.method === 'get') {
- return { code: 200, body: getSettings() };
+ // Get the full list for the type
+ if (req.route.method === 'get') {
+ return { code: 200, body: getSettings() };
+ }
+ if (req.route.method === 'put') {
+ for (const [key, value] of req.body) {
+ conf.set(key, value);
}
- if (req.route.method === 'put') {
- for (const [key, value] of req.body) {
- conf.set(key, value);
- }
- return { code: 200, body: getSettings() };
- }
-
- // Error to client for unsupported request types.
- return false;
- };
+ return { code: 200, body: getSettings() };
+ }
- return handlers;
+ // Error to client for unsupported request types.
+ return false;
};
diff --git a/src/components/core/comms/api/cncserver.api.tools.js b/src/components/core/comms/api/cncserver.api.tools.js
index 70491e19..61769ba6 100644
--- a/src/components/core/comms/api/cncserver.api.tools.js
+++ b/src/components/core/comms/api/cncserver.api.tools.js
@@ -1,76 +1,188 @@
/**
* @file CNCServer ReSTful API endpoint module for pen state management.
*/
-const handlers = {};
+import * as tools from 'cs/tools';
+import { validateData } from 'cs/schemas';
+import { err } from 'cs/rest';
+import { singleLineString, merge } from 'cs/utils';
+import { forceState } from 'cs/pen';
+import { gConf } from 'cs/settings';
-module.exports = (cncserver) => {
- handlers['/v2/tools'] = function toolsGet(req) {
- if (req.route.method === 'get') { // Get list of tools
+export const handlers = {};
+
+// Unified item not found.
+function notFound(name) {
+ return {
+ code: 404,
+ body: {
+ status: 'error',
+ message: `Tool: '${name}' not found.`,
+ validOptions: tools.getNames(),
+ },
+ };
+}
+
+// Primary tools endpoint handler. List, create.
+handlers['/v2/tools'] = function toolsMain(req, res) {
+ // Get list of tools
+ if (req.route.method === 'get') {
+ return {
+ code: 200,
+ body: {
+ set: tools.getResponseSet(res.t),
+ tools: tools.items(),
+ presets: tools.listPresets(),
+ customs: tools.customKeys(), // List of custom preset machine names.
+ internals: tools.internalKeys(), // List of internal preset machine names.
+ invalidSets: tools.invalidPresets(),
+ },
+ };
+ }
+
+ // Add new custom tool.
+ if (req.route.method === 'post') {
+ // Validate data and add tool item, or error out.
+ validateData('tools', req.body, true)
+ .then(tools.add)
+ .then(tool => { res.status(200).send(tool); })
+ .catch(err(res));
+
+ return true; // Tell endpoint wrapper we'll handle the POST response.
+ }
+
+ // Change toolset options directly.
+ if (req.route.method === 'patch') {
+ // If item data is attempted to be changed here, give a specific message for it.
+ if (req.body.items) {
+ err(res, 406)(
+ new Error(singleLineString`Patching the tools endpoint can only edit the
+ current toolset details, not individual tool items.
+ Patch to /v2/tools/[ID] to edit a custom toolset item.`)
+ );
+ }
+
+ // Merge with current set, validate data, then edit.
+ const set = { ...tools.set };
+ delete set.items;
+ const mergedItem = merge(set, req.body);
+ validateData('toolsets', mergedItem, true)
+ .then(tools.editSet)
+ .then(editSet => { res.status(200).send(editSet); })
+ .catch(err(res));
+
+ return true; // Tell endpoint wrapper we'll handle the PATCH response.
+ }
+
+ // Error to client for unsupported request types.
+ return false;
+};
+
+// Tool specific enpoint.
+handlers['/v2/tools/:toolID'] = function toolsItemMain(req, res) {
+ const { toolID } = req.params;
+ const tool = tools.getItem(toolID);
+
+ // Sanity check tool.
+ if (!tool) return notFound(toolID);
+
+ // Set current end of buffer tool to ID.
+ if (req.route.method === 'put') {
+ tools.changeTo(tool.id, null, () => {
+ res.status(200).send(JSON.stringify({
+ status: `Tool changed to ${tool.id}`,
+ }));
+
+ if (gConf.get('debug')) {
+ console.log('>RESP', req.route.path, 200, `Tool:${tool.id}`);
+ }
+ }, req.body.waitForCompletion);
+ return true; // Tell endpoint wrapper we'll handle the response
+ }
+
+ // Edit tool by ID (Only allow editing of colorset tools).
+ if (req.route.method === 'patch') {
+ // No rewriting ID via patch.
+ if (req.body.id) {
return {
- code: 200,
+ code: 406,
body: {
- tools: Object.keys(cncserver.settings.botConf.get('tools')),
- toolData: cncserver.settings.botConf.get('tools'),
+ status: 'error',
+ message: 'You cannot rewrite a tool ID in a patch. Delete item and recreate.',
},
};
}
- // Error to client for unsupported request types.
- return false;
- };
+ // Only edit colorset tools.
+ if (!tools.canEdit(toolID)) {
+ return {
+ code: 406,
+ body: {
+ status: 'error',
+ message: singleLineString`This is a bot level tool, you can only edit colorset
+ level tools via the API.`,
+ allowedValues: tools.canEdit(),
+ },
+ };
+ }
- // Standard toolchanges.
- // TODO: Prevent manual swap "wait" toolchanges on this endpoint?
- handlers['/v2/tools/:tool'] = function toolsMain(req, res) {
- const toolName = req.params.tool;
- // TODO: Support other tool methods... (needs API design!)
- if (req.route.method === 'put') { // Set Tool
- if (cncserver.settings.botConf.get(`tools:${toolName}`)) {
- cncserver.control.setTool(toolName, null, () => {
- res.status(200).send(JSON.stringify({
- status: `Tool changed to ${toolName}`,
- }));
-
- if (cncserver.settings.gConf.get('debug')) {
- console.log('>RESP', req.route.path, 200, `Tool:${toolName}`);
- }
- }, req.body.waitForCompletion);
- return true; // Tell endpoint wrapper we'll handle the response
- }
+ // Merge the incoming data with the existing object as we don't need delta.
+ const mergedItem = merge(tool, req.body);
- return [404, `Tool: "${toolName}" not found`];
+ // Validate the request data against the schema before continuing.
+ validateData('tools', mergedItem, true)
+ .then(tools.edit)
+ .then(finalItem => { res.status(200).send(finalItem); })
+ .catch(err(res));
+
+ return true; // Tell endpoint wrapper we'll handle the PATCH response.
+ }
+
+ // Delete color
+ if (req.route.method === 'delete') {
+ // Only allow deleting colorset tools.
+ if (!tools.canEdit(toolID)) {
+ return {
+ code: 406,
+ body: {
+ status: 'error',
+ message: singleLineString`This is a bot level tool, you can only delete
+ colorset level tools via the API.`,
+ validOptions: tools.canEdit(),
+ },
+ };
}
- // Error to client for unsupported request types.
- return false;
- };
+ tools.delete(toolID);
+ return { code: 200, body: { tools: tools.items() } };
+ }
- // "wait" manual swap toolchanges with index
- handlers['/v2/tools/:tool/:index'] = function toolsMain(req, res) {
- const toolName = req.params.tool;
- const toolIndex = req.params.index;
-
- if (req.route.method === 'put') { // Set Tool
- if (cncserver.settings.botConf.get(`tools:${toolName}`)) {
- cncserver.control.setTool(toolName, toolIndex, () => {
- cncserver.pen.forceState({ tool: toolName });
- res.status(200).send(JSON.stringify({
- status: `Tool changed to ${toolName}, for index ${toolIndex}`,
- }));
-
- if (cncserver.settings.gConf.get('debug')) {
- console.log('>RESP', req.route.path, 200, `Tool:${toolName}, Index:${toolIndex}`);
- }
- }, req.body.waitForCompletion);
- return true; // Tell endpoint wrapper we'll handle the response
- }
+ // Error to client for unsupported request types.
+ return false;
+};
- return [404, `Tool: "${toolName}" not found`];
- }
+// "wait" manual swap toolchanges with index
+handlers['/v2/tools/:tool/:index'] = function toolsIndex(req, res) {
+ const toolIndex = req.params.index;
+ const tool = tools.getItem(req.params.tool);
- // Error to client for unsupported request types.
- return false;
- };
+ // Sanity check tool.
+ if (!tool) return notFound(req.params.tool);
+
+ if (req.route.method === 'put') { // Set Tool
+ tools.changeTo(tool.id, toolIndex, () => {
+ // TODO: Is this force state needed?
+ forceState({ tool: tool.id });
+ res.status(200).send(JSON.stringify({
+ status: `Tool changed to ${tool.id}, for index ${toolIndex}`,
+ }));
+
+ if (gConf.get('debug')) {
+ console.log('>RESP', req.route.path, 200, `Tool:${tool.id}, Index:${toolIndex}`);
+ }
+ }, req.body.waitForCompletion);
+ return true; // Tell endpoint wrapper we'll handle the response
+ }
- return handlers;
+ // Error to client for unsupported request types.
+ return false;
};
diff --git a/src/components/core/comms/api/index.js b/src/components/core/comms/api/index.js
index 58bf39b3..4992b50c 100644
--- a/src/components/core/comms/api/index.js
+++ b/src/components/core/comms/api/index.js
@@ -1,14 +1,28 @@
/**
* @file Index for all ReSTful API endpoint handlers.
*/
-/* eslint-disable global-require, import/no-dynamic-require */
-module.exports = (cncserver) => {
- const modules = [
- {}, 'settings', 'pen', 'motors', 'buffer', 'tools', 'colors', 'projects', 'content',
- ];
+import { handlers as settings } from 'cs/api/handlers/settings';
+import { handlers as pen } from 'cs/api/handlers/pen';
+import { handlers as motors } from 'cs/api/handlers/motors';
+import { handlers as buffer } from 'cs/api/handlers/buffer';
+import { handlers as tools } from 'cs/api/handlers/tools';
+import { handlers as colors } from 'cs/api/handlers/colors';
+import { handlers as imps } from 'cs/api/handlers/implements';
+import { handlers as projects } from 'cs/api/handlers/projects';
+import { handlers as content } from 'cs/api/handlers/content';
+import { handlers as print } from 'cs/api/handlers/print';
+import { handlers as serial } from 'cs/api/handlers/serial';
- return modules.reduce((acc, name) => ({
- ...acc,
- ...require(`./cncserver.api.${name}.js`)(cncserver),
- }));
+export const handlers = {
+ ...settings,
+ ...pen,
+ ...motors,
+ ...buffer,
+ ...tools,
+ ...colors,
+ ...imps,
+ ...projects,
+ ...content,
+ ...print,
+ ...serial,
};
diff --git a/src/components/core/comms/cncserver.api.js b/src/components/core/comms/cncserver.api.js
index 5eb6aebc..a1b76cac 100644
--- a/src/components/core/comms/cncserver.api.js
+++ b/src/components/core/comms/cncserver.api.js
@@ -1,261 +1,255 @@
/**
* @file Abstraction module for all Restful API related code for CNC Server!
*/
+import fs from 'fs';
+import request from 'request';
+import querystring from 'querystring';
+import pathToRegexp from 'path-to-regexp';
+import { createServerEndpoint } from 'cs/rest';
+import { gConf } from 'cs/settings';
+import { handlers } from 'cs/api/handlers';
+
+export const batchState = { batchRunning: false };
+
+// CNC Server API ============================================================
+// Enpoints are created and assigned via a server path to respond to, and
+// and callback function that manages handles the request and response.
+// We hold all of these in handlers to be able to call them
+// directly from the batch API endpoint. These are actually only turned into
+// endpoints at the end via createServerEndpoint().
+Object.entries(handlers).forEach(([path, callback]) => {
+ createServerEndpoint(path, callback);
+});
-const fs = require('fs');
-const request = require('request');
-const _ = require('underscore');
-const querystring = require('querystring');
-const pathToRegexp = require('path-to-regexp');
-const handlerResolve = require('./api');
-
-const api = {};
-
-module.exports = (cncserver) => {
- // CNC Server API ============================================================
- // Enpoints are created and assigned via a server path to respond to, and
- // and callback function that manages handles the request and response.
- // We hold all of these in api.handlers to be able to call them
- // directly from the batch API endpoint. These are actually only turned into
- // endpoints at the end via createServerEndpoint().
- api.handlers = handlerResolve(cncserver);
- _.each(api.handlers, (callback, path) => {
- cncserver.rest.createServerEndpoint(path, callback);
- });
-
- /**
- * Return a dummy 'request' or 'response' object for faking express requests.
- *
- * @param {string} type
- * Either 'request' or 'response'.
- *
- * @return {object}
- * The dummy object with minimum required parts for the handlers to use the
- * same code as the express handler arguments.
- */
- function getDummyObject(type) {
- let out = {};
-
- switch (type) {
- case 'request':
- out = {
- route: {
- method: '',
- path: '',
- },
- query: {},
- params: {},
- body: {},
- };
- break;
-
- case 'response':
- out = { status: () => ({ send: () => { } }) };
- break;
- default:
- }
-
- return out;
+/**
+ * Return a dummy 'request' or 'response' object for faking express requests.
+ *
+ * @param {string} type
+ * Either 'request' or 'response'.
+ *
+ * @return {object}
+ * The dummy object with minimum required parts for the handlers to use the
+ * same code as the express handler arguments.
+ */
+function getDummyObject(type) {
+ let out = {};
+
+ switch (type) {
+ case 'request':
+ out = {
+ route: {
+ method: '',
+ path: '',
+ },
+ query: {},
+ params: {},
+ body: {},
+ };
+ break;
+
+ case 'response':
+ out = { status: () => ({ send: () => { } }) };
+ break;
+ default:
}
- /**
- * Process a flat array of semi-abstracted commands into the queue.
- *
- * @param {array} commands
- * Flat array of command objects in the following format:
- * {"[POST|PUT|DELETE] /v1/[ENDPOINT]": {data: 'for the endpoint'}}
- * @param {function} callback
- * Callback function when command processing is complete.
- * @param {number} index
- * Array index of commands to process. Ignore/Pass as undefined to init.
- * Function calls itself via callbacks to ensure delayed api handlers remain
- * queued in order while async.
- * @param {number} goodCount
- * Running tally of successful commands, to be returned to callback once
- * complete.
- */
- function processBatchData(commands, callback, index, goodCount) {
- // Initiate for the first loop run.
- if (typeof index === 'undefined') {
- // eslint-disable-next-line no-param-reassign
- index = 0;
- // eslint-disable-next-line no-param-reassign
- goodCount = 0;
-
- api.batchRunning = true;
- }
-
- const command = commands[index];
- if (typeof command !== 'undefined' && api.batchRunning) {
- const key = Object.keys(command)[0];
- const data = command[key];
- const method = key.split(' ')[0];
- let path = key.split(' ')[1].split('?')[0];
- if (path[0] !== '/') path = `/${path}`;
-
- const query = path.split('?')[1]; // Query params.
- const params = {}; // URL Params.
-
- // Batch runs are send and forget, force waitForCompletion false.
- data.waitForCompletion = false;
-
- const req = getDummyObject('request');
- const res = getDummyObject('response');
- let handlerKey = '';
+ return out;
+}
- // Attempt to match the path to a requstHandler by express path match.
- _.each(Object.keys(api.handlers), (pattern) => {
- const keys = [];
- const match = pathToRegexp(pattern, keys).exec(path);
- if (match) {
- handlerKey = pattern;
+/**
+ * Process a flat array of semi-abstracted commands into the queue.
+ *
+ * @param {array} commands
+ * Flat array of command objects in the following format:
+ * {"[POST|PUT|DELETE] /v1/[ENDPOINT]": {data: 'for the endpoint'}}
+ * @param {function} callback
+ * Callback function when command processing is complete.
+ * @param {number} index
+ * Array index of commands to process. Ignore/Pass as undefined to init.
+ * Function calls itself via callbacks to ensure delayed api handlers remain
+ * queued in order while async.
+ * @param {number} goodCount
+ * Running tally of successful commands, to be returned to callback once
+ * complete.
+ */
+function processBatchData(commands, callback, index, goodCount) {
+ // Initiate for the first loop run.
+ if (typeof index === 'undefined') {
+ // eslint-disable-next-line no-param-reassign
+ index = 0;
+ // eslint-disable-next-line no-param-reassign
+ goodCount = 0;
+
+ batchState.batchRunning = true;
+ }
- // If there's keyed url params, inject them.
- if (keys.length) {
- _.each(keys, (p, id) => {
- params[p.name] = match[id + 1];
- });
- }
+ const command = commands[index];
+ if (typeof command !== 'undefined' && batchState.batchRunning) {
+ const key = Object.keys(command)[0];
+ const data = command[key];
+ const method = key.split(' ')[0];
+ let path = key.split(' ')[1].split('?')[0];
+ if (path[0] !== '/') path = `/${path}`;
+
+ const query = path.split('?')[1]; // Query params.
+ const params = {}; // URL Params.
+
+ // Batch runs are send and forget, force waitForCompletion false.
+ data.waitForCompletion = false;
+
+ const req = getDummyObject('request');
+ const res = getDummyObject('response');
+ let handlerKey = '';
+
+ // Attempt to match the path to a requstHandler by express path match.
+ Object.keys(handlers).forEach(pattern => {
+ const keys = [];
+ const match = pathToRegexp(pattern, keys).exec(path);
+ if (match) {
+ handlerKey = pattern;
+
+ // If there's keyed url params, inject them.
+ if (keys.length) {
+ keys.forEach((p, id) => {
+ params[p.name] = match[id + 1];
+ });
}
- });
-
- // Fill out request details:
- req.route.method = method.toLowerCase();
- req.route.path = path;
- req.query = query ? querystring.parse(query) : {};
- req.params = params;
- req.body = data;
-
- // Call the api handler (send and forget via batch!)
- if (api.handlers[handlerKey]) {
- res.status = code => ({
- send: (sendData) => {
- if (cncserver.settings.gConf.get('debug')) {
- console.log(`#${index}, Batch Delay:`, handlerKey, code, sendData);
- }
-
- if (code.toString()[0] === '2') goodCount++;
- process.nextTick(() => {
- processBatchData(commands, callback, index + 1, goodCount);
- });
- },
- });
-
- // Naively check to see if the request was successful.
- // Technically if there's a wait for return (=== true), we could only
- // see it in the .status() return callback.
- const response = api.handlers[handlerKey](req, res);
- if (response !== true) {
- if (cncserver.settings.gConf.get('debug')) {
- console.log(`#${index}, Batch Immediate:`, handlerKey, response);
+ }
+ });
+
+ // Fill out request details:
+ req.route.method = method.toLowerCase();
+ req.route.path = path;
+ req.query = query ? querystring.parse(query) : {};
+ req.params = params;
+ req.body = data;
+
+ // Call the api handler (send and forget via batch!)
+ if (handlers[handlerKey]) {
+ res.status = code => ({
+ send: sendData => {
+ if (gConf.get('debug')) {
+ console.log(`#${index}, Batch Delay:`, handlerKey, code, sendData);
}
- if (response !== false) goodCount++;
+ if (code.toString()[0] === '2') goodCount++;
process.nextTick(() => {
processBatchData(commands, callback, index + 1, goodCount);
});
+ },
+ });
+
+ // Naively check to see if the request was successful.
+ // Technically if there's a wait for return (=== true), we could only
+ // see it in the .status() return callback.
+ const response = handlers[handlerKey](req, res);
+ if (response !== true) {
+ if (gConf.get('debug')) {
+ console.log(`#${index}, Batch Immediate:`, handlerKey, response);
}
- } else {
- // Unhandled path, not a valid API handler available. Move on.
- processBatchData(commands, callback, index + 1, goodCount);
+
+ if (response !== false) goodCount++;
+ process.nextTick(() => {
+ processBatchData(commands, callback, index + 1, goodCount);
+ });
}
} else {
- // We're out of commands, or batch was cancelled.
- api.batchRunning = false;
- if (callback) callback(goodCount);
+ // Unhandled path, not a valid API handler available. Move on.
+ processBatchData(commands, callback, index + 1, goodCount);
}
+ } else {
+ // We're out of commands, or batch was cancelled.
+ batchState.batchRunning = false;
+ if (callback) callback(goodCount);
}
+}
+
+// Batch Command API =========================================================
+export function setBatchRunningState(runningState = false) {
+ batchState.batchRunning = !!runningState;
+}
+
+createServerEndpoint('/v1/batch', (req, res) => {
+ // Create a new batch set.
+ if (req.route.method === 'post') {
+ // For exceedingly large batches over 50k commands, batching in takes
+ // longer than the socket will stay open, so we simply respond with a "201
+ // queued" immediately after counting.
+ if (req.body.file) {
+ const { file } = req.body;
+
+ if (file.substr(0, 4) === 'http') {
+ // Internet file.
+ request.get(file, (error, response, body) => {
+ // Attempt to parse/process data.
+ if (body) {
+ try {
+ const commands = JSON.parse(body);
+ res.status(201).send(JSON.stringify({
+ status: `Parsed ${commands.length} commands, queuing`,
+ count: commands.length,
+ }));
- // Batch Command API =========================================================
- api.batchRunning = false;
- api.setBatchRunningState = (runningState = false) => {
- api.batchRunning = !!runningState;
- };
-
- cncserver.rest.createServerEndpoint('/v1/batch', (req, res) => {
- // Create a new batch set.
- if (req.route.method === 'post') {
- // For exceedingly large batches over 50k commands, batching in takes
- // longer than the socket will stay open, so we simply respond with a "201
- // queued" immediately after counting.
- if (req.body.file) {
- const { file } = req.body;
-
- if (file.substr(0, 4) === 'http') {
- // Internet file.
- request.get(file, (error, response, body) => {
- // Attempt to parse/process data.
- if (body) {
- try {
- const commands = JSON.parse(body);
- res.status(201).send(JSON.stringify({
- status: `Parsed ${commands.length} commands, queuing`,
- count: commands.length,
- }));
-
- processBatchData(commands);
- } catch (err) {
- error = err;
- }
+ processBatchData(commands);
+ } catch (err) {
+ error = err;
}
+ }
- // Catch response for errors (on parsing or reading).
- if (error) {
- res.status(400).send(JSON.stringify({
- status: `Error reading file "${file}"`,
- remoteHTTPCode: response.statusCode,
- data: error,
+ // Catch response for errors (on parsing or reading).
+ if (error) {
+ res.status(400).send(JSON.stringify({
+ status: `Error reading file "${file}"`,
+ remoteHTTPCode: response.statusCode,
+ data: error,
+ }));
+ }
+ });
+ } else {
+ // Local file.
+ fs.readFile(file, (error, data) => {
+ // Attempt to read the data.
+ if (data) {
+ try {
+ const commands = JSON.parse(data.toString());
+ res.status(201).send(JSON.stringify({
+ status: `Parsed ${commands.length} commands, queuing`,
+ count: commands.length,
}));
+ processBatchData(commands);
+ } catch (err) {
+ error = err;
}
- });
- } else {
- // Local file.
- fs.readFile(file, (error, data) => {
- // Attempt to read the data.
- if (data) {
- try {
- const commands = JSON.parse(data.toString());
- res.status(201).send(JSON.stringify({
- status: `Parsed ${commands.length} commands, queuing`,
- count: commands.length,
- }));
- processBatchData(commands);
- } catch (err) {
- error = err;
- }
- }
+ }
- // Catch response for errors (on parsing or reading).
- if (error) {
- res.status(400).send(JSON.stringify({
- status: `Error reading file "${file}"`,
- data: error,
- }));
- }
- });
- }
- } else {
- // Raw command data (not from a file);
- try {
- res.status(201).send(JSON.stringify({
- status: `Parsed ${req.body.length} commands, queuing`,
- count: req.body.length,
- }));
- processBatchData(req.body);
- } catch (err) {
- res.status(400).send(JSON.stringify({
- status: 'Error reading/processing batch data',
- data: err,
- }));
- }
+ // Catch response for errors (on parsing or reading).
+ if (error) {
+ res.status(400).send(JSON.stringify({
+ status: `Error reading file "${file}"`,
+ data: error,
+ }));
+ }
+ });
+ }
+ } else {
+ // Raw command data (not from a file);
+ try {
+ res.status(201).send(JSON.stringify({
+ status: `Parsed ${req.body.length} commands, queuing`,
+ count: req.body.length,
+ }));
+ processBatchData(req.body);
+ } catch (err) {
+ res.status(400).send(JSON.stringify({
+ status: 'Error reading/processing batch data',
+ data: err,
+ }));
}
-
- return true; // Tell endpoint wrapper we'll handle the response
}
- // Error to client for unsupported request types.
- return false;
- });
+ return true; // Tell endpoint wrapper we'll handle the response
+ }
- return api;
-};
+ // Error to client for unsupported request types.
+ return false;
+});
diff --git a/src/components/core/comms/cncserver.ipc.js b/src/components/core/comms/cncserver.ipc.js
index 90b85eff..9d94cae5 100644
--- a/src/components/core/comms/cncserver.ipc.js
+++ b/src/components/core/comms/cncserver.ipc.js
@@ -3,231 +3,256 @@
* for talking to the "runner" process, for CNC Server!
*
*/
-const { spawn } = require('child_process'); // Process spawner.
-const nodeIPC = require('node-ipc'); // Inter Process Comms for runner.
+import { spawn } from 'child_process'; // Process spawner.
+import nodeIPC from 'node-ipc'; // Inter Process Comms for runner.
+import path from 'path';
+import { trigger } from 'cs/binder';
+import {
+ cmdstr, startItem, removeItem, setRunning
+} from 'cs/buffer';
+import { gConf, botConf } from 'cs/settings';
+import { callbacks as serialCallbacks } from 'cs/serial';
+import { forceState } from 'cs/pen';
+import { __basedir } from 'cs/utils';
+
+// IPC server config.
+nodeIPC.config.silent = true;
+nodeIPC.config.id = 'cncserver';
+nodeIPC.config.retry = 1500;
+
+// TODO: Evaluate usage of state to ensure only what we need exported is exported.
+export const state = {
+ runnerSocket: {}, // The IPC socket for communicating to the runner
+ runnerInitCallback: null, // Placeholder for init set callback.
+ getSerialValueCallback: null,
+};
-// Export object.
-const ipc = {};
+/**
+ * Send a message to the runner.
+ *
+ * @param {string} command
+ * The string identifier for the command in dot notation.
+ * @param {object/string} data
+ * Data to be sent message receiver on client.
+ * @param {object} socket
+ * The IPC socket to send to, defaults to initial connect socket.
+ *
+ * @return {null}
+ */
+export function sendMessage(command, data, socket = state.runnerSocket) {
+ const packet = {
+ command,
+ data,
+ };
-module.exports = (cncserver) => {
- let runnerInitCallback = null; // Placeholder for init set callback.
- ipc.runnerSocket = {}; // The IPC socket for communicating to the runner
+ nodeIPC.server.emit(socket, 'app.message', packet);
+}
- // IPC server config.
- nodeIPC.config.silent = true;
- nodeIPC.config.id = 'cncserver';
- nodeIPC.config.retry = 1500;
+// Define the runner tracker object.
+export const runner = {
+ process: {},
/**
- * Send a message to the runner.
- *
- * @param {string} command
- * The string identifier for the command in dot notation.
- * @param {object/string} data
- * Data to be sent message receiver on client.
- * @param {object} socket
- * The IPC socket to send to, defaults to initial connect socket.
- *
- * @return {null}
- */
- ipc.sendMessage = (command, data, socket = cncserver.ipc.runnerSocket) => {
- const packet = {
- command,
- data,
- };
-
- nodeIPC.server.emit(socket, 'app.message', packet);
- };
+ * Start up & init the Runner process via node
+ */
+ init: () => {
+ runner.process = spawn(
+ 'node',
+ [path.join(__basedir, 'components', 'core', 'runner', 'cncserver.runner.js')]
+ );
+
+ runner.process.stdout.on('data', rawData => {
+ const data = rawData.toString().split('\n');
+ for (const i in data) {
+ if (data[i].length) console.log(`RUNNER:${data[i]}`);
+ }
+ });
- /**
- * Initialize and start the IPC server
- *
- * @param {object} options
- * localRunner {boolean}: true if we should try to init the runner locally,
- * false to defer starting the runner to the parent application.
- * @param {Function} callback
- * Function called when the runner is connected and ready.
- *
- * @return {null}
- */
- ipc.initServer = (options, callback) => {
- runnerInitCallback = callback;
-
- // Initialize and start the IPC Server...
- nodeIPC.serve(() => {
- cncserver.binder.trigger('ipc.serve');
- nodeIPC.server.on('app.message', ipc.ipcGotMessage);
+ runner.process.stderr.on('data', data => {
+ console.log(`RUNNER ERROR: ${data}`);
});
- nodeIPC.server.start();
- console.log('Starting IPC server, waiting for runner client to start...');
+ runner.process.on('exit', exitCode => {
+ // TODO: Restart it? Who knows.
+ console.log(`RUNNER EXITED: ${exitCode}`);
+ });
+ },
- if (options.localRunner) {
- // Register an event callback to shutdown the runner if we're exiting.
- process.on('SIGTERM', ipc.runner.shutdown);
- process.on('SIGINT', ipc.runner.shutdown);
- ipc.runner.init();
- }
- };
+ shutdown: () => {
+ console.log('Killing runner process before exiting...');
+ runner.process.kill();
+ process.exit();
+ },
+};
+/**
+ * Helper for getting an async value from the serial port, always direct.
+ *
+ * @param {string} command
+ * A named machine configuration command.
+ * @param {object} options
+ * Keyed value replacement options for the command.
+ *
+ * @return {Promise}
+ * Promise that will always succeed with next message from serial.
+ */
+export const getSerialValue = (command, options = {}) => new Promise(resolve => {
+ process.nextTick(() => {
+ sendMessage('serial.direct.command', {
+ commands: [cmdstr(command, options)],
+ duration: 0,
+ });
- /**
- * Helper for getting an async value from the serial port, always direct.
- *
- * @param {string} command
- * A named machine configuration command.
- * @param {object} options
- * Keyed value replacement options for the command.
- *
- * @return {Promise}
- * Promise that will always succeed with next message from serial.
- */
- ipc.getSerialValueCallback = null;
- ipc.getSerialValue = (command, options = {}) => new Promise((resolver) => {
- process.nextTick(() => {
- ipc.sendMessage('serial.direct.command', {
- commands: [cncserver.buffer.cmdstr(command, options)],
- duration: 0,
- });
+ state.getSerialValueCallback = resolve;
+ });
+});
- ipc.getSerialValueCallback = resolver;
+/**
+ * Raw serial helper for getting an async value from the serial port, always direct.
+ *
+ * @param {string} command
+ * Raw serial string to send.
+ *
+ * @return {Promise}
+ * Promise that will always succeed with next message from serial.
+ */
+export const getSerialValueRaw = command => new Promise(resolve => {
+ process.nextTick(() => {
+ console.log('Sending raw', command);
+ sendMessage('serial.direct.command', {
+ commands: [command],
+ duration: 0,
});
+
+ state.getSerialValueCallback = resolve;
});
+});
- /**
- * IPC Message callback event parser/handler.
- *
- * @param {object} packet
- * The entire message object directly from the event.
- * @param {object} socket
- * The originating IPC client socket object.
- *
- * @return {null}
- */
- ipc.ipcGotMessage = (packet, socket) => {
- const { callbacks: serialCallbacks } = cncserver.serial;
- const { data } = packet;
- const messages = typeof data === 'string' ? data.trim().split('\n') : [];
- const { baudRate } = cncserver.settings.botConf.get('controller');
-
- switch (packet.command) {
- case 'runner.ready':
- ipc.runnerSocket = socket;
- ipc.sendMessage('runner.config', {
- debug: cncserver.settings.gConf.get('debug'),
- ack: cncserver.settings.botConf.get('controller').ack,
- showSerial: cncserver.settings.gConf.get('showSerial'),
- });
-
- if (runnerInitCallback) runnerInitCallback();
- break;
-
- // Sync simulation state from runner.
- case 'serial.simulation':
- cncserver.pen.forceState({ simulation: packet ? 1 : 0 });
- break;
-
- case 'serial.connected':
- console.log(
- `Serial connection open at ${baudRate}bps`
- );
+/**
+ * IPC Message callback event parser/handler.
+ *
+ * @param {object} packet
+ * The entire message object directly from the event.
+ * @param {object} socket
+ * The originating IPC client socket object.
+ *
+ * @return {null}
+ */
+export function ipcGotMessage(packet, socket) {
+ const { data } = packet;
+ const messages = typeof data === 'string' ? data.trim().split('\n') : [];
+ const { baudRate } = botConf.get('controller');
+
+ switch (packet.command) {
+ case 'runner.ready':
+ state.runnerSocket = socket;
+ sendMessage('runner.config', {
+ debug: gConf.get('debug'),
+ ack: botConf.get('controller').ack,
+ showSerial: gConf.get('showSerial'),
+ });
- cncserver.binder.trigger('serial.connected');
+ if (state.runnerInitCallback) state.runnerInitCallback();
+ break;
- if (serialCallbacks.connect) serialCallbacks.connect(data);
- if (serialCallbacks.success) serialCallbacks.success(data);
- break;
+ // Sync simulation state from runner.
+ case 'serial.simulation':
+ forceState({ simulation: packet ? 1 : 0 });
+ break;
- case 'serial.disconnected':
- if (serialCallbacks.disconnect) serialCallbacks.disconnect(data);
- break;
+ case 'serial.connected':
+ console.log(
+ `Serial connection open at ${baudRate}bps`
+ );
- case 'serial.error':
- if (packet.type === 'connect') {
- console.log(
- 'Serial port failed to connect. Is it busy or in use? Error #10'
- );
- console.log('SerialPort says:', packet.message);
- if (serialCallbacks.complete) serialCallbacks.complete(data);
- } else {
- // TODO: Add better error message here, or figure out when this
- // happens.
- console.log('Serial failed to send data. Error #44');
- }
+ trigger('serial.connected');
- if (serialCallbacks.error) serialCallbacks.error(data);
- break;
-
- case 'serial.data':
- // Either get the value for a caller, or trigger generic bind.
- messages.forEach((message) => {
- if (ipc.getSerialValueCallback && message !== cncserver.settings.botConf.get('controller').ack) {
- ipc.getSerialValueCallback(message);
- ipc.getSerialValueCallback = null;
- } else {
- cncserver.binder.trigger('serial.message', message);
- }
- });
- break;
-
- case 'buffer.item.start':
- // Buffer action item begun to run.
- cncserver.buffer.startItem(data);
- break;
-
- case 'buffer.item.done':
- // Buffer item has completed.
- cncserver.buffer.removeItem(data);
- break;
-
- case 'buffer.empty':
- // TODO: Is this needed?
- break;
-
- case 'buffer.running':
- cncserver.buffer.setRunning(data);
- break;
- default:
- }
- };
+ if (serialCallbacks.connect) serialCallbacks.connect(data);
+ if (serialCallbacks.success) serialCallbacks.success(data);
+ break;
- // Define the runner tracker object.
- ipc.runner = {
- process: {},
-
- /**
- * Start up & init the Runner process via node
- */
- init: () => {
- // TODO: Use FS path to join instead of fixed slashes.
- ipc.runner.process = spawn(
- 'node',
- [`${__dirname}/../runner/cncserver.runner`]
- );
+ case 'serial.disconnected':
+ if (serialCallbacks.disconnect) serialCallbacks.disconnect(data);
+ break;
- ipc.runner.process.stdout.on('data', (rawData) => {
- const data = rawData.toString().split('\n');
- for (const i in data) {
- if (data[i].length) console.log(`RUNNER:${data[i]}`);
+ case 'serial.error':
+ if (packet.type === 'connect') {
+ console.log(
+ 'Serial port failed to connect. Is it busy or in use? Error #10'
+ );
+ console.log('SerialPort says:', packet.message);
+ if (serialCallbacks.complete) serialCallbacks.complete(data);
+ } else {
+ // TODO: Add better error message here, or figure out when this
+ // happens.
+ console.log('Serial failed to send data. Error #44');
+ }
+
+ if (serialCallbacks.error) serialCallbacks.error(data);
+ break;
+
+ case 'serial.data':
+ // Either get the value for a caller, or trigger generic bind.
+ messages.forEach(message => {
+ // TODO: Does this work every time?
+ // && message !== botConf.get('controller').ack) {
+ if (state.getSerialValueCallback) {
+ state.getSerialValueCallback(message);
+ state.getSerialValueCallback = null;
+ } else {
+ trigger('serial.message', message);
}
});
+ break;
- ipc.runner.process.stderr.on('data', (data) => {
- console.log(`RUNNER ERROR: ${data}`);
- });
+ case 'buffer.item.start':
+ // Buffer action item begun to run.
+ startItem(data);
+ break;
- ipc.runner.process.on('exit', (exitCode) => {
- // TODO: Restart it? Who knows.
- console.log(`RUNNER EXITED: ${exitCode}`);
- });
- },
+ case 'buffer.item.done':
+ // Buffer item has completed.
+ removeItem(data);
+ break;
- shutdown: () => {
- console.log('Killing runner process before exiting...');
- ipc.runner.process.kill();
- process.exit();
- },
- };
+ case 'buffer.empty':
+ // TODO: Is this needed?
+ break;
- return ipc;
-};
+ case 'buffer.running':
+ setRunning(data);
+ break;
+ default:
+ }
+}
+
+/**
+ * Initialize and start the IPC server
+ *
+ * @param {object} options
+ * localRunner {boolean}: true if we should try to init the runner locally,
+ * false to defer starting the runner to the parent application.
+ * @param {Function} callback
+ * Function called when the runner is connected and ready.
+ *
+ * @return {null}
+ */
+export function initServer(options, callback) {
+ state.runnerInitCallback = callback;
+
+ // Initialize and start the IPC Server...
+ nodeIPC.serve(() => {
+ trigger('ipc.serve');
+ nodeIPC.server.on('app.message', ipcGotMessage);
+ });
+
+ nodeIPC.server.start();
+ console.log('Starting IPC server, waiting for runner client to start...');
+
+ if (options.localRunner) {
+ // Register an event callback to shutdown the runner if we're exiting.
+ process.on('SIGTERM', runner.shutdown);
+ process.on('SIGINT', runner.shutdown);
+ runner.init();
+ }
+}
diff --git a/src/components/core/comms/cncserver.rest.js b/src/components/core/comms/cncserver.rest.js
index 54db8597..995dabda 100644
--- a/src/components/core/comms/cncserver.rest.js
+++ b/src/components/core/comms/cncserver.rest.js
@@ -1,111 +1,128 @@
/**
* @file Abstraction module for restful helper utilities createServerEndpoint.
*/
-const express = require('express'); // Express object (for static).
+import express from 'express'; // Express object (for static).
+import { app } from 'cs/server';
+import { gConf } from 'cs/settings';
+import { getFromRequest } from 'cs/schemas';
-const rest = {};
+/**
+ * Wrapper for creating a static (directory reading HTML) endpoint.
+ *
+ * @param {string} userPath [description]
+ * Path a user would enter to get to the content.
+ * @param {string} sourcePath
+ * Path for source files to be served from.
+ * @param {object} options
+ * options object for static serving of files.
+ */
+export function createStaticEndpoint(userPath, sourcePath, options) {
+ app.use(userPath, express.static(sourcePath, options));
+}
-module.exports = (cncserver) => {
- /**
- * Wrapper for creating a static (directory reading HTML) endpoint.
- *
- * @param {string} userPath [description]
- * Path a user would enter to get to the content.
- * @param {string} sourcePath
- * Path for source files to be served from.
- * @param {object} options
- * options object for static serving of files.
- */
- rest.createStaticEndpoint = (userPath, sourcePath, options) => {
- cncserver.server.app.use(userPath, express.static(sourcePath, options));
- };
+/**
+ * Wrapper for unifying the creation and logic of standard endpoints, their
+ * headers and their responses and formats.
+ *
+ * @param {string} path
+ * Full path of HTTP callback in express path format (can include wildcards)
+ * @param {function} callback
+ * Callback triggered on HTTP request
+ */
+export function createServerEndpoint(path, callback) {
+ const what = Object.prototype.toString;
+ app.all(path, (req, res) => {
+ res.set('Content-Type', 'application/json; charset=UTF-8');
+ res.set('Access-Control-Allow-Origin', gConf.get('corsDomain'));
- /**
- * Wrapper for unifying the creation and logic of standard endpoints, their
- * headers and their responses and formats.
- *
- * @param {string} path
- * Full path of HTTP callback in express path format (can include wildcards)
- * @param {function} callback
- * Callback triggered on HTTP request
- */
- rest.createServerEndpoint = (path, callback) => {
- const what = Object.prototype.toString;
- cncserver.server.app.all(path, (req, res) => {
- res.set('Content-Type', 'application/json; charset=UTF-8');
- res.set('Access-Control-Allow-Origin', cncserver.settings.gConf.get('corsDomain'));
+ if (gConf.get('debug') && path !== '/poll') {
+ console.log(
+ req.route.method.toUpperCase(),
+ req.route.path,
+ JSON.stringify(req.body)
+ );
+ }
- if (cncserver.settings.gConf.get('debug') && path !== '/poll') {
- console.log(
- req.route.method.toUpperCase(),
- req.route.path,
- JSON.stringify(req.body)
- );
- }
+ // Handle CORS Pre-flight OPTIONS request ourselves
+ // TODO: Allow implementers to define options and allowed methods.
+ if (req.route.method === 'options') {
+ res.set(
+ 'Access-Control-Allow-Methods',
+ 'PUT, PATCH, POST, GET, DELETE'
+ );
+ res.set(
+ 'Access-Control-Allow-Headers',
+ 'Origin, X-Requested-Width, Content-Type, Accept'
+ );
- // Handle CORS Pre-flight OPTIONS request ourselves
- // TODO: Allow implementers to define options and allowed methods.
- if (req.route.method === 'options') {
- res.set(
- 'Access-Control-Allow-Methods',
- 'PUT, PATCH, POST, GET, DELETE'
- );
- res.set(
- 'Access-Control-Allow-Headers',
- 'Origin, X-Requested-Width, Content-Type, Accept'
- );
+ // Attach the schema and send, if any.
+ res.status(200).send(getFromRequest(path));
+ return;
+ }
- // Attach the schema and send, if any.
- res.status(200).send(cncserver.schemas.getFromRequest(path));
- return;
- }
+ const cbStat = callback(req, res);
- const cbStat = callback(req, res);
-
- if (cbStat === false) { // Super simple "not supported"
- // Debug Response
- if (cncserver.settings.gConf.get('debug') && path !== '/poll') {
- console.log('>RESP', req.route.path, 405, 'Not Supported');
- }
+ if (cbStat === false) { // Super simple "not supported"
+ // Debug Response
+ if (gConf.get('debug') && path !== '/poll') {
+ console.log('>RESP', req.route.path, 405, 'Not Supported');
+ }
- res.status(405).send(JSON.stringify({
- status: 'Not supported',
- }));
- } else if (what.call(cbStat) === '[object Array]') { // Just return message
- // Debug Response
- if (cncserver.settings.gConf.get('debug') && path !== '/poll') {
- console.log('>RESP', req.route.path, cbStat[0], cbStat[1]);
- }
+ res.status(405).send(JSON.stringify({
+ status: 'Not supported',
+ }));
+ } else if (what.call(cbStat) === '[object Array]') { // Just return message
+ // Debug Response
+ if (gConf.get('debug') && path !== '/poll') {
+ console.log('>RESP', req.route.path, cbStat[0], cbStat[1]);
+ }
- // Array format: [/http code/, /status message/]
- res.status(cbStat[0]).send(JSON.stringify({ status: cbStat[1] }));
- } else if (what.call(cbStat) === '[object Object]') { // Full message
- // Debug Response
- if (cncserver.settings.gConf.get('debug') && path !== '/poll') {
- console.log(
- '>RESP',
- req.route.path,
- cbStat.code,
- JSON.stringify(req.body)
- );
- }
+ // Array format: [/http code/, /status message/]
+ res.status(cbStat[0]).send(JSON.stringify({ status: cbStat[1] }));
+ } else if (what.call(cbStat) === '[object Object]') { // Full message
+ // Debug Response
+ if (gConf.get('debug') && path !== '/poll') {
+ console.log(
+ '>RESP',
+ req.route.path,
+ cbStat.code,
+ JSON.stringify(req.body)
+ );
+ }
- // Send plaintext if body is string, otherwise convert to JSON.
- if (typeof cbStat.body === 'string') {
- res.set('Content-Type', 'text/plain; charset=UTF-8');
- res.status(cbStat.code).send(cbStat.body);
- } else {
- res.status(cbStat.code).send(JSON.stringify(cbStat.body));
- }
+ // Send plaintext if body is string, otherwise convert to JSON.
+ if (typeof cbStat.body === 'string') {
+ res.set('Content-Type', 'text/plain; charset=UTF-8');
+ res.status(cbStat.code).send(cbStat.body);
+ } else {
+ res.status(cbStat.code).send(JSON.stringify(cbStat.body));
}
- });
- };
+ }
+ });
+}
- // Exports.
- rest.exports = {
- createStaticEndpoint: rest.createStaticEndpoint,
- createServerEndpoint: rest.createServerEndpoint,
+/**
+ * Standardized response error handler (to be passed to promise catches).
+ *
+ * The intent is to handle all error objects and give something useful to the
+ * client in the message and associated objects. This is curried to hand the catch a
+ * new function once we get the local response object.
+ *
+ * @param {HTTP Response} res
+ * Specific request response object.
+ * @param {number} code
+ * Response code to use.
+ *
+ * @returns {function}
+ * Catch callback that takes a single error object as arg.
+ */
+export const err = (res, code = 406) => error => {
+ const errBody = {
+ status: 'error',
+ message: error.message || error,
};
- return rest;
+ if (error.allowedValues) errBody.allowedValues = error.allowedValues;
+ if (!error.allowedValues && error.stack) errBody.stack = error.stack.split('\n');
+ res.status(code).send(errBody);
};
diff --git a/src/components/core/comms/cncserver.serial.js b/src/components/core/comms/cncserver.serial.js
index 3b9049fa..bd33c5c6 100644
--- a/src/components/core/comms/cncserver.serial.js
+++ b/src/components/core/comms/cncserver.serial.js
@@ -4,228 +4,224 @@
* Taking in only the global CNCServer object, add's the "serial" object.
*
*/
-const SerialPort = require('serialport');
-
-const serial = {}; // Export interface object;
-
-module.exports = (cncserver) => {
- serial.callbacks = {}; // Hold global serial connection/error callbacks.
- serial.connectPath = '{auto}';
-
- // Board support provided setup commands to be run on serial connect.
- serial.setupCommands = [];
- serial.setSetupCommands = (commands) => {
- serial.setupCommands = commands;
- };
-
- /**
- * Helper function to manage initial serial connection and reconnection.
- *
- * @param {object} options
- * Holds all possible callbacks for the serial connection:
- * connect: Callback for successful serial connect event
- * success: Callback for general success
- * error: Callback for init/connect error, arg of error string/object
- * disconnect: Callback for close/unexpected disconnect
- * complete: Callback for general completion
- */
- serial.connect = (options) => {
- // Apply any passed callbacks to a new serial callbacks object.
- serial.callbacks = {
- connect: options.connect,
- disconnect: options.disconnect,
- error: options.error,
- success: options.success,
- };
-
- // Run everything through the callback as port list is async.
- console.log('Finding available serial ports...');
- const botController = cncserver.settings.botConf.get('controller');
- cncserver.serial.autoDetectPort(botController, (ports) => {
- // Give some console feedback on ports.
- if (cncserver.settings.gConf.get('debug')) {
- console.log('Full Available Port Data:', ports.full);
- } else {
- const names = ports.names.length ? ports.names.join(', ') : '[NONE]';
- console.log(`Available Serial ports: ${names}`);
- }
+import SerialPort from 'serialport';
+import * as ipc from 'cs/ipc';
+import * as server from 'cs/server';
+import { botConf, gConf } from 'cs/settings';
+import { state as penState, forceState } from 'cs/pen';
+import { initAPI as initScratch } from 'cs/scratch';
+
+export const callbacks = {}; // Hold global serial connection/error callbacks.
+
+// Board support provided setup commands to be run on serial connect.
+export const state = {
+ setupCommands: [],
+ connectPath: '{auto}',
+};
- const passedPort = cncserver.settings.gConf.get('serialPath');
- if (passedPort === '' || passedPort === '{auto}') {
- if (ports.auto.length) {
- cncserver.settings.gConf.set('serialPath', ports.auto[0]);
- console.log(`Using first detected port: "${ports.auto[0]}"...`);
- } else {
- console.error('No matching serial ports detected.');
- }
- } else {
- console.log(`Using passed serial port "${passedPort}"...`);
- }
+// Setup command setter.
+export function setSetupCommands(commands) {
+ state.setupCommands = commands;
+}
- // Send connect to runner...
- const connectPath = cncserver.settings.gConf.get('serialPath');
-
- // Try to connect to serial, or exit with error code.
- if (connectPath === '' || connectPath === '{auto}') {
- console.error(
- `Error #22: ${botController.name} not found. \
- Are you sure it's connected?`
- );
-
- if (options.error) {
- options.error({
- message: `${botController.name} not found.`,
- type: 'notfound',
- });
- }
- } else {
- console.log(`Attempting to open serial port: "${connectPath}"...`);
-
- const connectData = {
- port: connectPath,
- baudRate: Number(botController.baudRate),
- autoReconnect: true,
- autoReconnectTries: 20,
- autoReconnectRate: 5000,
- setupCommands: serial.setupCommands,
- };
-
- cncserver.ipc.sendMessage('serial.connect', connectData);
+/**
+ * Helper function to implement matching port information to configured bot
+ * parameters.
+ *
+ * Attempts to match SerialPort.list output to parameters and set global
+ * 'serialPath' to matching port.
+ *
+ * @param {object} botControllerConf
+ * The configured bot controller to try to match
+ * @param {function} callback
+ * The callback function when async getports completes. Returns single
+ * object argument containing three keys:
+ * auto {array}: Array of auto detected port names based on bot conf.
+ * Empty array if none found.
+ * names {array}: Clean flat array of all available comm paths/port names.
+ * full {array}: Array of all valid serial port objects for debugging.
+ */
+export function autoDetectPort(botControllerConf, callback) {
+ const botMaker = botControllerConf.manufacturer.toLowerCase();
+ const botProductId = parseInt(botControllerConf.productId.toLowerCase(), 10);
+ const botName = botControllerConf.name.toLowerCase();
+
+ // Output data arrays.
+ const detectList = [];
+ const portNames = [];
+ const cleanList = [];
+
+ SerialPort.list().then(ports => {
+ ports.forEach(port => {
+ const portMaker = (port.manufacturer || '').toLowerCase();
+ // Convert reported product ID from hex string to decimal.
+ const portProductId = parseInt(
+ `0x${(port.productId || '').toLowerCase()}`,
+ 16 // TODO: Is this right?
+ );
+ const portPnpId = (port.pnpId || '').toLowerCase();
+
+ // Add this port to the clean list if its vendor ID isn't undefined.
+ if (typeof port.vendorId !== 'undefined') {
+ cleanList.push(port);
+ portNames.push(port.comName);
}
- });
- };
-
- /**
- * Helper function to implement matching port information to configured bot
- * parameters.
- *
- * Attempts to match SerialPort.list output to parameters and set global
- * 'serialPath' to matching port.
- *
- * @param {object} botControllerConf
- * The configured bot controller to try to match
- * @param {function} callback
- * The callback function when async getports completes. Returns single
- * object argument containing three keys:
- * auto {array}: Array of auto detected port names based on bot conf.
- * Empty array if none found.
- * names {array}: Clean flat array of all available comm paths/port names.
- * full {array}: Array of all valid serial port objects for debugging.
- */
- serial.autoDetectPort = (botControllerConf, callback) => {
- const botMaker = botControllerConf.manufacturer.toLowerCase();
- const botProductId = parseInt(botControllerConf.productId.toLowerCase(), 10);
- const botName = botControllerConf.name.toLowerCase();
-
- // Output data arrays.
- const detectList = [];
- const portNames = [];
- const cleanList = [];
-
- SerialPort.list((err, ports) => {
- // TODO: Catch errors thrown here.
- err = err;
-
- ports.forEach((port) => {
- const portMaker = (port.manufacturer || '').toLowerCase();
- // Convert reported product ID from hex string to decimal.
- const portProductId = parseInt(
- `0x${(port.productId || '').toLowerCase()}`,
- 16 // TODO: Is this right?
- );
- const portPnpId = (port.pnpId || '').toLowerCase();
-
- // Add this port to the clean list if its vendor ID isn't undefined.
- if (typeof port.vendorId !== 'undefined') {
- cleanList.push(port);
- portNames.push(port.comName);
- }
-
- // OS specific board detection based on serialport 2.0.5
- switch (process.platform) {
- case 'win32':
- // Match by manufacturer partial only.
- if (portMaker.indexOf(botMaker) !== -1) {
- detectList.push(port.comName);
- }
- break;
- default: // includes 'darwin', 'linux'
- // Match by Exact Manufacturer...
- if (portMaker === botMaker) {
- // Match by exact product ID (hex to dec), or PNP ID partial
- if (portProductId === botProductId
- || portPnpId.indexOf(botName) !== -1) {
- detectList.push(port.comName);
- }
+ // OS specific board detection based on serialport 2.0.5
+ switch (process.platform) {
+ case 'win32':
+ // Match by manufacturer partial only.
+ if (portMaker.indexOf(botMaker) !== -1) {
+ detectList.push(port.comName);
+ }
+
+ break;
+ default: // includes 'darwin', 'linux'
+ // Match by Exact Manufacturer...
+ if (portMaker === botMaker) {
+ // Match by exact product ID (hex to dec), or PNP ID partial
+ if (portProductId === botProductId
+ || portPnpId.indexOf(botName) !== -1) {
+ detectList.push(port.comName);
}
- }
- });
-
- callback({ auto: detectList, names: portNames, full: cleanList });
+ }
+ }
});
- };
- // Util function to just get the full port output from exports.
- serial.getPorts = (cb) => {
- SerialPort.list((err, ports) => {
- cb(ports, err);
- });
- };
-
- // Local triggers.
- serial.localTrigger = (event) => {
- const restriction = cncserver.settings.gConf.get('httpLocalOnly') ? 'localhost' : '*';
- const port = cncserver.settings.gConf.get('httpPort');
- const isVirtual = cncserver.pen.state.simulation ? ' (simulated)' : '';
-
- switch (event) {
- case 'simulationStart':
- console.log('=======Continuing in SIMULATION MODE!!!============');
- cncserver.pen.forceState({ simulation: 1 });
- break;
-
- case 'serialReady':
- console.log(`CNC server API listening on ${restriction}:${port}`);
-
- cncserver.pen.forceState({ simulation: 0 });
- cncserver.serial.localTrigger('botInit');
- cncserver.server.start();
-
- // Initialize scratch v2 endpoint & API.
- // TODO: This needs to be moved out into something more self contained.
- if (cncserver.settings.gConf.get('scratchSupport')) {
- cncserver.scratch.initAPI();
- }
- break;
-
- case 'serialClose':
- console.log(
- `Serialport connection to "${cncserver.settings.gConf.get(
- 'serialPath'
- )}" lost!! Did it get unplugged?`
- );
-
- // Assume the serialport isn't coming back... It's on a long vacation!
- cncserver.settings.gConf.set('serialPath', '');
- cncserver.serial.localTrigger('simulationStart');
- break;
-
- case 'botInit':
- console.info(
- `---=== ${cncserver.settings.botConf.get(
- 'name'
- )}${isVirtual} is ready to receive commands ===---`
- );
- break;
- default:
+ callback({ auto: detectList, names: portNames, full: cleanList });
+ }).catch(err => {
+ // TODO: Catch errors thrown here.
+ // err = err;
+ });
+}
+
+/**
+ * Helper function to manage initial serial connection and reconnection.
+ *
+ * @param {object} options
+ * Holds all possible callbacks for the serial connection:
+ * connect: Callback for successful serial connect event
+ * success: Callback for general success
+ * error: Callback for init/connect error, arg of error string/object
+ * disconnect: Callback for close/unexpected disconnect
+ * complete: Callback for general completion
+ */
+export function connect(options) {
+ // Apply any passed callbacks to a new serial callbacks object.
+ callbacks.connect = options.connect;
+ callbacks.disconnect = options.disconnect;
+ callbacks.error = options.error;
+ callbacks.success = options.success;
+
+ // Run everything through the callback as port list is async.
+ console.log('Finding available serial ports...');
+ const botController = botConf.get('controller');
+ autoDetectPort(botController, ports => {
+ // Give some console feedback on ports.
+ if (gConf.get('debug')) {
+ console.log('Full Available Port Data:', ports.full);
+ } else {
+ const names = ports.names.length ? ports.names.join(', ') : '[NONE]';
+ console.log(`Available Serial ports: ${names}`);
}
- };
- // Exports...
- serial.exports = {
- getPorts: serial.getPorts,
- };
+ const passedPort = gConf.get('serialPath');
+ if (passedPort === '' || passedPort === '{auto}') {
+ if (ports.auto.length) {
+ gConf.set('serialPath', ports.auto[0]);
+ console.log(`Using first detected port: "${ports.auto[0]}"...`);
+ } else {
+ console.error('No matching serial ports detected.');
+ }
+ } else {
+ console.log(`Using passed serial port "${passedPort}"...`);
+ }
- return serial;
-};
+ // Send connect to runner...
+ const connectPath = gConf.get('serialPath');
+
+ // Try to connect to serial, or exit with error code.
+ if (connectPath === '' || connectPath === '{auto}') {
+ console.error(
+ `Error #22: ${botController.name} not found. \
+ Are you sure it's connected?`
+ );
+
+ if (options.error) {
+ options.error({
+ message: `${botController.name} not found.`,
+ type: 'notfound',
+ });
+ }
+ } else {
+ console.log(`Attempting to open serial port: "${connectPath}"...`);
+
+ const connectData = {
+ port: connectPath,
+ baudRate: Number(botController.baudRate),
+ autoReconnect: true,
+ autoReconnectTries: 20,
+ autoReconnectRate: 5000,
+ setupCommands: state.setupCommands,
+ };
+
+ ipc.sendMessage('serial.connect', connectData);
+ }
+ });
+}
+
+// Util function to just get the full port output from exports.
+export function getPorts(cb) {
+ SerialPort.list((err, ports) => {
+ cb(ports, err);
+ });
+}
+
+// Local triggers.
+export function localTrigger(event) {
+ const restriction = gConf.get('httpLocalOnly') ? 'localhost' : '*';
+ const port = gConf.get('httpPort');
+ const isVirtual = penState.simulation ? ' (simulated)' : '';
+
+ switch (event) {
+ case 'simulationStart':
+ console.log('=======Continuing in SIMULATION MODE!!!============');
+ forceState({ simulation: 1 });
+ break;
+
+ case 'serialReady':
+ console.log(`CNC server API listening on ${restriction}:${port}`);
+
+ forceState({ simulation: 0 });
+ localTrigger('botInit');
+ server.start();
+
+ // Initialize scratch v2 endpoint & API.
+ // TODO: This needs to be moved out into something more self contained.
+ if (gConf.get('scratchSupport')) {
+ initScratch();
+ }
+ break;
+
+ case 'serialClose':
+ console.log(
+ `Serialport connection to "${gConf.get(
+ 'serialPath'
+ )}" lost!! Did it get unplugged?`
+ );
+
+ // Assume the serialport isn't coming back... It's on a long vacation!
+ gConf.set('serialPath', '');
+ localTrigger('simulationStart');
+ break;
+
+ case 'botInit':
+ console.info(
+ `---=== ${botConf.get(
+ 'name'
+ )}${isVirtual} is ready to receive commands ===---`
+ );
+ break;
+ default:
+ }
+}
diff --git a/src/components/core/comms/cncserver.server.js b/src/components/core/comms/cncserver.server.js
index 3935d38f..6fa9cace 100644
--- a/src/components/core/comms/cncserver.server.js
+++ b/src/components/core/comms/cncserver.server.js
@@ -2,108 +2,106 @@
* @file Abstraction module for all express/server related code for CNC Server!
*
*/
-const express = require('express'); // Express Webserver Requires
-const slashes = require('connect-slashes'); // Middleware to manage URI slashes
-const http = require('http');
-const path = require('path');
-const { homedir } = require('os');
-
-const server = {}; // Global component export.
-
-module.exports = (cncserver) => {
- server.app = express(); // Create router (app).
-
- // Setup the cental server object.
- server.httpServer = http.createServer(server.app);
-
- // Global express initialization (must run before any endpoint creation)
- server.app.configure(() => {
- // Base static path for remote interface.
- server.app.use('/', express.static(path.join(global.__basedir, 'interface')));
-
- // Configure module JS file mime type.
- express.static.mime.define({ 'text/javascript': ['mjs'] });
-
- // Add static libraries from node_modules.
- const nm = path.resolve(global.__basedir, '..', 'node_modules');
-
- // Custom static dirs.
- const statics = {
- paper: path.join(nm, 'paper', 'dist'),
- axios: path.join(nm, 'axios', 'dist'),
- jquery: path.join(nm, 'jquery', 'dist'),
- jsonform: path.join(nm, 'jsonform', 'lib'),
- underscore: path.join(nm, 'underscore'),
- bulma: path.join(nm, 'bulma', 'css'),
- select2: path.join(nm, 'select2', 'dist'),
- jsoneditor: path.join(nm, '@json-editor', 'json-editor', 'dist'),
- bootstrap: path.join(nm, 'bootstrap', 'dist'),
- 'font-awesome': path.join(nm, '@fortawesome', 'fontawesome-free', 'css'),
- webfonts: path.join(nm, '@fortawesome', 'fontawesome-free', 'webfonts'),
- modules: path.resolve(global.__basedir, '..', 'web_modules'),
- home: path.join(path.resolve(homedir(), 'cncserver')),
- };
-
- // Add routing for all static dirs.
- Object.entries(statics).forEach(([staticPath, dirSource]) => {
- server.app.use(`/${staticPath}`, express.static(dirSource));
- });
-
- // Setup remaining middleware.
- server.app.use(express.bodyParser());
- server.app.use(slashes());
+import express from 'express'; // Express Webserver Requires
+import slashes from 'connect-slashes'; // Middleware to manage URI slashes
+import http from 'http';
+import path from 'path';
+import { homedir } from 'os';
+import { trigger } from 'cs/binder';
+import { gConf } from 'cs/settings';
+import { __basedir } from 'cs/utils';
+
+export const app = express(); // Create router (app).
+
+// Setup the cental server object.
+export const httpServer = http.createServer(app);
+
+// Global express initialization (must run before any endpoint creation)
+app.configure(() => {
+ console.log('APP CONFIG ======================================= ');
+ // Base static path for remote interface.
+ app.use('/', express.static(path.join(__basedir, 'interface')));
+
+ // Configure module JS file mime type.
+ express.static.mime.define({ 'text/javascript': ['mjs'] });
+
+ // Add static libraries from node_modules.
+ const nm = path.resolve(__basedir, '..', 'node_modules');
+
+ // Custom static dirs.
+ const statics = {
+ paper: path.join(nm, 'paper', 'dist'),
+ axios: path.join(nm, 'axios', 'dist'),
+ jquery: path.join(nm, 'jquery', 'dist'),
+ jsonform: path.join(nm, 'jsonform', 'lib'),
+ underscore: path.join(nm, 'underscore'),
+ bulma: path.join(nm, 'bulma', 'css'),
+ chroma: path.join(nm, 'chroma-js'),
+ select2: path.join(nm, 'select2', 'dist'),
+ jsoneditor: path.join(nm, '@json-editor', 'json-editor', 'dist'),
+ bootstrap: path.join(nm, 'bootstrap', 'dist'),
+ 'font-awesome': path.join(nm, '@fortawesome', 'fontawesome-free', 'css'),
+ webfonts: path.join(nm, '@fortawesome', 'fontawesome-free', 'webfonts'),
+ modules: path.resolve(__basedir, '..', 'web_modules'),
+ home: path.join(path.resolve(homedir(), 'cncserver')),
+ };
- // Allow any implementing binder support for middleware or static routes.
- cncserver.binder.trigger('server.configure', server.app);
+ // Add routing for all static dirs.
+ Object.entries(statics).forEach(([staticPath, dirSource]) => {
+ app.use(`/${staticPath}`, express.static(dirSource));
});
- // Start express HTTP server for API on the given port
- let serverStarted = false;
-
- /**
- * Attempt to start the server.
- */
- server.start = () => {
- // Only run start server once...
- if (serverStarted) return;
- serverStarted = true;
+ // Setup remaining middleware.
+ app.use(express.bodyParser());
+ app.use(slashes());
- const hostname = cncserver.settings.gConf.get('httpLocalOnly') ? 'localhost' : null;
+ // Allow any implementing binder support for middleware or static routes.
+ trigger('server.configure', app, true);
+});
- // Catch Addr in Use Error
- server.httpServer.on('error', (e) => {
- if (e.code === 'EADDRINUSE') {
- console.log('Address in use, retrying...');
- setTimeout(() => {
- server.close();
- server.httpServer.listen(cncserver.settings.gConf.get('httpPort'), hostname);
- }, 1000);
- }
- });
+// Start express HTTP server for API on the given port
+let serverStarted = false;
- server.httpServer.listen(
- cncserver.settings.gConf.get('httpPort'),
- hostname,
- () => {
- // Properly close down server on fail/close
- process.on('SIGTERM', (err) => {
- console.log(err);
- server.close();
- });
- }
- );
- };
-
- /**
- * Attempt to close down the server.
- */
- server.close = () => {
- try {
- server.httpServer.close();
- } catch (e) {
- console.log("Whoops, server wasn't running.. Oh well.");
+/**
+ * Attempt to close down the server.
+ */
+export function close() {
+ try {
+ httpServer.close();
+ } catch (e) {
+ console.log("Whoops, server wasn't running.. Oh well.");
+ }
+}
+/**
+ * Attempt to start the server.
+ */
+export function start() {
+ // Only run start server once...
+ if (serverStarted) return;
+ serverStarted = true;
+
+ const hostname = gConf.get('httpLocalOnly') ? 'localhost' : null;
+
+ // Catch Addr in Use Error
+ httpServer.on('error', err => {
+ if (err.code === 'EADDRINUSE') {
+ console.log('Address in use, retrying...');
+ setTimeout(() => {
+ close();
+ httpServer.listen(gConf.get('httpPort'), hostname);
+ }, 1000);
}
- };
+ });
- return server;
-};
+ httpServer.listen(
+ gConf.get('httpPort'),
+ hostname,
+ () => {
+ // Properly close down server on fail/close
+ process.on('SIGTERM', err => {
+ console.log(err);
+ close();
+ });
+ }
+ );
+}
diff --git a/src/components/core/comms/cncserver.sockets.js b/src/components/core/comms/cncserver.sockets.js
index cc65c375..1a4829a5 100644
--- a/src/components/core/comms/cncserver.sockets.js
+++ b/src/components/core/comms/cncserver.sockets.js
@@ -2,187 +2,214 @@
* @file Abstraction module for all Socket I/O related code for CNC Server!
*
*/
-const socketio = require('socket.io');
+import socketio from 'socket.io';
+import { bindTo, trigger } from 'cs/binder';
+import { httpServer } from 'cs/server';
+import { setPen, state as penState } from 'cs/pen';
+import { state as actualPenState } from 'cs/actualPen';
+import { state as bufferState } from 'cs/buffer';
+import { layers } from 'cs/drawing/base';
+import { snapPathsToColorset } from 'cs/drawing/colors';
+
+const io = socketio(httpServer);
+
+// Shortcut functions for move/height streaming.
+export const shortcut = {
+ move: data => {
+ setPen(data, () => {
+ if (data.returnData) io.emit('move', penState);
+ });
+ },
-const sockets = {}; // Export interface.
+ height: data => {
+ setPen({ state: data.state }, () => {
+ if (data.returnData) io.emit('height', penState);
+ });
+ },
+};
-module.exports = (cncserver) => {
- const io = socketio(cncserver.server.httpServer);
+/**
+ * Send an update to all stream clients when something is added to the buffer.
+ * Includes only the item added to the buffer, expects the client to handle.
+ */
+export function sendBufferAdd(item, hash) {
+ const data = {
+ type: 'add',
+ item,
+ hash,
+ };
- // SOCKET DATA STREAM ========================================================
- io.on('connection', (socket) => {
- // Send buffer and pen updates on user connect
- cncserver.sockets.sendPenUpdate();
+ /* TODO: This is not the right way to do this.
+ if (cncserver.exports.bufferUpdateTrigger) {
+ cncserver.exports.bufferUpdateTrigger(data);
+ }
+ */
- // TODO: this likely needs to be sent ONLY to new connections
- cncserver.sockets.sendBufferComplete();
+ io.emit('buffer update', data);
+}
- socket.on('disconnect', () => {
- // console.log('user disconnected');
- });
+/**
+ * Send an update to all stream clients when something is removed from the
+ * buffer. Assumes the client knows where to remove from.
+ */
+export function sendBufferRemove() {
+ const data = {
+ type: 'remove',
+ };
- // Shortcuts for moving and height for streaming lots of commands.
- socket.on('move', cncserver.sockets.shortcut.move);
- socket.on('height', cncserver.sockets.shortcut.height);
- });
+ /* TODO: This is not the right way to do this.
+ if (cncserver.exports.bufferUpdateTrigger) {
+ cncserver.exports.bufferUpdateTrigger(data);
+ }
+ */
+ io.emit('buffer update', data);
+}
- /**
- * Send an update to all Stream clients about the actualPen object.
- * Called whenever actualPen object has been changed, E.G.: right before
- * a serial command is run, or internal state changes.
- */
- sockets.sendPenUpdate = () => {
- if (cncserver.exports.penUpdateTrigger) {
- cncserver.exports.penUpdateTrigger(cncserver.actualPen.state);
- }
- io.emit('pen update', cncserver.actualPen.state);
+/**
+ * Send an update to all stream clients when something is added to the buffer.
+ * Includes only the item added to the buffer, expects the client to handle.
+ */
+export function sendBufferVars() {
+ const data = {
+ type: 'vars',
+ bufferRunning: bufferState.running,
+ bufferPaused: bufferState.paused,
+ bufferPausePen: bufferState.pausePen,
};
- /**
- * Send an update to all stream clients when something is added to the buffer.
- * Includes only the item added to the buffer, expects the client to handle.
- */
- sockets.sendBufferAdd = (item, hash) => {
- const data = {
- type: 'add',
- item,
- hash,
- };
-
- if (cncserver.exports.bufferUpdateTrigger) {
- cncserver.exports.bufferUpdateTrigger(data);
- }
- io.emit('buffer update', data);
- };
+ /* TODO: This is not the right way to do this.
+ if (cncserver.exports.bufferUpdateTrigger) {
+ cncserver.exports.bufferUpdateTrigger(data);
+ }
+ */
+ io.emit('buffer update', data);
+}
- /**
- * Send an update to all stream clients when something is removed from the
- * buffer. Assumes the client knows where to remove from.
- */
- sockets.sendBufferRemove = () => {
- const data = {
- type: 'remove',
- };
+/**
+ * Send an update to all stream clients of the given custom text string.
+ *
+ * @param {string} message
+ * Message to send out to all clients.
+ */
+export function sendMessageUpdate(message) {
+ io.emit('message update', {
+ message,
+ timestamp: new Date().toString(),
+ });
+}
- if (cncserver.exports.bufferUpdateTrigger) {
- cncserver.exports.bufferUpdateTrigger(data);
- }
- io.emit('buffer update', data);
- };
+/**
+ * Send an update to all stream clients of a machine name callback event.
+ *
+ * @param {string} name
+ * Machine name of callback to send to clients
+ */
+export function sendCallbackUpdate(name) {
+ io.emit('callback update', {
+ name,
+ timestamp: new Date().toString(),
+ });
+}
- /**
- * Send an update to all stream clients when something is added to the buffer.
- * Includes only the item added to the buffer, expects the client to handle.
- */
- sockets.sendBufferVars = () => {
- const data = {
- type: 'vars',
- bufferRunning: cncserver.buffer.running,
- bufferPaused: cncserver.buffer.paused,
- bufferPausePen: cncserver.buffer.pausePen,
- };
-
- if (cncserver.exports.bufferUpdateTrigger) {
- cncserver.exports.bufferUpdateTrigger(data);
- }
- io.emit('buffer update', data);
- };
+/**
+ * Trigger manual swap complete to all stream clients. Buffer will be paused.
+ *
+ * @param {int} vIndex
+ * Virtual index of manual swap
+ */
+export function manualSwapTrigger(vIndex) {
+ io.emit('manualswap trigger', { index: vIndex });
+}
- /**
- * Send an update to all stream clients about everything buffer related.
- * Called only during connection inits.
- */
- sockets.sendBufferComplete = () => {
- const data = {
- type: 'complete',
- bufferList: cncserver.buffer.data,
- bufferData: cncserver.buffer.dataSet,
- bufferRunning: cncserver.buffer.running,
- bufferPaused: cncserver.buffer.paused,
- bufferPausePen: cncserver.buffer.pausePen,
- };
-
- // Low-level event callback trigger to avoid Socket.io overhead
- if (cncserver.exports.bufferUpdateTrigger) {
- cncserver.exports.bufferUpdateTrigger(data);
- }
- io.emit('buffer update', data);
-
- // Send this second.
- sockets.sendPaperUpdate('preview');
- sockets.sendPaperUpdate('stage');
- };
+/**
+ * Clientside helper for keeping object variable states in sync.
+ *
+ * @export
+ * @param {string} key
+ * Global recognized key for variable (colorset, pen, etc).
+ * @param {*} value
+ * New complete value for item.
+ */
+export function liveStateUpdate(key, value) {
+ io.emit(`livestate ${key}`, value);
+}
- /**
- * Send an update to all stream clients of the given custom text string.
- *
- * @param {string} message
- * Message to send out to all clients.
- */
- sockets.sendMessageUpdate = (message) => {
- io.emit('message update', {
- message,
- timestamp: new Date().toString(),
- });
- };
+// Manage state change via binders.
+bindTo('colors.update', 'sockets.liveupdate', colorset => {
+ liveStateUpdate('colorset', colorset);
+});
+bindTo('project.update', 'sockets.liveupdate', project => {
+ liveStateUpdate('project', project);
+});
- /**
- * Send an update to all stream clients of a machine name callback event.
- *
- * @param {string} name
- * Machine name of callback to send to clients
- */
- sockets.sendCallbackUpdate = (name) => {
- io.emit('callback update', {
- name,
- timestamp: new Date().toString(),
- });
- };
+bindTo('pen.update', 'sockets.liveupdate', pen => {
+ liveStateUpdate('pen', pen);
+});
- /**
- * Trigger manual swap complete to all stream clients. Buffer will be paused.
- *
- * @param {int} vIndex
- * Virtual index of manual swap
- */
- sockets.manualSwapTrigger = (vIndex) => {
- io.emit('manualswap trigger', { index: vIndex });
- };
+bindTo('actualpen.update', 'sockets.liveupdate', actualPen => {
+ liveStateUpdate('actualPen', actualPen);
+});
- /**
- * Send an update to all stream clients for a Paper layer update.
- */
- sockets.sendPaperUpdate = (layer = 'preview') => {
- if (layer === 'preview') {
- cncserver.drawing.colors.snapPathColors(
- cncserver.drawing.base.layers.preview
- );
- }
-
- io.emit('paper layer', {
- layer,
- paperJSON: cncserver.drawing.base.layers[layer].exportJSON(),
- timestamp: new Date().toString(),
- });
- };
+/**
+ * Send an update to all stream clients for a Paper layer update.
+ */
+export function sendPaperUpdate(layer = 'preview') {
+ if (layer === 'preview') {
+ snapPathsToColorset(layers.preview);
+ sendPaperUpdate('print');
+ }
+
+ io.emit('paper layer', {
+ layer,
+ paperJSON: layers[layer].exportJSON(),
+ timestamp: new Date().toString(),
+ });
+}
- // Shortcut functions for move/height streaming.
- sockets.shortcut = {
- move: (data) => {
- cncserver.pen.setPen(data, () => {
- if (data.returnData) io.emit('move', cncserver.pen.state);
- });
- },
-
- height: (data) => {
- cncserver.pen.setPen({ state: data.state }, () => {
- if (data.returnData) io.emit('height', cncserver.pen.state);
- });
- },
+/**
+ * Send an update to all stream clients about everything buffer related.
+ * Called only during connection inits.
+ */
+export function sendBufferComplete() {
+ const data = {
+ type: 'complete',
+ bufferList: bufferState.data,
+ bufferData: bufferState.dataSet,
+ bufferRunning: bufferState.running,
+ bufferPaused: bufferState.paused,
+ bufferPausePen: bufferState.pausePen,
};
- return sockets;
-};
+ // Low-level event callback trigger to avoid Socket.io overhead
+ /* TODO: This is not the right way to do this.
+ if (cncserver.exports.bufferUpdateTrigger) {
+ cncserver.exports.bufferUpdateTrigger(data);
+ }
+ */
+ io.emit('buffer update', data);
+
+ // Send this second.
+ sendPaperUpdate('preview');
+ sendPaperUpdate('stage');
+ sendPaperUpdate('tools');
+}
+
+// SOCKET DATA STREAM.
+io.on('connection', socket => {
+ // Send buffer and pen updates on user connect
+ trigger('pen.update', penState);
+
+ // TODO: this likely needs to be sent ONLY to new connections
+ sendBufferComplete();
+
+ socket.on('disconnect', () => {
+ // console.log('user disconnected');
+ });
+
+ // Shortcuts for moving and height for streaming lots of commands.
+ socket.on('move', shortcut.move);
+ socket.on('height', shortcut.height);
+});
diff --git a/src/components/core/control/cncserver.actualpen.js b/src/components/core/control/cncserver.actualpen.js
index c5910b77..3983ab8e 100644
--- a/src/components/core/control/cncserver.actualpen.js
+++ b/src/components/core/control/cncserver.actualpen.js
@@ -2,46 +2,31 @@
* @file Abstraction module for functions that generate movement or help
* functions for calculating movement command generation for CNC Server!
*/
-const extend = require('util')._extend; // Util for cloning objects
+import { bindTo, trigger } from 'cs/binder';
+import { applyObjectTo } from 'cs/utils';
-const actualPen = {}; // Exposed export.
+// actualPen: This is set to the state of the pen variable as it passes through
+// the buffer queue and into the robot, meant to reflect the actual position and
+// state of the robot, and will be where the pen object is reset to when the
+// buffer is cleared and the future state is lost.
+export const state = {};
-module.exports = (cncserver) => {
- // actualPen: This is set to the state of the pen variable as it passes through
- // the buffer queue and into the robot, meant to reflect the actual position and
- // state of the robot, and will be where the pen object is reset to when the
- // buffer is cleared and the future state is lost.
- actualPen.state = extend({}, cncserver.pen.state);
-
- /**
- * Set internal state object as an extended copy of the passed object.
- *
- * @param {object} state
- */
- actualPen.updateState = (state) => {
- actualPen.state = extend({}, state);
-
- // Trigger an update for actualPen change.
- cncserver.sockets.sendPenUpdate();
- };
-
- /**
- * Force the values of a given set of keys within the actualPen state.
- *
- * @param {object} inState
- * Flat object of key/value pairs to FORCE into the actualPen state. Only
- * used to fix state when it needs correcting from inherited buffer.
- * EG: After a cancel/estop.
- */
- actualPen.forceState = (inState) => {
- for (const [key, value] of Object.entries(inState)) {
- // Only set a value if the key exists in the state already.
- if (key in actualPen.state) {
- actualPen.state[key] = value;
- }
- }
- cncserver.sockets.sendPenUpdate();
- };
+/**
+ * Force the values of a given set of keys within the actualPen state.
+ *
+ * @param {object} inState
+ * Flat object of key/value pairs to FORCE into the actualPen state. Only
+ * used to fix state when it needs correcting from inherited buffer.
+ * EG: After a cancel/estop.
+ */
+export function forceState(inState) {
+ // Only trigger update if state changed.
+ if (applyObjectTo(inState, state)) {
+ trigger('actualpen.update', state);
+ }
+}
- return actualPen;
-};
+// On pen setup, force state to match.
+bindTo('pen.setup', 'actualPen', newState => {
+ forceState(newState);
+});
diff --git a/src/components/core/control/cncserver.content.js b/src/components/core/control/cncserver.content.js
index f7a83849..9819ec4e 100644
--- a/src/components/core/control/cncserver.content.js
+++ b/src/components/core/control/cncserver.content.js
@@ -1,495 +1,488 @@
/**
* @file Abstraction for high level project content management.
*/
-const { Raster, PointText } = require('paper');
-const request = require('request');
-const DataURI = require('datauri');
-
-// Exposed export to be attached as cncserver.content
-const content = {
- id: 'content',
- items: new Map(),
- limits: {
- fileSize: 9 * 1024 * 1024, // TODO: Get this into settings somewhere.
- mimetypes: {
- svg: ['image/svg+xml'],
- path: ['text/plain'],
- paper: ['application/json'],
- raster: ['image/bmp', 'image/gif', 'image/png', 'image/jpeg'],
- text: ['text/plain'],
- },
- // Extensions from mimetype.
- extensions: {
- 'image/svg+xml': 'svg',
- 'application/json': 'json',
- 'text/plain': 'txt',
- 'image/bmp': 'bmp',
- 'image/gif': 'gif',
- 'image/png': 'png',
- 'image/jpeg': 'jpg',
- },
+import Paper from 'paper';
+import request from 'request';
+import DataURI from 'datauri';
+import { singleLineString, getHash, merge } from 'cs/utils';
+import { sendPaperUpdate } from 'cs/sockets';
+import { getDataDefault } from 'cs/schemas';
+import * as drawing from 'cs/drawing';
+import * as projects from 'cs/projects';
+
+const { Raster, PointText } = Paper;
+
+export const items = new Map();
+export const limits = { // TODO: Get this into settings somewhere.
+ fileSize: 9 * 1024 * 1024,
+ mimetypes: {
+ svg: ['image/svg+xml'],
+ path: ['text/plain'],
+ paper: ['application/json'],
+ raster: ['image/bmp', 'image/gif', 'image/png', 'image/jpeg'],
+ text: ['text/plain'],
+ },
+ // Extensions from mimetype.
+ extensions: {
+ 'image/svg+xml': 'svg',
+ 'application/json': 'json',
+ 'text/plain': 'txt',
+ 'image/bmp': 'bmp',
+ 'image/gif': 'gif',
+ 'image/png': 'png',
+ 'image/jpeg': 'jpg',
},
};
-module.exports = (cncserver) => {
- const { utils, projects } = cncserver;
- const { limits } = content;
-
- /**
- * Validates a source object to contain the correctly formatted string.
- * Converts raster binary URL input to data URI as needed.
- *
- * @param {object} source
- * "Content" schema validated source object from request.
- *
- * @returns {Promise}
- * Resolves with source object with content key populated with string,
- * rejects error object detailing problem.
- */
- function normalizeContentSource(source) {
- return new Promise((resolve, reject) => {
- // First, sanity check that we have anything to work with.
- if (!source.url && !source.content) {
- reject(new Error('Request failed: Must include either source content or url'));
- } else if (source.url && !source.content) {
- // Validate URL via HEADer request first, don't download if we don't have to.
- request({ url: source.url, method: 'HEAD' }, (error, res) => {
- const mimetype = res.headers['content-type'];
-
- // How'd the request go?
- if (error) {
- // Escape on error.
- reject(error);
- } else if (res.statusCode < 200 || res.statusCode > 202) {
- // Escape if the server says something bad happened.
- reject(
- new Error(`Request failed: Server returned status code ${res.statusCode}`)
- );
- } else if (res.headers['content-length'] > limits.fileSize) {
- // Escape if the file is too big.
- reject(
- new Error(
- utils.singleLineString`Request failed: Requested file size
- (${res.headers['content-length']} bytes) is greater than maximum
- allowed (${limits.fileSize} bytes)`
- )
- );
- } else if (!limits.mimetypes[source.type].includes(mimetype)) {
- reject(
- new Error(
- utils.singleLineString`Content type for URL is
- '${mimetype}', must be one of
- [${limits.mimetypes[source.type].join(', ')}]
- for type: ${source.type}`
- )
- );
-
- // We should be good! Actually go get the file.
- } else if (source.type === 'raster') {
- // Binary source content needs to be converted to data URI.
- // TODO: Handling this like this means a LOT of memory usage for larger files,
- // should move to buffer/stream based handling.
- request.get({ url: source.url, encoding: null }, (getErr, getRes, body) => {
- if (getErr) {
- reject(getErr);
- } else {
- const datauri = new DataURI();
- datauri.format(limits.extensions[mimetype], body);
- resolve({
- ...source,
- mimetype,
- content: datauri.content,
- });
- }
- });
- } else {
- // UTF-8/ASCII text content can just be grabbed directly.
- request.get({ url: source.url }, (getErr, getRes, body) => {
- if (getErr) {
- reject(getErr);
- } else {
- resolve({
- ...source,
- content: body,
- mimetype: limits.mimetypes[source.type][0],
- });
- }
- });
- }
- });
- } else {
- // We already have the content, resolve the full source object.
- // TODO: Validate that raster input handed here is actually a data URI
- // as the error for adding bad data here isn't helpful.
- resolve({ ...source, mimetype: limits.mimetypes[source.type][0] });
- }
- });
- }
-
- /**
- * Normalize incoming content between allowed types given intent.
- * Imports content to "import" layer in prep for adding to project.
- *
- * @param {object} payload
- * The content schema validated object from request to be imported.
- *
- * @return {Promise}
- * Promise that returns on success the imported & normalized input, or error
- * on failure.
- */
- content.normalizeInput = payload => new Promise((success, err) => {
- const { drawing: { base: { layers } } } = cncserver;
- const settings = projects.getFullSettings(payload.settings);
-
- // Validate the incoming request for correct project destination.
- if (!payload.project) {
- // Default to current.
- payload.project = projects.getCurrentHash();
- } else if (!projects.items.get(payload.project)) {
- err(new Error(
- utils.singleLineString`Project identified by '${payload.project}' does
- not exist. Verify you have the correct project, or create a new one
- before adding content.`
- ));
- return;
- }
-
- // Get the data from the URL or the content.
- normalizeContentSource(payload.source).then((source) => {
- // Draw to empty import layer.
- layers.import.activate();
- layers.import.removeChildren();
-
- let item = null;
-
- // What kind of input content is this?
- // Options: svg, path, paper, raster, text
- switch (source.type) {
- // Full SVG XML content
- case 'svg':
- try {
- item = layers.import.importSVG(source.content.trim(), {
- expandShapes: true,
- applyMatrix: true,
- });
-
- if (!item || !item.children) {
- err(new Error(
- utils.singleLineString`Failed to import SVG, please validate
- source content and try again.`
- ));
+/**
+ * Validates a source object to contain the correctly formatted string.
+ * Converts raster binary URL input to data URI as needed.
+ *
+ * @param {object} source
+ * "Content" schema validated source object from request.
+ *
+ * @returns {Promise}
+ * Resolves with source object with content key populated with string,
+ * rejects error object detailing problem.
+ */
+function normalizeContentSource(source) {
+ return new Promise((resolve, reject) => {
+ // First, sanity check that we have anything to work with.
+ if (!source.url && !source.content) {
+ reject(new Error('Request failed: Must include either source content or url'));
+ } else if (source.url && !source.content) {
+ // Validate URL via HEADer request first, don't download if we don't have to.
+ request({ url: source.url, method: 'HEAD' }, (error, res) => {
+ const mimetype = res.headers['content-type'];
+
+ // How'd the request go?
+ if (error) {
+ // Escape on error.
+ reject(error);
+ } else if (res.statusCode < 200 || res.statusCode > 202) {
+ // Escape if the server says something bad happened.
+ reject(
+ new Error(`Request failed: Server returned status code ${res.statusCode}`)
+ );
+ } else if (res.headers['content-length'] > limits.fileSize) {
+ // Escape if the file is too big.
+ reject(
+ new Error(
+ singleLineString`Request failed: Requested file size
+ (${res.headers['content-length']} bytes) is greater than maximum
+ allowed (${limits.fileSize} bytes)`
+ )
+ );
+ } else if (!limits.mimetypes[source.type].includes(mimetype)) {
+ reject(
+ new Error(
+ singleLineString`Content type for URL is
+ '${mimetype}', must be one of
+ [${limits.mimetypes[source.type].join(', ')}]
+ for type: ${source.type}`
+ )
+ );
+
+ // We should be good! Actually go get the file.
+ } else if (source.type === 'raster') {
+ // Binary source content needs to be converted to data URI.
+ // TODO: Handling this like this means a LOT of memory usage for larger files,
+ // should move to buffer/stream based handling.
+ request.get({ url: source.url, encoding: null }, (getErr, getRes, body) => {
+ if (getErr) {
+ reject(getErr);
} else {
- success({ ...payload, source, item });
+ const datauri = new DataURI();
+ datauri.format(limits.extensions[mimetype], body);
+ resolve({
+ ...source,
+ mimetype,
+ content: datauri.content,
+ });
}
- } catch (error) {
- // Likely couldn't parse SVG.
- err(error);
- }
- break;
-
- case 'path':
- try {
- item = cncserver.drawing.base.normalizeCompoundPath(source.content);
-
- // Apply all path item defaults directly.
- // TODO: streamline this a bit more?
- if (settings.fill.render) item.fillColor = settings.path.fillColor;
- if (settings.stroke.render) item.strokeColor = settings.path.strokeColor;
-
- // Does this come from somewhere else??
- // item.closed = settings.path.closed;
-
- // If we don't have a path at this point we failed to import anything.
- if (!item || !item.length) {
- throw new Error('Invalid path source, verify input content and try again.');
+ });
+ } else {
+ // UTF-8/ASCII text content can just be grabbed directly.
+ request.get({ url: source.url }, (getErr, getRes, body) => {
+ if (getErr) {
+ reject(getErr);
+ } else {
+ resolve({
+ ...source,
+ content: body,
+ mimetype: limits.mimetypes[source.type][0],
+ });
}
-
- layers.import.addChild(item);
- success({ ...payload, source, item });
- } catch (error) {
- // Likely not a valid path string.
- err(error);
- }
- break;
-
- case 'paper':
- try {
- item = layers.import.importJSON(source.content);
- success({ ...payload, source, item });
- } catch (error) {
- // Likely couldn't parse JSON.
- err(error);
- }
- break;
-
- case 'raster':
- // Content should be a Data URI at this point.
- try {
- item = new Raster(source.content);
- item.onError = (error) => {
- err(new Error(`Problem loading raster: ${error}`));
- };
- item.onLoad = () => {
- success({ ...payload, source, item });
- };
- } catch (imageErr) {
- // Couldn't load image.
- err(imageErr);
- }
- break;
-
- case 'text':
- item = new PointText({
- point: cncserver.drawing.base.validateBounds(payload.bounds),
- content: source.content,
});
- success({ ...payload, source, item });
- break;
-
- default:
- break;
- }
- // ^^^ All promise resolves exist in the type specific logic above ^^^
- }).catch(err);
- });
-
- // TODO: Get all items in current project.
- content.getItems = () => {
- const items = [];
- content.items.forEach(({
- hash, title, bounds,
- }) => {
- items.push({
- hash, title, bounds,
+ }
});
- });
- return items;
- };
-
- // Assume schema has been checked by the time we get here.
- content.addItem = payload => new Promise((resolve, reject) => {
- // TODO: We have content! We need to add it to the requested project.
- // - Generate a non-incremental hash for the content string
- // - Write a file with the contents to the project folder: [hash].[extension]
- // - Add functionality for project to retrieve content data to write to its
- // store and JSON.
- // - Build the content object and return.
-
- projects.saveContentFile(payload.source, payload.project).then((filePath) => {
- // Build the final content item.
- const item = { ...payload };
- item.source.content = filePath;
- const hash = utils.getHash(item);
- item.hash = hash;
-
- item.item = cncserver.drawing.stage.import(payload.item, hash, item.bounds);
- content.items.set(hash, item);
-
- const responseItem = content.getResponseItem(hash);
- projects.saveContentData(responseItem, payload.project);
- resolve(responseItem);
- }).catch(reject);
- });
-
- // Format a content item from an internal item into a response item.
- content.getResponseItem = (hash) => {
- const item = content.items.get(hash);
- const fullItem = cncserver.schemas.getDataDefault('content', item);
- return {
- project: item.project,
- hash: item.hash,
- autoRender: fullItem.autoRender,
- title: fullItem.title,
- bounds: fullItem.bounds, // TODO: Format as object
- source: {
- type: item.source.type,
- originalUrl: item.source.url || '',
- content: item.source.content,
- },
- settings: item.settings || {},
- };
- };
-
- // Take in a validated merged item object and compare against the change item
- // to see how to make edits.
- content.editItem = ({ hash }, deltaItem, mergedItem) => new Promise((resolve, reject) => {
- // Changing content? Run through normalizer.
- if (deltaItem.source) {
- content.normalizeInput(mergedItem).then((finalItem) => {
- projects.saveContentFile(finalItem.source, finalItem.project).then((filePath) => {
- // Build the final content item.
- const item = { ...finalItem };
- item.source.content = filePath;
- item.hash = hash;
-
- item.item = cncserver.drawing.stage.import(finalItem.item, hash, item.bounds);
- content.items.set(hash, item);
-
- const responseItem = content.getResponseItem(hash);
- projects.saveContentData(responseItem, mergedItem.project);
- resolve(responseItem);
-
- // Trigger autorender after resolve.
- if (responseItem.autoRender) {
- projects.setRenderingState(true, hash);
- }
- }).catch(reject);
- }).catch(reject);
} else {
- // Otherwise, just assume the deep merged object is good and update stage.
- const item = content.items.get(hash);
- const fullChangedItem = utils.merge(item, deltaItem);
- content.items.set(hash, fullChangedItem);
- cncserver.drawing.stage.updateItem(fullChangedItem);
-
- // TODO: Apply path specific defaults that change stage item (color, fill render, etc);
- const responseItem = content.getResponseItem(hash);
- projects.saveContentData(responseItem, mergedItem.project);
- resolve(responseItem);
-
- // Trigger autorender after resolve.
- if (responseItem.autoRender) {
- projects.setRenderingState(true, hash);
- }
+ // We already have the content, resolve the full source object.
+ // TODO: Validate that raster input handed here is actually a data URI
+ // as the error for adding bad data here isn't helpful.
+ resolve({ ...source, mimetype: limits.mimetypes[source.type][0] });
}
});
+}
+/**
+ * Normalize incoming content between allowed types given intent.
+ * Imports content to "import" layer in prep for adding to project.
+ *
+ * @param {object} payload
+ * The content schema validated object from request to be imported.
+ *
+ * @return {Promise}
+ * Promise that returns on success the imported & normalized input, or error
+ * on failure.
+ */
+export const normalizeInput = payload => new Promise((success, err) => {
+ const settings = projects.getFullSettings(payload.settings);
+
+ // Validate the incoming request for correct project destination.
+ if (!payload.project) {
+ // Default to current.
+ payload.project = projects.getCurrentHash();
+ } else if (!projects.items.get(payload.project)) {
+ err(new Error(
+ singleLineString`Project identified by '${payload.project}' does
+ not exist. Verify you have the correct project, or create a new one
+ before adding content.`
+ ));
+ return;
+ }
- // Pull a piece of content in fully from a file and config.
- content.loadFromFile = (fileItem, data) => {
- const fileName = fileItem.source.content;
- const filePath = projects.getContentFilePath(fileName, fileItem.project);
+ // Get the data from the URL or the content.
+ normalizeContentSource(payload.source).then(source => {
+ const { layers } = drawing.base;
+ // Draw to empty import layer.
+ layers.import.activate();
+ layers.import.removeChildren();
- // Prepare a copy of the item to be validated.
- const item = { ...fileItem };
- item.source.content = data;
+ let item = null;
- content.normalizeInput(item).then((finalItem) => {
- // Build the final content item.
- item.source.content = fileName;
+ // What kind of input content is this?
+ // Options: svg, path, paper, raster, text
+ switch (source.type) {
+ // Full SVG XML content
+ case 'svg':
+ try {
+ item = layers.import.importSVG(source.content.trim(), {
+ expandShapes: true,
+ applyMatrix: true,
+ });
- item.item = cncserver.drawing.stage.import(finalItem.item, item.hash, item.bounds);
- content.items.set(item.hash, item);
- }).catch((err) => {
- console.error(err);
- throw new Error(`Error loading content for file ${filePath}`);
- });
- };
+ if (!item || !item.children) {
+ err(new Error(
+ singleLineString`Failed to import SVG, please validate
+ source content and try again.`
+ ));
+ } else {
+ drawing.base.validateFills(item);
+ success({ ...payload, source, item });
+ }
+ } catch (error) {
+ // Likely couldn't parse SVG.
+ err(error);
+ }
+ break;
- // Fully remove a piece of content from a project.
- content.removeItem = (hash) => {
- const { project } = content.items.get(hash);
- cncserver.drawing.stage.remove(hash);
- content.items.delete(hash);
- projects.removeContentData(hash, project);
- cncserver.sockets.sendPaperUpdate('stage');
- // TODO: Delete file, update project.
- };
+ case 'path':
+ try {
+ item = drawing.base.normalizeCompoundPath(source.content);
- // Render a single content item (that may or may not contain other paths).
- // Assume a fully validated content item with delta settings.
- content.renderContentItem = ({
- hash, title, item, source, bounds, settings: rawSettings, project,
- }) => new Promise((resolve, reject) => {
- const {
- drawing: {
- fill, trace, vectorize, text, preview,
- },
- } = cncserver;
- const settings = projects.getFullSettings(rawSettings);
- const promiseQueue = [];
+ // Apply all path item defaults directly.
+ // TODO: streamline this a bit more?
+ if (settings.fill.render) item.fillColor = settings.path.fillColor;
+ if (settings.stroke.render) item.strokeColor = settings.path.strokeColor;
- console.log(`Rendering ${title} - ${hash}...`);
- preview.remove(hash, true);
+ // Does this come from somewhere else??
+ // item.closed = settings.path.closed;
+ // If we don't have a path at this point we failed to import anything.
+ if (!item || !item.length) {
+ throw new Error('Invalid path source, verify input content and try again.');
+ }
- switch (source.type) {
- // Full Item Group (svg or paper)
- case 'paper':
- case 'svg':
- // Break it down into parts and render those single items.
- promiseQueue.push(content.renderGroup(item, hash, settings));
+ layers.import.addChild(item);
+ success({ ...payload, source, item });
+ } catch (error) {
+ // Likely not a valid path string.
+ err(error);
+ }
break;
- case 'path':
- // Single item render.
- // console.log(settings);
- if (settings.fill.render) {
- promiseQueue.push(fill(item, hash, bounds, settings.fill));
- }
- if (settings.stroke.render) {
- promiseQueue.push(trace(item, hash, bounds, settings.stroke));
+ case 'paper':
+ try {
+ item = layers.import.importJSON(source.content);
+ success({ ...payload, source, item });
+ } catch (error) {
+ // Likely couldn't parse JSON.
+ err(error);
}
break;
case 'raster':
- // Rasterization render (single item only)
- // Full settings are passed here as fills and stroke can be generated.
- if (settings.vectorize.render) {
- const imagePath = projects.getContentFilePath(source.content, project);
- promiseQueue.push(vectorize(imagePath, hash, bounds, settings));
+ // Content should be a Data URI at this point.
+ try {
+ item = new Raster(source.content);
+ item.onError = error => {
+ err(new Error(`Problem loading raster: ${error}`));
+ };
+ item.onLoad = () => {
+ success({ ...payload, source, item });
+ };
+ } catch (imageErr) {
+ // Couldn't load image.
+ err(imageErr);
}
break;
case 'text':
- // Text render to bounds, etc.
- // Full settings are passed here as fills can be sub-rendered on system fonts.
- if (settings.text.render) {
- promiseQueue.push(text.draw(item.content, hash, bounds, settings));
- }
+ item = new PointText({
+ point: drawing.base.validateBounds(payload.bounds),
+ content: drawing.text.format(source.content),
+ data: { originalText: source.content },
+ });
+ success({ ...payload, source, item });
break;
default:
break;
}
-
- Promise.all(promiseQueue).then(resolve).catch(reject);
+ // ^^^ All promise resolves exist in the type specific logic above ^^^
+ }).catch(err);
+});
+
+// TODO: Get all items in current project.
+export function getItems() {
+ const returnItems = [];
+ items.forEach(({
+ hash, title, bounds,
+ }) => {
+ returnItems.push({
+ hash, title, bounds,
+ });
});
-
- // Do everything needed to render a group of path items (no rasters or text).
- content.renderGroup = (group, hash, settings, skipOcclusion = false) => new Promise((resolve, reject) => {
- const {
- fill, trace, occlusion, temp,
- } = cncserver.drawing;
- const { ungroupAllGroups, cleanupInput } = cncserver.drawing.base;
- const promiseQueue = [];
-
- // Render Stroke for all subitems.
- if (settings.stroke.render) {
- const tempGroup = temp.addItem(group, `${hash}-stroke`);
- ungroupAllGroups(tempGroup);
- cleanupInput(tempGroup, settings);
-
- if (settings.stroke.cutoutOcclusion && !skipOcclusion) {
- occlusion('stroke', tempGroup);
+ return returnItems;
+}
+
+// Assume schema has been checked by the time we get here.
+export const addItem = payload => new Promise((resolve, reject) => {
+ // TODO: We have content! We need to add it to the requested project.
+ // - Generate a non-incremental hash for the content string
+ // - Write a file with the contents to the project folder: [hash].[extension]
+ // - Add functionality for project to retrieve content data to write to its
+ // store and JSON.
+ // - Build the content object and return.
+
+ projects.saveContentFile(payload.source, payload.project).then(filePath => {
+ // Build the final content item.
+ const item = { ...payload };
+ item.source.content = filePath;
+ const hash = getHash(item);
+ item.hash = hash;
+
+ item.item = drawing.stage.import(payload.item, hash, item.bounds);
+ items.set(hash, item);
+
+ const responseItem = getResponseItem(hash);
+ projects.saveContentData(responseItem, payload.project);
+ resolve(responseItem);
+ }).catch(reject);
+});
+
+// Format a content item from an internal item into a response item.
+export function getResponseItem(hash) {
+ const item = items.get(hash);
+ const fullItem = getDataDefault('content', item);
+ return {
+ project: item.project,
+ hash: item.hash,
+ autoRender: fullItem.autoRender,
+ title: fullItem.title,
+ bounds: fullItem.bounds, // TODO: Format as object
+ source: {
+ type: item.source.type,
+ originalUrl: item.source.url || '',
+ content: item.source.content,
+ },
+ settings: item.settings || {},
+ };
+}
+
+// Take in a validated merged item object and compare against the change item
+// to see how to make edits.
+export const editItem = (
+ { hash }, deltaItem, mergedItem
+) => new Promise((resolve, reject) => {
+ // Changing content? Run through normalizer.
+ if (deltaItem.source) {
+ normalizeInput(mergedItem).then(finalItem => {
+ projects.saveContentFile(finalItem.source, finalItem.project).then(filePath => {
+ // Build the final content item.
+ const item = { ...finalItem };
+ item.source.content = filePath;
+ item.hash = hash;
+
+ item.item = drawing.stage.import(finalItem.item, hash, item.bounds);
+ items.set(hash, item);
+
+ const responseItem = getResponseItem(hash);
+ projects.saveContentData(responseItem, mergedItem.project);
+ resolve(responseItem);
+
+ // Trigger autorender after resolve.
+ if (responseItem.autoRender) {
+ projects.setRenderingState(true, hash);
+ }
+ }).catch(reject);
+ }).catch(reject);
+ } else {
+ // Otherwise, just assume the deep merged object is good and update stage.
+ const item = items.get(hash);
+ const fullChangedItem = merge(item, deltaItem);
+ items.set(hash, fullChangedItem);
+ drawing.stage.updateItem(fullChangedItem);
+
+ // TODO: Apply path specific defaults to change stage item (color, fill render, etc);
+ const responseItem = getResponseItem(hash);
+ projects.saveContentData(responseItem, mergedItem.project);
+ resolve(responseItem);
+
+ // Trigger autorender after resolve.
+ if (responseItem.autoRender) {
+ projects.setRenderingState(true, hash);
+ }
+ }
+});
+
+// Pull a piece of content in fully from a file and config.
+export function loadFromFile(fileItem, data) {
+ const fileName = fileItem.source.content;
+ const filePath = projects.getContentFilePath(fileName, fileItem.project);
+
+ // Prepare a copy of the item to be validated.
+ const item = { ...fileItem };
+ item.source.content = data;
+
+ normalizeInput(item).then(finalItem => {
+ // Build the final content item.
+ item.source.content = fileName;
+
+ item.item = drawing.stage.importGroup(finalItem.item, item.hash, item.bounds);
+ items.set(item.hash, item);
+ }).catch(err => {
+ console.error(err);
+ throw new Error(`Error loading content for file ${filePath}`);
+ });
+}
+
+// Fully remove a piece of content from a project.
+export function removeItem(hash) {
+ const { project } = items.get(hash);
+ drawing.stage.remove(hash);
+ items.delete(hash);
+ projects.removeContentData(hash, project);
+ drawing.preview.remove(hash, true);
+ sendPaperUpdate('stage');
+ // TODO: Delete file, update project.
+}
+
+// Render a single content item (that may or may not contain other paths).
+// Assume a fully validated content item with delta settings.
+export const renderContentItem = ({
+ hash, title, item, source, bounds, settings: rawSettings, project,
+}) => new Promise((resolve, reject) => {
+ const settings = projects.getFullSettings(rawSettings);
+ const promiseQueue = [];
+
+ console.log(`Rendering ${title} - ${hash}...`);
+ drawing.preview.remove(hash, true);
+
+ switch (source.type) {
+ // Full Item Group (svg or paper)
+ case 'paper':
+ case 'svg':
+ // Break it down into parts and render those single items.
+ promiseQueue.push(renderGroup(item, hash, settings));
+ break;
+
+ case 'path':
+ // Single item render.
+ if (settings.fill.render) {
+ promiseQueue.push(drawing.fill(item, hash, bounds, settings.fill));
+ }
+ if (settings.stroke.render) {
+ promiseQueue.push(drawing.trace(item, hash, bounds, settings.stroke));
}
+ break;
+
+ case 'raster':
+ // Rasterization render (single item only)
+ // Full settings are passed here as fills and stroke can be generated.
+ if (settings.vectorize.render) {
+ const imagePath = projects.getContentFilePath(source.content, project);
+ promiseQueue.push(drawing.vectorize(imagePath, hash, bounds, settings));
+ }
+ break;
+
+ case 'text':
+ // Text render to bounds, etc.
+ // Full settings are passed here as fills can be sub-rendered on system fonts.
+ if (settings.text.render) {
+ promiseQueue.push(
+ drawing.text.draw(item.data.originalText, hash, bounds, settings)
+ );
+ }
+ break;
- // TODO: Only run stroke here if one is needed.
- tempGroup.children.forEach((path) => {
- promiseQueue.push(trace(path, hash, null, settings.stroke));
- });
- }
+ default:
+ break;
+ }
+ Promise.all(promiseQueue).then(resolve).catch(reject);
+});
- // Render Fill for all subitems.
- if (settings.fill.render) {
- const tempGroupFill = temp.addItem(group, `${hash}-fill`);
- ungroupAllGroups(tempGroupFill);
- cleanupInput(tempGroupFill, settings);
+// Do everything needed to render a group of path items (no rasters or text).
+export const renderGroup = (
+ group, hash, settings, skipOcclusion = false
+) => new Promise((resolve, reject) => {
+ const { ungroupAllGroups, cleanupInput } = drawing.base;
+ const promiseQueue = [];
- if (settings.fill.cutoutOcclusion && !skipOcclusion) {
- occlusion('fill', tempGroupFill);
- }
+ // Render Stroke for all subitems.
+ if (settings.stroke.render) {
+ const tempGroup = drawing.temp.addItem(group, `${hash}-stroke`);
+ ungroupAllGroups(tempGroup);
+ cleanupInput(tempGroup, settings);
- tempGroupFill.children.forEach((path, index) => {
- if (path.hasFill()) {
- promiseQueue.push(fill(path, hash, null, settings.fill, index));
- }
- });
+ if (settings.stroke.cutoutOcclusion && !skipOcclusion) {
+ drawing.occlusion('stroke', tempGroup);
}
- Promise.all(promiseQueue).then(resolve).catch(reject);
- });
+ // TODO: Only run stroke here if one is needed.
+ tempGroup.children.forEach(path => {
+ promiseQueue.push(drawing.trace(path, hash, null, settings.stroke));
+ });
+ }
- return content;
-};
+ // Render Fill for all subitems.
+ if (settings.fill.render) {
+ const tempGroupFill = drawing.temp.addItem(group, `${hash}-fill`);
+ ungroupAllGroups(tempGroupFill);
+ cleanupInput(tempGroupFill, settings);
+
+ if (settings.fill.cutoutOcclusion && !skipOcclusion) {
+ drawing.occlusion('fill', tempGroupFill);
+ }
+
+ tempGroupFill.children.forEach((path, index) => {
+ if (path.hasFill()) {
+ promiseQueue.push(drawing.fill(path, hash, null, settings.fill, index));
+ }
+ });
+ }
+
+ Promise.all(promiseQueue).then(resolve).catch(reject);
+});
diff --git a/src/components/core/control/cncserver.control.js b/src/components/core/control/cncserver.control.js
index da66350f..655096b8 100644
--- a/src/components/core/control/cncserver.control.js
+++ b/src/components/core/control/cncserver.control.js
@@ -2,542 +2,324 @@
* @file Abstraction module for functions that generate movement or help
* functions for calculating movement command generation for CNC Server!
*/
-const control = {}; // Exposed export.
-
-module.exports = (cncserver) => {
- /**
- * Run the operation to set the current tool (and any aggregate operations
- * required) into the buffer
- *
- * @param name
- * The machine name of the tool (as defined in the bot config file).
- * @param index
- * Index for notifying user of what the manual tool change is for.
- * @param callback
- * Triggered when the full tool change is to have been completed, or on
- * failure.
- * @param waitForCompletion
- * Pass false to call callback immediately after calculation, true to
- * callback only after physical movement is complete.
- *
- * @returns {boolean}
- * True if success, false on failure.
- */
- control.setTool = (name, index = null, callback = () => {}, waitForCompletion = false) => {
- // Get the matching tool object from the bot configuration.
- const tool = cncserver.settings.botConf.get(`tools:${name}`);
-
- // No tool found with that name? Augh! Run AWAY!
- if (!tool) {
- cncserver.run('callback', callback);
- return false;
- }
+import * as utils from 'cs/utils';
+import { bot, gConf } from 'cs/settings';
+import { state as bufferState, render } from 'cs/buffer';
+import * as pen from 'cs/pen';
+import * as actualPen from 'cs/actualPen';
+import run from 'cs/run';
+import { sendMessage } from 'cs/ipc';
+
+// Command state variables.
+export const state = {
+ commandDuration: 0,
+};
- // For wait=false/"resume" tools, we really just resume the buffer.
- // It should be noted, this is obviously NOT a queable toolchange.
- // This should ONLY be called to restart the queue after a swap.
- if (tool.wait !== undefined && tool.wait === false) {
- cncserver.buffer.resume();
- callback(1);
- return true;
+/**
+ * Triggered when the pen is requested to move across the bounds of the draw
+ * area (either in or out).
+ *
+ * @param {boolean} newValue
+ * Pass true when moving "off screen", false when moving back into bounds
+ */
+export function offCanvasChange(newValue) {
+ // Only do anything if the value is different
+ if (pen.state.offCanvas !== newValue) {
+ pen.forceState({ offCanvas: newValue });
+ if (pen.state.offCanvas) { // Pen is now off screen/out of bounds
+ if (pen.isDown()) {
+ // Don't draw stuff while out of bounds (also, don't change the
+ // current known state so we can come back to it when we return to
+ // bounds),but DO change the buffer tip height so that is reflected on
+ // actualPen if it's every copied over on buffer execution.
+ run('callback', () => {
+ pen.setHeight('up', false, true);
+ const { height } = utils.stateToHeight('up');
+ pen.forceState({ z: height, height });
+ });
+ }
+ } else { // Pen is now back in bounds
+ // Set the state regardless of actual change
+ console.log('Go back to:', pen.state.back);
+
+ // Assume starting from up state & height (ensures correct timing)
+ const newHeight = utils.stateToHeight('up').height;
+ pen.forceState({
+ state: 'up',
+ height: newHeight,
+ z: newHeight,
+ });
+ pen.setHeight(pen.state.back);
}
+ }
+}
- // Pen Up
- cncserver.pen.setHeight('up');
-
- // Move to the tool
- cncserver.control.movePenAbs(tool);
-
- // Set the tip of state pen to the tool now that the change is done.
- cncserver.pen.forceState({ tool: name });
+/**
+ * Actually move the position of the pen, called inside and outside buffer
+ * runs, figures out timing/offset based on actualPen position.
+ *
+ * @param {{x: number, y: number}} destination
+ * Absolute destination coordinate position (in steps).
+ * @param {function} callback
+ * Optional, callback for when operation should have completed.
+ * @param {number} speedOverride
+ * Percent of speed to set for this movement only.
+ */
+export function actuallyMove(destination, callback, speedOverride = null) {
+ // Get the amount of change/duration from difference between actualPen and
+ // absolute position in given destination
+ const change = pen.getPosChangeData(
+ actualPen.state,
+ destination,
+ speedOverride
+ );
+
+ state.commandDuration = Math.max(change.d, 0);
+
+ // Execute the command immediately via serial.direct.command.
+ sendMessage('serial.direct.command',
+ render({
+ command: {
+ type: 'absmove',
+ x: destination.x,
+ y: destination.y,
+ source: actualPen.state,
+ },
+ }));
+
+ // Set the correct duration and new position through to actualPen
+ actualPen.forceState({
+ lastDuration: change.d,
+ x: destination.x,
+ y: destination.y,
+ });
- // Trigger the binder event.
- cncserver.binder.trigger('tool.change', {
- ...tool,
- index,
- name,
- });
+ // If there's nothing in the buffer, reset pen to actualPen
+ if (bufferState.data.length === 0) {
+ pen.resetState();
+ }
- // Finish up.
- if (waitForCompletion) { // Run inside the buffer
- cncserver.run('callback', callback);
- } else { // Run as soon as items have been buffered
+ // Delayed callback (if used)
+ if (callback) {
+ setTimeout(() => {
callback(1);
- }
-
- return true;
- };
-
- /**
- * "Move" the pen (tip of the buffer) to an absolute point inside the maximum
- * available bot area. Includes cutoffs and sanity checks.
- *
- * @param {{x: number, y: number, [limit: string]}} inPoint
- * Absolute coordinate measured in steps to move to. src is assumed to be
- * "pen" tip of buffer. Also can contain optional "limit" key to set where
- * movement should be limited to. Defaults to none, accepts "workArea".
- * @param {function} callback
- * Callback triggered when operation should be complete.
- * @param {boolean} immediate
- * Set to true to trigger the callback immediately.
- * @param {boolean} skip
- * Set to true to skip adding to the buffer, simplifying this function
- * down to just a sanity checker.
- * @param {number} speedOverride
- * Percent of speed to set for this movement only.
- *
- * @returns {number}
- * Distance moved from previous position, in steps.
- */
- control.movePenAbs = (inPoint, callback, immediate = true, skip, speedOverride = null) => {
- // Something really bad happened here...
- if (Number.isNaN(inPoint.x) || Number.isNaN(inPoint.y)) {
- console.error('INVALID Move pen input, given:', inPoint);
- if (callback) callback(false);
- return 0;
- }
-
- // Make a local copy of point as we don't want to mess with its values ByRef
- const point = cncserver.utils.extend({}, inPoint);
-
- // Sanity check absolute position input point and round everything (as we
- // only move in whole number steps)
- point.x = Math.round(Number(point.x));
- point.y = Math.round(Number(point.y));
-
- // If moving in the workArea only, limit to allowed workArea, and trigger
- // on/off screen events when we go offscreen, retaining suggested position.
- let startOffCanvasChange = false;
- if (point.limit === 'workArea') {
- // Off the Right
- if (point.x > cncserver.settings.bot.workArea.right) {
- point.x = cncserver.settings.bot.workArea.right;
- startOffCanvasChange = true;
- }
-
- // Off the Left
- if (point.x < cncserver.settings.bot.workArea.left) {
- point.x = cncserver.settings.bot.workArea.left;
- startOffCanvasChange = true;
- }
+ }, Math.max(state.commandDuration, 0));
+ }
+}
- // Off the Top
- if (point.y < cncserver.settings.bot.workArea.top) {
- point.y = cncserver.settings.bot.workArea.top;
- startOffCanvasChange = true;
- }
+/**
+ * Actually change the height of the pen, called inside and outside buffer
+ * runs, figures out timing offset based on actualPen position.
+ *
+ * @param {integer} height
+ * Write-ready servo "height" value calculated from "state"
+ * @param {string} stateValue
+ * Optional, pass what the name of the state should be saved as in the
+ * actualPen object when complete.
+ * @param {function} cb
+ * Optional, callback for when operation should have completed.
+ */
+export function actuallyMoveHeight(height, stateValue, cb) {
+ const change = utils.getHeightChangeData(
+ actualPen.state.height,
+ height
+ );
+
+ state.commandDuration = Math.max(change.d, 0);
+
+ // Pass along the correct height position through to actualPen.
+ if (typeof stateValue !== 'undefined') {
+ actualPen.forceState({ state: stateValue });
+ }
+
+ // Execute the command immediately via serial.direct.command.
+ sendMessage('serial.direct.command',
+ render({
+ command: {
+ type: 'absheight',
+ z: height,
+ source: actualPen.state.height,
+ },
+ }));
+
+ actualPen.forceState({
+ z: height,
+ height,
+ lastDuration: change.d,
+ });
- // Off the Bottom
- if (point.y > cncserver.settings.bot.workArea.bottom) {
- point.y = cncserver.settings.bot.workArea.bottom;
- startOffCanvasChange = true;
- }
+ // Delayed callback (if used)
+ if (cb) {
+ setTimeout(() => {
+ cb(1);
+ }, Math.max(state.commandDuration, 0));
+ }
+}
- // Are we beyond our workarea limits?
- if (startOffCanvasChange) { // Yep.
- // We MUST trigger the start offscreen change AFTER the movement to draw
- // up to that point (which happens later).
- startOffCanvasChange = true;
- } else { // Nope!
- // The off canvas STOP trigger must happen BEFORE the move happens
- // (which is fine right here)
- cncserver.control.offCanvasChange(false);
- }
+/**
+ * "Move" the pen (tip of the buffer) to an absolute point inside the maximum
+ * available bot area. Includes cutoffs and sanity checks.
+ *
+ * @param {{x: number, y: number, [limit: string]}} inPoint
+ * Absolute coordinate measured in steps to move to. src is assumed to be
+ * "pen" tip of buffer. Also can contain optional "limit" key to set where
+ * movement should be limited to. Defaults to none, accepts "workArea".
+ * @param {function} callback
+ * Callback triggered when operation should be complete.
+ * @param {boolean} immediate
+ * Set to true to trigger the callback immediately.
+ * @param {boolean} skip
+ * Set to true to skip adding to the buffer, simplifying this function
+ * down to just a sanity checker.
+ * @param {number} speedOverride
+ * Percent of speed to set for this movement only.
+ *
+ * @returns {number}
+ * Distance moved from previous position, in steps.
+ */
+export function movePenAbs(
+ inPoint, callback, immediate = true, skip, speedOverride = null
+) {
+ // Something really bad happened here...
+ if (Number.isNaN(inPoint.x) || Number.isNaN(inPoint.y)) {
+ console.error('INVALID Move pen input, given:', inPoint);
+ if (callback) callback(false);
+ return 0;
+ }
+
+ // Make a local copy of point as we don't want to mess with its values ByRef
+ const point = utils.extend({}, inPoint);
+
+ // Sanity check absolute position input point and round everything (as we
+ // only move in whole number steps)
+ point.x = Math.round(Number(point.x));
+ point.y = Math.round(Number(point.y));
+
+ // If moving in the workArea only, limit to allowed workArea, and trigger
+ // on/off screen events when we go offscreen, retaining suggested position.
+ let startOffCanvasChange = false;
+ if (point.limit === 'workArea') {
+ // Off the Right
+ if (point.x > bot.workArea.right) {
+ point.x = bot.workArea.right;
+ startOffCanvasChange = true;
}
- // Ensure values don't go off the rails
- cncserver.utils.sanityCheckAbsoluteCoord(point);
-
- // If we're skipping the buffer, just move to the point
- // Pen stays put as last point set in buffer
- if (skip) {
- console.log('Skipping buffer for:', point);
- cncserver.control.actuallyMove(point, callback, speedOverride);
- return 0; // Don't return any distance for buffer skipped movements
+ // Off the Left
+ if (point.x < bot.workArea.left) {
+ point.x = bot.workArea.left;
+ startOffCanvasChange = true;
}
- // Calculate change from end of buffer pen position
- const source = { x: cncserver.pen.state.x, y: cncserver.pen.state.y };
- const change = {
- x: Math.round(point.x - cncserver.pen.state.x),
- y: Math.round(point.y - cncserver.pen.state.y),
- };
-
- // Don't do anything if there's no change
- if (change.x === 0 && change.y === 0) {
- if (callback) callback(cncserver.pen.state);
- return 0;
+ // Off the Top
+ if (point.y < bot.workArea.top) {
+ point.y = bot.workArea.top;
+ startOffCanvasChange = true;
}
- /*
- Duration/distance is only calculated as relative from last assumed point,
- which may not actually ever happen, though it is likely to happen.
- Buffered items may not be pushed out of order, but previous location may
- have changed as user might pause the buffer, and move the actualPen
- position.
- @see executeNext - for more details on how this is handled.
- */
- const distance = cncserver.utils.getVectorLength(change);
- const duration = cncserver.utils.getDurationFromDistance(distance, 1, null, speedOverride);
-
- // Only if we actually moved anywhere should we queue a movement
- if (distance !== 0) {
- // Set the tip of buffer pen at new position
- cncserver.pen.forceState({
- x: point.x,
- y: point.y,
- });
-
- // Adjust the distance counter based on movement amount, not if we're off
- // the canvas though.
- if (cncserver.utils.penDown()
- && !cncserver.pen.state.offCanvas
- && cncserver.settings.bot.inWorkArea(point)) {
- cncserver.pen.forceState({
- distanceCounter: parseFloat(
- Number(distance) + Number(cncserver.pen.state.distanceCounter)
- ),
- });
- }
-
- // Queue the final absolute move (serial command generated later)
- cncserver.run(
- 'move',
- {
- x: cncserver.pen.state.x,
- y: cncserver.pen.state.y,
- source,
- },
- duration
- );
+ // Off the Bottom
+ if (point.y > bot.workArea.bottom) {
+ point.y = bot.workArea.bottom;
+ startOffCanvasChange = true;
}
- // Required start offCanvas change -after- movement has been queued
- if (startOffCanvasChange) {
- cncserver.control.offCanvasChange(true);
+ // Are we beyond our workarea limits?
+ if (startOffCanvasChange) { // Yep.
+ // We MUST trigger the start offscreen change AFTER the movement to draw
+ // up to that point (which happens later).
+ startOffCanvasChange = true;
+ } else { // Nope!
+ // The off canvas STOP trigger must happen BEFORE the move happens
+ // (which is fine right here)
+ offCanvasChange(false);
}
-
- if (callback) {
- if (immediate === true) {
- callback(cncserver.pen.state);
- } else {
- // Set the timeout to occur sooner so the next command will execute
- // before the other is actually complete. This will push into the buffer
- // and allow for far smoother move runs.
-
- const latency = cncserver.settings.gConf.get('bufferLatencyOffset');
- const cmdDuration = Math.max(duration - latency, 0);
-
- if (cmdDuration < 2) {
- callback(cncserver.pen.state);
- } else {
- setTimeout(() => {
- callback(cncserver.pen.state);
- }, cmdDuration);
- }
- }
- }
-
- return distance;
+ }
+
+ // Ensure values don't go off the rails
+ utils.sanityCheckAbsoluteCoord(point);
+
+ // If we're skipping the buffer, just move to the point
+ // Pen stays put as last point set in buffer
+ if (skip) {
+ console.log('Skipping buffer for:', point);
+ actuallyMove(point, callback, speedOverride);
+ return 0; // Don't return any distance for buffer skipped movements
+ }
+
+ // Calculate change from end of buffer pen position
+ const source = { x: pen.state.x, y: pen.state.y };
+ const change = {
+ x: Math.round(point.x - pen.state.x),
+ y: Math.round(point.y - pen.state.y),
};
- /**
- * Triggered when the pen is requested to move across the bounds of the draw
- * area (either in or out).
- *
- * @param {boolean} newValue
- * Pass true when moving "off screen", false when moving back into bounds
- */
- control.offCanvasChange = (newValue) => {
- // Only do anything if the value is different
- if (cncserver.pen.state.offCanvas !== newValue) {
- cncserver.pen.forceState({ offCanvas: newValue });
- if (cncserver.pen.state.offCanvas) { // Pen is now off screen/out of bounds
- if (cncserver.utils.penDown()) {
- // Don't draw stuff while out of bounds (also, don't change the
- // current known state so we can come back to it when we return to
- // bounds),but DO change the buffer tip height so that is reflected on
- // actualPen if it's every copied over on buffer execution.
- cncserver.run('callback', () => {
- cncserver.pen.setHeight('up', false, true);
- const { height } = cncserver.utils.stateToHeight('up');
- cncserver.pen.forceState({ height });
- });
- }
- } else { // Pen is now back in bounds
- // Set the state regardless of actual change
- const { state: back } = cncserver.pen.state;
- console.log('Go back to:', back);
-
- // Assume starting from up state & height (ensures correct timing)
- cncserver.pen.forceState({
- state: 'up',
- height: cncserver.utils.stateToHeight('up').height,
- });
- cncserver.pen.setHeight(back);
- }
+ // Don't do anything if there's no change
+ if (change.x === 0 && change.y === 0) {
+ if (callback) callback(pen.state);
+ return 0;
+ }
+
+ /*
+ Duration/distance is only calculated as relative from last assumed point,
+ which may not actually ever happen, though it is likely to happen.
+ Buffered items may not be pushed out of order, but previous location may
+ have changed as user might pause the buffer, and move the actualPen
+ position.
+ @see executeNext - for more details on how this is handled.
+ */
+ const distance = utils.getVectorLength(change);
+ const duration = pen.getDurationFromDistance(distance, 1, null, speedOverride);
+
+ // Only if we actually moved anywhere should we queue a movement
+ if (distance !== 0) {
+ // Set the tip of buffer pen at new position
+ // TODO: Figure out a better way to do all of this.
+ pen.forceState({
+ x: point.x,
+ y: point.y,
+ }, true);
+
+ // Adjust the distance counter based on movement amount, not if we're off
+ // the canvas though.
+ if (pen.isDown()
+ && !pen.state.offCanvas
+ && bot.inWorkArea(point)) {
+ pen.forceState({
+ distanceCounter: parseFloat(
+ Number(distance) + Number(pen.state.distanceCounter)
+ ),
+ });
}
- };
- /**
- * Actually move the position of the pen, called inside and outside buffer
- * runs, figures out timing/offset based on actualPen position.
- *
- * @param {{x: number, y: number}} destination
- * Absolute destination coordinate position (in steps).
- * @param {function} callback
- * Optional, callback for when operation should have completed.
- * @param {number} speedOverride
- * Percent of speed to set for this movement only.
- */
- control.actuallyMove = (destination, callback, speedOverride = null) => {
- // Get the amount of change/duration from difference between actualPen and
- // absolute position in given destination
- const change = cncserver.utils.getPosChangeData(
- cncserver.actualPen.state,
- destination,
- speedOverride
- );
-
- control.commandDuration = Math.max(change.d, 0);
-
- // Execute the command immediately via serial.direct.command.
- cncserver.ipc.sendMessage('serial.direct.command',
- cncserver.buffer.render({
- command: {
- type: 'absmove',
- x: destination.x,
- y: destination.y,
- source: cncserver.actualPen.state,
- },
- }));
-
- // Set the correct duration and new position through to actualPen
- cncserver.actualPen.forceState({
- lastDuration: change.d,
- x: destination.x,
- y: destination.y,
- });
-
- // If there's nothing in the buffer, reset pen to actualPen
- if (cncserver.buffer.data.length === 0) {
- cncserver.pen.resetState();
- }
+ // Queue the final absolute move (serial command generated later)
+ run('move', { x: pen.state.x, y: pen.state.y, source }, duration);
+ }
- // Trigger an update for pen position
- cncserver.sockets.sendPenUpdate();
+ // Required start offCanvas change -after- movement has been queued
+ if (startOffCanvasChange) {
+ offCanvasChange(true);
+ }
- // Delayed callback (if used)
- if (callback) {
- setTimeout(() => {
- callback(1);
- }, Math.max(cncserver.control.commandDuration, 0));
- }
- };
-
- /**
- * Actually change the height of the pen, called inside and outside buffer
- * runs, figures out timing offset based on actualPen position.
- *
- * @param {integer} height
- * Write-ready servo "height" value calculated from "state"
- * @param {string} stateValue
- * Optional, pass what the name of the state should be saved as in the
- * actualPen object when complete.
- * @param {function} cb
- * Optional, callback for when operation should have completed.
- */
- control.actuallyMoveHeight = (height, stateValue, cb) => {
- const change = cncserver.utils.getHeightChangeData(
- cncserver.actualPen.state.height,
- height
- );
-
- control.commandDuration = Math.max(change.d, 0);
-
- // Pass along the correct height position through to actualPen.
- if (typeof stateValue !== 'undefined') {
- cncserver.actualPen.forceState({ state: stateValue });
- }
-
- // Execute the command immediately via serial.direct.command.
- cncserver.ipc.sendMessage('serial.direct.command',
- cncserver.buffer.render({
- command: {
- type: 'absheight',
- z: height,
- source: cncserver.actualPen.state.height,
- },
- }));
-
- cncserver.actualPen.forceState({
- height,
- lastDuration: change.d,
- });
-
- // Trigger an update for pen position.
- cncserver.sockets.sendPenUpdate();
-
- // Delayed callback (if used)
- if (cb) {
- setTimeout(() => {
- cb(1);
- }, Math.max(cncserver.control.commandDuration, 0));
- }
- };
+ if (callback) {
+ if (immediate === true) {
+ callback(pen.state);
+ } else {
+ // Set the timeout to occur sooner so the next command will execute
+ // before the other is actually complete. This will push into the buffer
+ // and allow for far smoother move runs.
+ const latency = gConf.get('bufferLatencyOffset');
+ const cmdDuration = Math.max(duration - latency, 0);
- /**
- * Actually render Paper paths into movements
- *
- * @param {object} source
- * Source paper object containing the children, defaults to preview layer.
- */
- control.renderPathsToMoves = (source = cncserver.drawing.base.layers.preview, reqSettings = {}) => {
- const { settings: { botConf }, drawing: { colors } } = cncserver;
- const settings = {
- parkAfter: true,
- ...reqSettings,
- };
- // TODO:
- // * Join extant non-closed paths with endpoint distances < 0.5mm
- // * Split work by colors
- // * Allow WCB bot support to inject tool changes for refill support
- // * Order paths by pickup/dropoff distance
-
- // Store work for all paths grouped by color
- const workGroups = colors.getWorkGroups();
- const validColors = Object.keys(workGroups);
- const allPaths = cncserver.drawing.base.getPaths(source);
- allPaths.forEach((path) => {
- const colorID = cncserver.drawing.base.getColorID(path);
-
- if (path.length && colorID && validColors.includes(colorID)) {
- workGroups[colorID].push(path);
- // Allow implementing triggers to modify current list of paths.
- workGroups[colorID] = cncserver.binder.trigger('control.render.path.select', workGroups[colorID]);
- } else if (colorID !== 'ignore') {
- console.log(`DEBUG: Invalid Draw path ${colorID} ${path.name} ==================`);
- }
- });
-
- let workGroupIndex = 0;
- function nextWorkGroup() {
- const colorID = validColors[workGroupIndex];
- if (colorID) {
- const paths = workGroups[colorID];
-
- if (paths.length) {
- // Bind trigger for implementors on work group begin.
- cncserver.binder.trigger('control.render.group.begin', colorID);
-
- // Do we have a tool for this colorID? If not, use manualswap.
- if (colors.doColorParsing()) {
- const changeTool = botConf.get(`tools:${colorID}`) ? colorID : 'manualswap';
- control.setTool(changeTool, colorID);
- }
-
- let pathIndex = 0;
- const nextPath = () => {
- if (paths[pathIndex]) {
- control.accelMoveOnPath(paths[pathIndex]).then(() => {
- // Trigger implementors for plath render complete.
- cncserver.binder.trigger('control.render.path.finish', paths[pathIndex]);
-
- // Path in this group done, move to the next.
- pathIndex++;
- nextPath();
- }).catch((error) => {
- // If we have an error object, it's an actual error!
- if (error) {
- console.error(error);
- }
- // Otherwise, path generation has been entirely cancelled.
- workGroupIndex = validColors.length;
- });
- } else {
- // No more paths in this group, move to the next.
- workGroupIndex++;
- nextWorkGroup();
- }
- };
-
- // Start processing paths in the initial workgroup.
- nextPath();
- } else {
- // There is no work for this group, move to the next one.
- workGroupIndex++;
- nextWorkGroup();
- }
+ if (cmdDuration < 2) {
+ callback(pen.state);
} else {
- // Actually complete with all paths in all work groups!
- // TODO: Fullfull a promise for the function?
-
- // Trigger binding for implementors on sucessfull rendering completion.
- cncserver.binder.trigger('control.render.finish');
-
- // If settings say to park.
- if (settings.parkAfter) cncserver.pen.park();
+ setTimeout(() => {
+ callback(pen.state);
+ }, cmdDuration);
}
}
- // Intitialize working on the first group on the next process tick.
- process.nextTick(nextWorkGroup);
- };
-
- control.accelMoveOnPath = path => new Promise((success, error) => {
- const move = (point, speed = null) => {
- const stepPoint = cncserver.utils.absToSteps(point, 'mm', true);
- control.movePenAbs(stepPoint, null, true, null, speed);
- };
-
- // Pen up
- cncserver.pen.setPen({ state: 'up' });
-
- // Move to start of path, then pen down.
- move(path.getPointAt(0));
- cncserver.pen.setPen({ state: 'draw' });
-
- // Calculate groups of accell points and run them into moves.
- cncserver.drawing.accell.getPoints(path, (accellPoints) => {
- // If we have data, move to those points.
- if (accellPoints && accellPoints.length) {
- // Move through all accell points from start to nearest end point
- accellPoints.forEach((pos) => {
- move(pos.point, pos.speed);
- });
- } else {
- // Null means generation of accell points was cancelled.
- if (accellPoints !== null) {
- // No points? We're done. Wrap up the line.
-
- // Move to end of path...
- move(path.getPointAt(path.length));
-
- // If it's a closed path, overshoot back home.
- if (path.closed) {
- move(path.getPointAt(0));
- }
-
- // End with pen up.
- cncserver.pen.setPen({ state: 'up' });
+ }
- // Fulfull the promise for this subpath.
- success();
- return;
- }
-
- // If we're here, path generation was cancelled, bubble it.
- error();
- }
- });
- });
-
-
- // Exports...
- control.exports = {
- setTool: control.setTool,
- movePenAbs: control.movePenAbs,
- };
-
- return control;
-};
+ return distance;
+}
diff --git a/src/components/core/control/cncserver.pen.base.js b/src/components/core/control/cncserver.pen.base.js
new file mode 100644
index 00000000..742b1487
--- /dev/null
+++ b/src/components/core/control/cncserver.pen.base.js
@@ -0,0 +1,435 @@
+/**
+ * @file Abstraction module for pen state and setter/helper methods.
+ */
+import { gConf, bot, botConf } from 'cs/settings';
+import * as utils from 'cs/utils';
+import { connect, localTrigger } from 'cs/serial';
+import { bindTo, trigger } from 'cs/binder';
+import { movePenAbs, actuallyMoveHeight } from 'cs/control';
+import { cmdstr } from 'cs/buffer';
+import run from 'cs/run';
+import * as actualPen from 'cs/actualPen';
+
+// The pen state: this holds the state of the pen at the "latest tip" of the buffer,
+// meaning that as soon as an instruction intended to be run in the buffer is
+// received, this is updated to reflect the intention of the buffered item.
+export const state = {
+ x: null, // XY set by bot defined park position (assumed initial location)
+ y: null,
+ z: null,
+ state: 0, // Pen state is from 0 (up/off) to 1 (down/on)
+ height: 0, // Last set pen height in output servo value
+ power: 0, // Pen power (only used in special circumstances)
+ tool: null, // Current tool ID string.
+ colorsetItem: null, // Current colorset item ID.
+ implement: null, // Inherit current colorset implement by default.
+ bufferHash: null, // Holds the last pen buffer hash.
+ offCanvas: false, // Whether the current position is beyond the edges.
+ lastDuration: 0, // Holds the last movement timing in milliseconds
+ distanceCounter: 0, // Holds a running tally of distance travelled
+ simulation: 0, // Fake everything and act like it's working, no serial
+};
+
+/**
+ * General logic sorting function for most "pen" requests.
+ *
+ * @param {object} inPenState
+ * Raw object containing data from /v1/pen PUT requests. See API spec for
+ * pen to get an idea of what can live in this object.
+ * @param {function} callback
+ * Callback triggered when intended action should be complete.
+ * @param {number} speedOverride
+ * Percent of speed to set for this movement only.
+ */
+export function setPen(inPenState, callback = () => { }, speedOverride = null) {
+ const debug = gConf.get('debug');
+
+ // What can happen here?
+ // We're changing state, and what we need is the ability to find all the
+ // passed, changed state from either the actual pen state, OR the tip of
+ // our last "buffered" movement.
+ //
+ // We can set:
+ // - Power
+ // - X/Y (with speed)
+ // - Height/pen "state"/Z
+ // - X/Y/Z (must complete together)
+ // - Simulation on/off
+
+ // Force the distanceCounter to be a number (was coming up as null)
+ state.distanceCounter = parseFloat(state.distanceCounter);
+
+ // Counter Reset
+ if (inPenState.resetCounter) {
+ state.distanceCounter = Number(0);
+ callback(true);
+ return;
+ }
+
+ // Setting the value of the power to the pen
+ if (typeof inPenState.power !== 'undefined') {
+ setPower(inPenState.power, callback);
+ return;
+ }
+
+ // Setting the value of simulation
+ if (typeof inPenState.simulation !== 'undefined') {
+ // No change
+ if (inPenState.simulation === state.simulation) {
+ callback(true);
+ return;
+ }
+
+ if (inPenState.simulation === '0') { // Attempt to connect to serial
+ connect({ complete: callback });
+ } else { // Turn off serial!
+ // TODO: Actually nullify connection.. no use case worth it yet
+ localTrigger('simulationStart');
+ }
+
+ return;
+ }
+
+ // State/z position has been passed
+ if (typeof inPenState.state !== 'undefined') {
+ // Disallow actual pen setting when off canvas (unless skipping buffer)
+ if (!state.offCanvas || inPenState.skipBuffer) {
+ setHeight(inPenState.state, callback, inPenState.skipBuffer);
+ } else {
+ // Save the state anyways so we can come back to it
+ state.state = inPenState.state;
+ if (callback) callback(1);
+ }
+ return;
+ }
+
+ // Absolute positions are set
+ if (inPenState.x !== undefined) {
+ // Input values are given as percentages of working area (not max area)
+ const point = {
+ abs: inPenState.abs,
+ x: Number(inPenState.x),
+ y: Number(inPenState.y),
+ };
+
+ // Don't accept bad input
+ const penNaN = Number.isNaN(point.x) || Number.isNaN(point.y);
+ const penFinite = Number.isFinite(point.x) && Number.isFinite(point.y);
+ if (penNaN || !penFinite) {
+ if (debug) {
+ console.log('setPen: Either X/Y not valid numbers.');
+ }
+ callback(false);
+ return;
+ }
+
+ // Override this to move with accelleration if override isn't set.
+ // TODO: Allow switching between accell and flat speed movements.
+ /* if (speedOverride === null) {
+ // Convert the percentage or absolute in/mm XY values into absolute steps.
+ const startPoint = cncserver.utils.stepsToAbs(pen.state, 'mm');
+ const endPoint = utils.stepsToAbs(cncserver.utils.inPenToSteps(point), 'mm');
+
+ const movePath = new Path([
+ startPoint,
+ endPoint,
+ ]);
+ // cncserver.sockets.sendPaperUpdate();
+
+ const accellPoints = cncserver.drawing.accell.getPointsSync(movePath);
+ accellPoints.forEach((pos) => {
+ pen.setPen({ ...pos.point, abs: 'mm' }, null, pos.speed);
+ });
+
+ pen.setPen({ ...endPoint, abs: 'mm' }, null, 0);
+ callback(true);
+
+ // movePath.remove(); // TODO: Move this to when it's done?
+ return;
+ } */
+
+ // Convert the percentage or absolute in/mm XY values into absolute steps.
+ const absInput = utils.inPenToSteps(point);
+ absInput.limit = 'workArea';
+
+ // Are we parking?
+ if (inPenState.park) {
+ // Don't repark if already parked (but not if we're skipping the buffer)
+ const parkPos = utils.centToSteps(bot.park, true);
+ if (
+ state.x === parkPos.x
+ && state.y === parkPos.y
+ && !inPenState.skipBuffer
+ ) {
+ if (debug) {
+ console.log('setPen: Can\'t park when already parked.');
+ }
+ if (callback) callback(false);
+ return;
+ }
+
+ // Set Absolute input value to park position in steps
+ absInput.x = parkPos.x;
+ absInput.y = parkPos.y;
+ absInput.limit = 'maxArea';
+ }
+
+ movePenAbs(
+ absInput,
+ callback,
+ inPenState.waitForCompletion,
+ inPenState.skipBuffer,
+ speedOverride
+ );
+
+ return;
+ }
+
+ if (callback) callback(state);
+}
+
+/**
+ * Set the "power" option
+ *
+ * @param {number} power
+ * Value from 0 to 100 to send to the bot.
+ * @param callback
+ * Callback triggered when operation should be complete.
+ * @param skipBuffer
+ * Set to true to skip adding the command to the buffer and run it
+ * immediately.
+ */
+export function setPower(power, callback, skipBuffer) {
+ const powers = botConf.get('penpower') || { min: 0, max: 0 };
+
+ run(
+ 'custom',
+ cmdstr(
+ 'penpower',
+ { p: Math.round(power * powers.max) + Number(powers.min) }
+ )
+ );
+
+ state.power = power;
+ if (callback) callback(true);
+}
+
+/**
+ * Run a servo position from a given percentage or named height value into
+ * the buffer, or directly via skipBuffer.
+ *
+ * @param {number|string} inState
+ * Named height preset machine name, or float between 0 & 1.
+ * @param callback
+ * Callback triggered when operation should be complete.
+ * @param skipBuffer
+ * Set to true to skip adding the command to the buffer and run it
+ * immediately.
+ */
+export function setHeight(inState, callback, skipBuffer) {
+ let servoDuration = botConf.get('servo:minduration');
+
+ // Convert the incoming state
+ const conv = utils.stateToHeight(inState);
+ const { height = 0, state: stateValue = null } = conv;
+
+ // If we're skipping the buffer, just set the height directly
+ if (skipBuffer) {
+ console.log('Skipping buffer to set height:', height);
+ actuallyMoveHeight(height, stateValue, callback);
+ return;
+ }
+
+ const sourceHeight = state.height;
+
+ // Pro-rate the duration depending on amount of change to tip of buffer.
+ // TODO: Replace with cncserver.utils.getHeightChangeData()
+ if (state.height) {
+ const servo = botConf.get('servo');
+ const range = parseInt(servo.max, 10) - parseInt(servo.min, 10);
+ servoDuration = Math.round(
+ (Math.abs(height - state.height) / range) * servoDuration
+ ) + 1;
+ }
+
+ // Actually set tip of buffer to given sanitized state & servo height.
+ state.height = height;
+ state.z = height;
+ state.state = stateValue;
+
+ // Run the height into the command buffer
+ run('height', { z: height, source: sourceHeight }, servoDuration);
+
+ // Height movement callback servo movement duration offset
+ const delay = servoDuration - gConf.get('bufferLatencyOffset');
+ if (callback) {
+ setTimeout(() => {
+ callback(1);
+ }, Math.max(delay, 0));
+ }
+}
+
+/**
+ * Reset state of pen to current head (actualPen).
+ */
+export function resetState() {
+ utils.applyObjectTo(actualPen.state, state);
+ trigger('pen.update', state);
+}
+
+/**
+ * Park the pen.
+ *
+ * @param {boolean} direct
+ * True to send direct and skip the buffer.
+ * @param {function} callback
+ * Ya know.
+ */
+export function park(direct = false, callback = () => { }) {
+ setHeight('up', () => {
+ setPen({
+ x: bot.park.x,
+ y: bot.park.y,
+ park: true,
+ skipBuffer: direct,
+ }, callback);
+ }, direct);
+}
+
+/**
+ * Force the values of a given set of keys within the pen state.
+ *
+ * @param {object} inState
+ * Flat object of key/value pairs to FORCE into the pen state. Only used to
+ * correct head state or to update position along the buffer.
+ * @param {bool} skipUpdate
+ * If true, no update will be sent.
+ */
+export function forceState(inState, skipUpdate = false) {
+ // Only trigger update if not skipping.
+ utils.applyObjectTo(inState, state, true);
+ if (!skipUpdate) {
+ trigger('pen.update', state);
+ }
+}
+
+/**
+ * Set the internal state hash value.
+ *
+ * @param {string} hash
+ * Buffer hash to set.
+ */
+export function setHash(hash) {
+ state.bufferHash = hash;
+}
+
+// Setup initial park position and set default.
+bindTo('controller.setup', 'pen', () => {
+ // Set initial pen position at park position
+ forceState(utils.centToSteps(bot.park, true));
+
+ // Trigger pen.setup with full state.
+ trigger('pen.setup', state, true);
+});
+
+/**
+ * Helper abstraction for checking if the tip of buffer pen is "down" or not.
+ *
+ * @param {object} inPen
+ * The pen state object to check for down status, defaults to buffer tip.
+ * @returns {Boolean}
+ * False if pen is considered up, true if pen is considered down.
+ */
+export function isDown(inPen) {
+ const checkPen = inPen?.state || state;
+ if (checkPen.state === 'up' || checkPen.state < 0.5) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Calculate the duration for a pen movement from the distance.
+ * Takes into account whether pen is up or down
+ *
+ * @param {float} distance
+ * Distance in steps that we'll be moving
+ * @param {int} min
+ * Optional minimum value for output duration, defaults to 1.
+ * @param {object} inPen
+ * Incoming pen object to check (buffer tip or bot current).
+ * @param {number} speedOverride
+ * Optional speed override, overrides calculated speed percent.
+ *
+ * @returns {number}
+ * Millisecond duration of how long the move should take
+ */
+export function getDurationFromDistance(distance, min = 1, inPen, speedOverride = null) {
+ const minSpeed = parseFloat(botConf.get('speed:min'));
+ const maxSpeed = parseFloat(botConf.get('speed:max'));
+ const drawingSpeed = botConf.get('speed:drawing');
+ const movingSpeed = botConf.get('speed:moving');
+
+ // Use given speed over distance to calculate duration
+ let speed = (isDown(inPen)) ? drawingSpeed : movingSpeed;
+ if (speedOverride != null) {
+ speed = speedOverride;
+ }
+
+ speed = parseFloat(speed) / 100;
+
+ // Convert to steps from percentage
+ speed = (speed * (maxSpeed - minSpeed)) + minSpeed;
+
+ // Sanity check speed value
+ speed = speed > maxSpeed ? maxSpeed : speed;
+ speed = speed < minSpeed ? minSpeed : speed;
+
+ // How many steps a second?
+ return Math.max(Math.abs(Math.round(distance / speed * 1000)), min);
+}
+
+/**
+ * Given two points, find the difference and duration at current speed
+ *
+ * @param {{x: number, y: number}} src
+ * Source position coordinate (in steps).
+ * @param {{x: number, y: number}} dest
+ * Destination position coordinate (in steps).
+ * @param {number} speed
+ * Speed override for this movement in percent.
+ *
+ * @returns {{d: number, x: number, y: number}}
+ * Object containing the change amount in steps for x & y, along with the
+ * duration in milliseconds.
+ */
+export function getPosChangeData(src, dest, speed = null) {
+ let change = {
+ x: Math.round(dest.x - src.x),
+ y: Math.round(dest.y - src.y),
+ };
+
+ // Calculate distance
+ const duration = getDurationFromDistance(utils.getVectorLength(change), 1, src, speed);
+
+ // Adjust change direction/inversion
+ if (botConf.get('controller').position === 'relative') {
+ // Invert X or Y to match stepper direction
+ change.x = gConf.get('invertAxis:x') ? change.x * -1 : change.x;
+ change.y = gConf.get('invertAxis:y') ? change.y * -1 : change.y;
+ } else { // Absolute! Just use the "new" absolute X & Y locations
+ change.x = state.x;
+ change.y = state.y;
+ }
+
+ // Swap motor positions
+ if (gConf.get('swapMotors')) {
+ change = {
+ x: change.y,
+ y: change.x,
+ };
+ }
+
+ return { d: duration, x: change.x, y: change.y };
+}
diff --git a/src/components/core/control/cncserver.pen.js b/src/components/core/control/cncserver.pen.js
index d65a1773..742b1487 100644
--- a/src/components/core/control/cncserver.pen.js
+++ b/src/components/core/control/cncserver.pen.js
@@ -1,328 +1,435 @@
/**
* @file Abstraction module for pen state and setter/helper methods.
*/
-const { Path } = require('paper');
-
-const pen = {}; // Exposed export.
-
-module.exports = (cncserver) => {
- // The pen state: this holds the state of the pen at the "latest tip" of the buffer,
- // meaning that as soon as an instruction intended to be run in the buffer is
- // received, this is updated to reflect the intention of the buffered item.
- pen.state = {
- x: null, // XY set by bot defined park position (assumed initial location)
- y: null,
- z: null,
- state: 0, // Pen state is from 0 (up/off) to 1 (down/on)
- height: 0, // Last set pen height in output servo value
- power: 0,
- busy: false,
- tool: 'color0', // TODO: This seems wrong and assuming.
- offCanvas: false,
- bufferHash: '', // Holds the last pen buffer hash.
- lastDuration: 0, // Holds the last movement timing in milliseconds
- distanceCounter: 0, // Holds a running tally of distance travelled
- simulation: 0, // Fake everything and act like it's working, no serial
- };
+import { gConf, bot, botConf } from 'cs/settings';
+import * as utils from 'cs/utils';
+import { connect, localTrigger } from 'cs/serial';
+import { bindTo, trigger } from 'cs/binder';
+import { movePenAbs, actuallyMoveHeight } from 'cs/control';
+import { cmdstr } from 'cs/buffer';
+import run from 'cs/run';
+import * as actualPen from 'cs/actualPen';
+
+// The pen state: this holds the state of the pen at the "latest tip" of the buffer,
+// meaning that as soon as an instruction intended to be run in the buffer is
+// received, this is updated to reflect the intention of the buffered item.
+export const state = {
+ x: null, // XY set by bot defined park position (assumed initial location)
+ y: null,
+ z: null,
+ state: 0, // Pen state is from 0 (up/off) to 1 (down/on)
+ height: 0, // Last set pen height in output servo value
+ power: 0, // Pen power (only used in special circumstances)
+ tool: null, // Current tool ID string.
+ colorsetItem: null, // Current colorset item ID.
+ implement: null, // Inherit current colorset implement by default.
+ bufferHash: null, // Holds the last pen buffer hash.
+ offCanvas: false, // Whether the current position is beyond the edges.
+ lastDuration: 0, // Holds the last movement timing in milliseconds
+ distanceCounter: 0, // Holds a running tally of distance travelled
+ simulation: 0, // Fake everything and act like it's working, no serial
+};
- const debug = cncserver.settings.gConf.get('debug');
-
- /**
- * General logic sorting function for most "pen" requests.
- *
- * @param {object} inPenState
- * Raw object containing data from /v1/pen PUT requests. See API spec for
- * pen to get an idea of what can live in this object.
- * @param {function} callback
- * Callback triggered when intended action should be complete.
- * @param {number} speedOverride
- * Percent of speed to set for this movement only.
- */
- pen.setPen = (inPenState, callback = () => {}, speedOverride = null) => {
- // What can happen here?
- // We're changing state, and what we need is the ability to find all the
- // passed, changed state from either the actual pen state, OR the tip of
- // our last "buffered" movement.
- //
- // We can set:
- // - Power
- // - X/Y (with speed)
- // - Height/pen "state"/Z
- // - X/Y/Z (must complete together)
- // - Simulation on/off
-
- // Force the distanceCounter to be a number (was coming up as null)
- pen.state.distanceCounter = parseFloat(pen.state.distanceCounter);
-
- // Counter Reset
- if (inPenState.resetCounter) {
- pen.state.distanceCounter = Number(0);
+/**
+ * General logic sorting function for most "pen" requests.
+ *
+ * @param {object} inPenState
+ * Raw object containing data from /v1/pen PUT requests. See API spec for
+ * pen to get an idea of what can live in this object.
+ * @param {function} callback
+ * Callback triggered when intended action should be complete.
+ * @param {number} speedOverride
+ * Percent of speed to set for this movement only.
+ */
+export function setPen(inPenState, callback = () => { }, speedOverride = null) {
+ const debug = gConf.get('debug');
+
+ // What can happen here?
+ // We're changing state, and what we need is the ability to find all the
+ // passed, changed state from either the actual pen state, OR the tip of
+ // our last "buffered" movement.
+ //
+ // We can set:
+ // - Power
+ // - X/Y (with speed)
+ // - Height/pen "state"/Z
+ // - X/Y/Z (must complete together)
+ // - Simulation on/off
+
+ // Force the distanceCounter to be a number (was coming up as null)
+ state.distanceCounter = parseFloat(state.distanceCounter);
+
+ // Counter Reset
+ if (inPenState.resetCounter) {
+ state.distanceCounter = Number(0);
+ callback(true);
+ return;
+ }
+
+ // Setting the value of the power to the pen
+ if (typeof inPenState.power !== 'undefined') {
+ setPower(inPenState.power, callback);
+ return;
+ }
+
+ // Setting the value of simulation
+ if (typeof inPenState.simulation !== 'undefined') {
+ // No change
+ if (inPenState.simulation === state.simulation) {
callback(true);
return;
}
- // Setting the value of the power to the pen
- if (typeof inPenState.power !== 'undefined') {
- pen.setPower(inPenState.power, callback);
- return;
+ if (inPenState.simulation === '0') { // Attempt to connect to serial
+ connect({ complete: callback });
+ } else { // Turn off serial!
+ // TODO: Actually nullify connection.. no use case worth it yet
+ localTrigger('simulationStart');
}
- // Setting the value of simulation
- if (typeof inPenState.simulation !== 'undefined') {
- // No change
- if (inPenState.simulation === pen.state.simulation) {
- callback(true);
- return;
- }
-
- if (inPenState.simulation === '0') { // Attempt to connect to serial
- cncserver.serial.connect({ complete: callback });
- } else { // Turn off serial!
- // TODO: Actually nullify connection.. no use case worth it yet
- cncserver.serial.localTrigger('simulationStart');
+ return;
+ }
+
+ // State/z position has been passed
+ if (typeof inPenState.state !== 'undefined') {
+ // Disallow actual pen setting when off canvas (unless skipping buffer)
+ if (!state.offCanvas || inPenState.skipBuffer) {
+ setHeight(inPenState.state, callback, inPenState.skipBuffer);
+ } else {
+ // Save the state anyways so we can come back to it
+ state.state = inPenState.state;
+ if (callback) callback(1);
+ }
+ return;
+ }
+
+ // Absolute positions are set
+ if (inPenState.x !== undefined) {
+ // Input values are given as percentages of working area (not max area)
+ const point = {
+ abs: inPenState.abs,
+ x: Number(inPenState.x),
+ y: Number(inPenState.y),
+ };
+
+ // Don't accept bad input
+ const penNaN = Number.isNaN(point.x) || Number.isNaN(point.y);
+ const penFinite = Number.isFinite(point.x) && Number.isFinite(point.y);
+ if (penNaN || !penFinite) {
+ if (debug) {
+ console.log('setPen: Either X/Y not valid numbers.');
}
-
+ callback(false);
return;
}
+ // Override this to move with accelleration if override isn't set.
+ // TODO: Allow switching between accell and flat speed movements.
+ /* if (speedOverride === null) {
+ // Convert the percentage or absolute in/mm XY values into absolute steps.
+ const startPoint = cncserver.utils.stepsToAbs(pen.state, 'mm');
+ const endPoint = utils.stepsToAbs(cncserver.utils.inPenToSteps(point), 'mm');
- // State/z position has been passed
- if (typeof inPenState.state !== 'undefined') {
- // Disallow actual pen setting when off canvas (unless skipping buffer)
- if (!pen.state.offCanvas || inPenState.skipBuffer) {
- pen.setHeight(inPenState.state, callback, inPenState.skipBuffer);
- } else {
- // Save the state anyways so we can come back to it
- pen.state.state = inPenState.state;
- if (callback) callback(1);
- }
- return;
- }
+ const movePath = new Path([
+ startPoint,
+ endPoint,
+ ]);
+ // cncserver.sockets.sendPaperUpdate();
+
+ const accellPoints = cncserver.drawing.accell.getPointsSync(movePath);
+ accellPoints.forEach((pos) => {
+ pen.setPen({ ...pos.point, abs: 'mm' }, null, pos.speed);
+ });
- // Absolute positions are set
- if (inPenState.x !== undefined) {
- // Input values are given as percentages of working area (not max area)
- const point = {
- abs: inPenState.abs,
- x: Number(inPenState.x),
- y: Number(inPenState.y),
- };
-
- // Don't accept bad input
- const penNaN = Number.isNaN(point.x) || Number.isNaN(point.y);
- const penFinite = Number.isFinite(point.x) && Number.isFinite(point.y);
- if (penNaN || !penFinite) {
+ pen.setPen({ ...endPoint, abs: 'mm' }, null, 0);
+ callback(true);
+
+ // movePath.remove(); // TODO: Move this to when it's done?
+ return;
+ } */
+
+ // Convert the percentage or absolute in/mm XY values into absolute steps.
+ const absInput = utils.inPenToSteps(point);
+ absInput.limit = 'workArea';
+
+ // Are we parking?
+ if (inPenState.park) {
+ // Don't repark if already parked (but not if we're skipping the buffer)
+ const parkPos = utils.centToSteps(bot.park, true);
+ if (
+ state.x === parkPos.x
+ && state.y === parkPos.y
+ && !inPenState.skipBuffer
+ ) {
if (debug) {
- console.log('setPen: Either X/Y not valid numbers.');
+ console.log('setPen: Can\'t park when already parked.');
}
- callback(false);
+ if (callback) callback(false);
return;
}
+ // Set Absolute input value to park position in steps
+ absInput.x = parkPos.x;
+ absInput.y = parkPos.y;
+ absInput.limit = 'maxArea';
+ }
- // Override this to move with accelleration if override isn't set.
- // TODO: Allow switching between accell and flat speed movements.
- /* if (speedOverride === null) {
- // Convert the percentage or absolute in/mm XY values into absolute steps.
- const startPoint = cncserver.utils.stepsToAbs(pen.state, 'mm');
- const endPoint = cncserver.utils.stepsToAbs(cncserver.utils.inPenToSteps(point), 'mm');
-
- const movePath = new Path([
- startPoint,
- endPoint,
- ]);
- // cncserver.sockets.sendPaperUpdate();
-
- const accellPoints = cncserver.drawing.accell.getPointsSync(movePath);
- accellPoints.forEach((pos) => {
- pen.setPen({ ...pos.point, abs: 'mm' }, null, pos.speed);
- });
+ movePenAbs(
+ absInput,
+ callback,
+ inPenState.waitForCompletion,
+ inPenState.skipBuffer,
+ speedOverride
+ );
- pen.setPen({ ...endPoint, abs: 'mm' }, null, 0);
- callback(true);
+ return;
+ }
- // movePath.remove(); // TODO: Move this to when it's done?
- return;
- } */
+ if (callback) callback(state);
+}
- // Convert the percentage or absolute in/mm XY values into absolute steps.
- const absInput = cncserver.utils.inPenToSteps(point);
- absInput.limit = 'workArea';
-
- // Are we parking?
- if (inPenState.park) {
- // Don't repark if already parked (but not if we're skipping the buffer)
- const park = cncserver.utils.centToSteps(cncserver.settings.bot.park, true);
- if (
- pen.state.x === park.x
- && pen.state.y === park.y
- && !inPenState.skipBuffer
- ) {
- if (debug) {
- console.log('setPen: Can\'t park when already parked.');
- }
- if (callback) callback(false);
- return;
- }
+/**
+ * Set the "power" option
+ *
+ * @param {number} power
+ * Value from 0 to 100 to send to the bot.
+ * @param callback
+ * Callback triggered when operation should be complete.
+ * @param skipBuffer
+ * Set to true to skip adding the command to the buffer and run it
+ * immediately.
+ */
+export function setPower(power, callback, skipBuffer) {
+ const powers = botConf.get('penpower') || { min: 0, max: 0 };
- // Set Absolute input value to park position in steps
- absInput.x = park.x;
- absInput.y = park.y;
- absInput.limit = 'maxArea';
- }
+ run(
+ 'custom',
+ cmdstr(
+ 'penpower',
+ { p: Math.round(power * powers.max) + Number(powers.min) }
+ )
+ );
- cncserver.control.movePenAbs(
- absInput,
- callback,
- inPenState.waitForCompletion,
- inPenState.skipBuffer,
- speedOverride
- );
+ state.power = power;
+ if (callback) callback(true);
+}
- return;
- }
+/**
+ * Run a servo position from a given percentage or named height value into
+ * the buffer, or directly via skipBuffer.
+ *
+ * @param {number|string} inState
+ * Named height preset machine name, or float between 0 & 1.
+ * @param callback
+ * Callback triggered when operation should be complete.
+ * @param skipBuffer
+ * Set to true to skip adding the command to the buffer and run it
+ * immediately.
+ */
+export function setHeight(inState, callback, skipBuffer) {
+ let servoDuration = botConf.get('servo:minduration');
+
+ // Convert the incoming state
+ const conv = utils.stateToHeight(inState);
+ const { height = 0, state: stateValue = null } = conv;
+
+ // If we're skipping the buffer, just set the height directly
+ if (skipBuffer) {
+ console.log('Skipping buffer to set height:', height);
+ actuallyMoveHeight(height, stateValue, callback);
+ return;
+ }
+
+ const sourceHeight = state.height;
+
+ // Pro-rate the duration depending on amount of change to tip of buffer.
+ // TODO: Replace with cncserver.utils.getHeightChangeData()
+ if (state.height) {
+ const servo = botConf.get('servo');
+ const range = parseInt(servo.max, 10) - parseInt(servo.min, 10);
+ servoDuration = Math.round(
+ (Math.abs(height - state.height) / range) * servoDuration
+ ) + 1;
+ }
+
+ // Actually set tip of buffer to given sanitized state & servo height.
+ state.height = height;
+ state.z = height;
+ state.state = stateValue;
+
+ // Run the height into the command buffer
+ run('height', { z: height, source: sourceHeight }, servoDuration);
+
+ // Height movement callback servo movement duration offset
+ const delay = servoDuration - gConf.get('bufferLatencyOffset');
+ if (callback) {
+ setTimeout(() => {
+ callback(1);
+ }, Math.max(delay, 0));
+ }
+}
- if (callback) callback(pen);
- };
+/**
+ * Reset state of pen to current head (actualPen).
+ */
+export function resetState() {
+ utils.applyObjectTo(actualPen.state, state);
+ trigger('pen.update', state);
+}
- /**
- * Set the "power" option
- *
- * @param {number} power
- * Value from 0 to 100 to send to the bot.
- * @param callback
- * Callback triggered when operation should be complete.
- * @param skipBuffer
- * Set to true to skip adding the command to the buffer and run it
- * immediately.
- */
- pen.setPower = (power, callback, skipBuffer) => {
- const powers = cncserver.settings.botConf.get('penpower') || { min: 0, max: 0 };
-
- cncserver.run(
- 'custom',
- cncserver.control.cmdstr(
- 'penpower',
- { p: Math.round(power * powers.max) + Number(powers.min) }
- )
- );
+/**
+ * Park the pen.
+ *
+ * @param {boolean} direct
+ * True to send direct and skip the buffer.
+ * @param {function} callback
+ * Ya know.
+ */
+export function park(direct = false, callback = () => { }) {
+ setHeight('up', () => {
+ setPen({
+ x: bot.park.x,
+ y: bot.park.y,
+ park: true,
+ skipBuffer: direct,
+ }, callback);
+ }, direct);
+}
- pen.state.power = power;
- if (callback) callback(true);
- };
+/**
+ * Force the values of a given set of keys within the pen state.
+ *
+ * @param {object} inState
+ * Flat object of key/value pairs to FORCE into the pen state. Only used to
+ * correct head state or to update position along the buffer.
+ * @param {bool} skipUpdate
+ * If true, no update will be sent.
+ */
+export function forceState(inState, skipUpdate = false) {
+ // Only trigger update if not skipping.
+ utils.applyObjectTo(inState, state, true);
+ if (!skipUpdate) {
+ trigger('pen.update', state);
+ }
+}
- /**
- * Run a servo position from a given percentage or named height value into
- * the buffer, or directly via skipBuffer.
- *
- * @param {number|string} state
- * Named height preset machine name, or float between 0 & 1.
- * @param callback
- * Callback triggered when operation should be complete.
- * @param skipBuffer
- * Set to true to skip adding the command to the buffer and run it
- * immediately.
- */
- pen.setHeight = (state, callback, skipBuffer) => {
- let servoDuration = cncserver.settings.botConf.get('servo:minduration');
-
- // Convert the incoming state
- const conv = cncserver.utils.stateToHeight(state);
- const { height = 0, state: stateValue = null } = conv;
-
- // If we're skipping the buffer, just set the height directly
- if (skipBuffer) {
- console.log('Skipping buffer to set height:', height);
- cncserver.control.actuallyMoveHeight(height, stateValue, callback);
- return;
- }
+/**
+ * Set the internal state hash value.
+ *
+ * @param {string} hash
+ * Buffer hash to set.
+ */
+export function setHash(hash) {
+ state.bufferHash = hash;
+}
- const sourceHeight = pen.state.height;
+// Setup initial park position and set default.
+bindTo('controller.setup', 'pen', () => {
+ // Set initial pen position at park position
+ forceState(utils.centToSteps(bot.park, true));
- // Pro-rate the duration depending on amount of change to tip of buffer.
- // TODO: Replace with cncserver.utils.getHeightChangeData()
- if (pen.state.height) {
- const servo = cncserver.settings.botConf.get('servo');
- const range = parseInt(servo.max, 10) - parseInt(servo.min, 10);
- servoDuration = Math.round(
- (Math.abs(height - pen.state.height) / range) * servoDuration
- ) + 1;
- }
+ // Trigger pen.setup with full state.
+ trigger('pen.setup', state, true);
+});
- // Actually set tip of buffer to given sanitized state & servo height.
- pen.state.height = height;
- pen.state.state = stateValue;
+/**
+ * Helper abstraction for checking if the tip of buffer pen is "down" or not.
+ *
+ * @param {object} inPen
+ * The pen state object to check for down status, defaults to buffer tip.
+ * @returns {Boolean}
+ * False if pen is considered up, true if pen is considered down.
+ */
+export function isDown(inPen) {
+ const checkPen = inPen?.state || state;
+ if (checkPen.state === 'up' || checkPen.state < 0.5) {
+ return false;
+ }
- // Run the height into the command buffer
- cncserver.run('height', { z: height, source: sourceHeight }, servoDuration);
+ return true;
+}
- // Height movement callback servo movement duration offset
- const delay = servoDuration - cncserver.settings.gConf.get('bufferLatencyOffset');
- if (callback) {
- setTimeout(() => {
- callback(1);
- }, Math.max(delay, 0));
- }
- };
+/**
+ * Calculate the duration for a pen movement from the distance.
+ * Takes into account whether pen is up or down
+ *
+ * @param {float} distance
+ * Distance in steps that we'll be moving
+ * @param {int} min
+ * Optional minimum value for output duration, defaults to 1.
+ * @param {object} inPen
+ * Incoming pen object to check (buffer tip or bot current).
+ * @param {number} speedOverride
+ * Optional speed override, overrides calculated speed percent.
+ *
+ * @returns {number}
+ * Millisecond duration of how long the move should take
+ */
+export function getDurationFromDistance(distance, min = 1, inPen, speedOverride = null) {
+ const minSpeed = parseFloat(botConf.get('speed:min'));
+ const maxSpeed = parseFloat(botConf.get('speed:max'));
+ const drawingSpeed = botConf.get('speed:drawing');
+ const movingSpeed = botConf.get('speed:moving');
- /**
- * Reset state of pen to current head (actualPen).
- */
- pen.resetState = () => {
- pen.state = cncserver.utils.extend({}, cncserver.actualPen.state);
- };
+ // Use given speed over distance to calculate duration
+ let speed = (isDown(inPen)) ? drawingSpeed : movingSpeed;
+ if (speedOverride != null) {
+ speed = speedOverride;
+ }
- /**
- * Park the pen.
- *
- * @param {boolean} direct
- * True to send direct and skip the buffer.
- * @param {function} callback
- * Ya know.
- */
- pen.park = (direct = false, callback = () => {}) => {
- pen.setHeight('up', () => {
- pen.setPen({
- x: cncserver.settings.bot.park.x,
- y: cncserver.settings.bot.park.y,
- park: true,
- skipBuffer: direct,
- }, callback);
- }, direct);
- };
+ speed = parseFloat(speed) / 100;
+ // Convert to steps from percentage
+ speed = (speed * (maxSpeed - minSpeed)) + minSpeed;
- /**
- * Force the values of a given set of keys within the pen state.
- *
- * @param {object} inState
- * Flat object of key/value pairs to FORCE into the pen state. Only used to
- * correct head state or to update position along the buffer.
- */
- pen.forceState = (inState) => {
- for (const [key, value] of Object.entries(inState)) {
- // Only set a value if the key exists in the state already.
- if (key in pen.state) {
- pen.state[key] = value;
- }
- }
- };
+ // Sanity check speed value
+ speed = speed > maxSpeed ? maxSpeed : speed;
+ speed = speed < minSpeed ? minSpeed : speed;
- /**
- * Set the internal state hash value.
- *
- * @param {string} hash
- * Buffer hash to set.
- */
- pen.setHash = (hash) => {
- pen.state.bufferHash = hash;
- };
+ // How many steps a second?
+ return Math.max(Math.abs(Math.round(distance / speed * 1000)), min);
+}
- // Exports...
- pen.exports = {
- setPen: pen.setPen,
- setHeight: pen.setHeight,
+/**
+ * Given two points, find the difference and duration at current speed
+ *
+ * @param {{x: number, y: number}} src
+ * Source position coordinate (in steps).
+ * @param {{x: number, y: number}} dest
+ * Destination position coordinate (in steps).
+ * @param {number} speed
+ * Speed override for this movement in percent.
+ *
+ * @returns {{d: number, x: number, y: number}}
+ * Object containing the change amount in steps for x & y, along with the
+ * duration in milliseconds.
+ */
+export function getPosChangeData(src, dest, speed = null) {
+ let change = {
+ x: Math.round(dest.x - src.x),
+ y: Math.round(dest.y - src.y),
};
- return pen;
-};
+ // Calculate distance
+ const duration = getDurationFromDistance(utils.getVectorLength(change), 1, src, speed);
+
+ // Adjust change direction/inversion
+ if (botConf.get('controller').position === 'relative') {
+ // Invert X or Y to match stepper direction
+ change.x = gConf.get('invertAxis:x') ? change.x * -1 : change.x;
+ change.y = gConf.get('invertAxis:y') ? change.y * -1 : change.y;
+ } else { // Absolute! Just use the "new" absolute X & Y locations
+ change.x = state.x;
+ change.y = state.y;
+ }
+
+ // Swap motor positions
+ if (gConf.get('swapMotors')) {
+ change = {
+ x: change.y,
+ y: change.x,
+ };
+ }
+
+ return { d: duration, x: change.x, y: change.y };
+}
diff --git a/src/components/core/control/cncserver.print.js b/src/components/core/control/cncserver.print.js
new file mode 100644
index 00000000..70a6c87d
--- /dev/null
+++ b/src/components/core/control/cncserver.print.js
@@ -0,0 +1,269 @@
+/**
+ * @file Abstraction module for print API and print rendering.
+ */
+
+import * as projects from 'cs/projects';
+import png from 'png-metadata';
+import path from 'path';
+import * as utils from 'cs/utils';
+import * as tools from 'cs/tools';
+import { movePenAbs } from 'cs/control';
+import { colors, base, accell } from 'cs/drawing';
+import { trigger } from 'cs/binder';
+
+// TODO:
+// - Convert buffer render over to string render
+// - Resettable render pen state (Don't do it like this below)
+// - Render sections to work groups, output as ordered colorset item keyed "gcode" arrays
+// - Ensure this render is still bufferable and supports all features (Inside, outside, tool positions)
+// - Render progress updates.
+// - Build PNG with info window in bottom left.
+// - Write render data to PNG
+// - Finish WCB options.
+
+// Set of PNG chunk headers for C.NC S.erver P.rint PNGs.
+export const pngPayloadChunks = {
+ PRINT_CHUNK_TITLE: 'CSPt', // Plain: Title of the print.
+ PRINT_CHUNK_PROJECT: 'CSPp', // JSON: Project specific dat (paper color, etc).
+ PRINT_CHUNK_COLORSET: 'CSPc', // JSON: Colorset used to generate data.
+ PRINT_CHUNK_SETTINGS: 'CSPs', // JSON: Print settings used to generate data.
+ PRINT_CHUNK_DATA: 'CSPd', // JSON: Array of rendered work groupings.
+};
+
+/**
+ * Using a loaded PNG file buffer, set a specified chunk by name.
+ *
+ * @param {Buffer} data
+ * PNG data buffer from readFileSync.
+ * @param {string} name
+ * 4 character chunk identifier.
+ *
+ * @returns {Buffer}
+ * Joined binary buffer containing new data.
+ */
+function addChunk(data, name, value, isJSON = true) {
+ const chunks = png.splitChunk(data);
+ const writeData = isJSON ? JSON.stringify(value) : value;
+ chunks.splice(-1, 0, png.createChunk(name, writeData));
+ return png.joinChunk(chunks);
+}
+
+/**
+ * Using a loaded PNG file buffer, get the specified chunk by name.
+ *
+ * @param {Buffer} data
+ * PNG data buffer from readFileSync.
+ * @param {string} name
+ * 4 character chunk identifier.
+ *
+ * @returns {string|Object}
+ * Data pulled from PNG file data.
+ */
+function getChunk(data, name, isJSON = true) {
+ const chunks = png.splitChunk(data);
+ let outData = chunks.find(chunk => chunk.type === name);
+ outData = outData?.data ?? null;
+ if (isJSON) {
+ try {
+ outData = JSON.parse(outData);
+ } catch (error) {
+ // Oh well. Return the untouched data.
+ }
+ }
+ return outData;
+}
+
+/**
+ * Save the current project and rendered print content as a PNG file.
+ *
+ * @export
+ */
+export function saveFile() {
+ const filePath = path.join(utils.__basedir, 'interface', 'test_zener.png');
+ const data = png.readFileSync(filePath);
+
+ /*
+ data = addChunk(data, PRINT_CHUNK_COLORSET, 'colorset goes here');
+ data = addChunk(data, PRINT_CHUNK_SETTINGS, 'settings goes here');
+ data = addChunk(data, PRINT_CHUNK_DATA, 'data goes here');
+ */
+
+ const outFilePath = path.join(utils.__basedir, 'interface', 'test_png_write.png');
+ png.writeFileSync(outFilePath, data, 'binary');
+}
+
+/**
+ * Parse the data chunks from a given PNG file.
+ *
+ * @export
+ * @param {string} filePath
+ * Path to file to be read.
+ *
+ * @returns {Object|null}
+ * Object of all print data from the PNG, null if invalid.
+ */
+export function getPrintData(filePath) {
+ const data = png.readFileSync(filePath);
+
+ const commands = getChunk(data, pngPayloadChunks.PRINT_CHUNK_DATA);
+
+ const out = {};
+ if (commands) {
+ out.title = getChunk(data, pngPayloadChunks.PRINT_CHUNK_TITLE);
+ out.colorset = getChunk(data, pngPayloadChunks.PRINT_CHUNK_COLORSET);
+ out.settings = getChunk(data, pngPayloadChunks.PRINT_CHUNK_SETTINGS);
+ out.data = commands;
+ } else {
+ return null;
+ }
+
+ return out;
+}
+
+/**
+ * Recursively calculate acceleration along a given path.
+ *
+ * @export
+ * @param {paper.Path} pathItem
+ * Path to file to be read.
+ *
+ * @returns {Object|null}
+ * Object of all print data from the PNG, null if invalid.
+ */
+export const accelMoveOnPath = pathItem => new Promise((success, error) => {
+ const move = (point, speed = null) => {
+ const stepPoint = utils.absToSteps(point, 'mm', true);
+ movePenAbs(stepPoint, null, true, null, speed);
+ };
+
+ // Pen up
+ pen.setPen({ state: 'up' });
+
+ // Move to start of path, then pen down.
+ move(pathItem.getPointAt(0));
+ pen.setPen({ state: 'draw' });
+
+ // Calculate groups of accell points and run them into moves.
+ accell.getPoints(pathItem, accellPoints => {
+ // If we have data, move to those points.
+ if (accellPoints && accellPoints.length) {
+ // Move through all accell points from start to nearest end point
+ accellPoints.forEach(pos => {
+ move(pos.point, pos.speed);
+ });
+ } else {
+ // Null means generation of accell points was cancelled.
+ if (accellPoints !== null) {
+ // No points? We're done. Wrap up the line.
+
+ // Move to end of path...
+ move(pathItem.getPointAt(pathItem.length));
+
+ // If it's a closed path, overshoot back home.
+ if (pathItem.closed) {
+ move(pathItem.getPointAt(0));
+ }
+
+ // End with pen up.
+ pen.setPen({ state: 'up' });
+
+ // Fulfull the promise for this subpath.
+ success();
+ return;
+ }
+
+ // If we're here, path generation was cancelled, bubble it.
+ error();
+ }
+ });
+});
+
+/**
+ * Actually render Paper paths into movements
+ *
+ * @param {object} source
+ * Source paper object containing the children, defaults to preview layer.
+ */
+export function renderPathsToMoves(reqSettings = {}) {
+ const source = base.layers.print;
+ const settings = {
+ parkAfter: true,
+ ...reqSettings,
+ };
+ // TODO:
+ // * Join extant non-closed paths with endpoint distances < 0.5mm
+ // * Split work by colors
+ // * Allow WCB bot support to inject tool changes for refill support
+ // * Order paths by pickup/dropoff distance
+
+ // Store work for all paths grouped by color
+ const workGroups = colors.getWorkGroups();
+ const validColors = Object.keys(workGroups);
+ source.children.forEach(colorGroup => {
+ if (workGroups[colorGroup.name]) {
+ workGroups[colorGroup.name] = base.getPaths(colorGroup);
+ }
+ });
+
+ let workGroupIndex = 0;
+ function nextWorkGroup() {
+ const colorID = validColors[workGroupIndex];
+ if (colorID) {
+ const paths = workGroups[colorID];
+
+ if (paths.length) {
+ // Bind trigger for implementors on work group begin.
+ trigger('print.render.group.begin', colorID);
+
+ // Do we have a tool for this colorID? If not, use manualswap.
+ if (colors.doColorParsing()) {
+ const changeTool = tools.has(colorID) ? colorID : 'manualswap';
+ tools.changeTo(changeTool, colorID);
+ }
+
+ let pathIndex = 0;
+ const nextPath = () => {
+ if (paths[pathIndex]) {
+ accelMoveOnPath(paths[pathIndex]).then(() => {
+ // Trigger implementors for path render complete.
+ trigger('print.render.path.finish', paths[pathIndex]);
+
+ // Path in this group done, move to the next.
+ pathIndex++;
+ nextPath();
+ }).catch(error => {
+ // If we have an error object, it's an actual error!
+ if (error) {
+ console.error(error);
+ }
+ // Otherwise, path generation has been entirely cancelled.
+ workGroupIndex = validColors.length;
+ });
+ } else {
+ // No more paths in this group, move to the next.
+ workGroupIndex++;
+ nextWorkGroup();
+ }
+ };
+
+ // Start processing paths in the initial workgroup.
+ nextPath();
+ } else {
+ // There is no work for this group, move to the next one.
+ workGroupIndex++;
+ nextWorkGroup();
+ }
+ } else {
+ // Actually complete with all paths in all work groups!
+ // TODO: Fullfull a promise for the function?
+
+ // Trigger binding for implementors on sucessfull rendering completion.
+ trigger('print.render.finish');
+
+ // If settings say to park.
+ if (settings.parkAfter) pen.park();
+ }
+ }
+ // Intitialize working on the first group on the next process tick.
+ process.nextTick(nextWorkGroup);
+}
diff --git a/src/components/core/control/cncserver.projects.js b/src/components/core/control/cncserver.projects.js
index 8ce5ed42..f850ba82 100644
--- a/src/components/core/control/cncserver.projects.js
+++ b/src/components/core/control/cncserver.projects.js
@@ -1,211 +1,249 @@
/**
* @file Abstraction for high level project management, execution.
*/
-const fs = require('fs');
-const path = require('path');
-const DataURI = require('datauri');
-const dataUriToBuffer = require('data-uri-to-buffer');
-const { homedir } = require('os');
+import fs from 'fs';
+import path from 'path';
+import DataURI from 'datauri';
+import dataUriToBuffer from 'data-uri-to-buffer';
+import { homedir } from 'os';
+import * as utils from 'cs/utils';
+import { getDataDefault } from 'cs/schemas';
+import {
+ colors, stage, preview, temp
+} from 'cs/drawing';
+import { bindTo, trigger } from 'cs/binder';
+// import { renderPathsToMoves } from 'cs/control';
+import * as content from 'cs/content';
const PROJECT_JSON = 'cncserver.project.json';
const PREVIEW_SVG = 'cncserver.project.preview.svg';
// Exposed export to be attached as cncserver.projects
-const projects = {
- items: new Map(),
- id: 'projects',
+export const state = {
homeDir: '',
current: '',
rendering: false,
printing: false,
};
+export const items = new Map();
+
+// Fit a "simple" or complete object into the full schema object shape.
+export const fitShape = data => getDataDefault('projects', data);
+
const getDirectories = source => fs.readdirSync(source, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
-function getProjectDirName(item) {
- return path.resolve(projects.homeDir, `${item.name}-${item.hash}`);
+function getProjectDirName({ name, hash }) {
+ return path.resolve(state.homeDir, `${name}-${hash}`);
}
// Load all projects by file from given paths.
function loadProjects() {
- projects.items.clear();
+ items.clear();
- const dirs = getDirectories(projects.homeDir);
- dirs.forEach((dir) => {
- const jsonPath = path.resolve(projects.homeDir, dir, PROJECT_JSON);
+ const dirs = getDirectories(state.homeDir);
+ dirs.forEach(dir => {
+ const jsonPath = path.resolve(state.homeDir, dir, PROJECT_JSON);
if (fs.existsSync(jsonPath)) {
- // eslint-disable-next-line import/no-dynamic-require, global-require
- const item = require(jsonPath);
- projects.items.set(item.hash, {
+ const item = fitShape(utils.getJSONFile(jsonPath));
+ items.set(item.hash, {
...item,
- dir: path.resolve(projects.homeDir, dir),
+ dir: path.resolve(state.homeDir, dir),
});
}
});
}
+// Load a project from a file. Assume hash is validated.
+export function openProject(hash) {
+ const project = items.get(hash);
+ state.current = hash;
+
+ content.items.clear();
+ stage.clearAll();
+ preview.clearAll();
+
+ // Apply the colorset preset in the project, if we can.
+ if (project.colorset) {
+ colors.applyPreset(project.colorset).catch(e => {
+ console.error(e);
+ });
+ }
+
+ // Get all the info loaded into the content items, and get the file data.
+ Object.entries(project.content).forEach(([, item]) => {
+ const filePath = getContentFilePath(item.source.content, hash);
+ let data = '';
+
+ // TODO: What if a file isn't there?
+ if (item.source.type === 'raster') {
+ const datauri = new DataURI(filePath);
+ data = datauri.content;
+ } else {
+ data = fs.readFileSync(filePath).toString();
+ }
+
+ content.loadFromFile(item, data);
+ });
+
+ trigger('projects.update', project);
+
+ return getResponseItem(hash);
+}
+
// Initialize with a "current" project.
function initProject() {
const now = new Date();
// Create a temp project or load last.
- projects.addItem({
+ // TODO: When deleting an open project, default to this.
+ addItem({
title: 'New Project',
description: `Automatic project created ${now.toLocaleDateString()}`,
open: true,
+ colorset: 'default',
});
}
-module.exports = (cncserver) => {
- const { utils } = cncserver;
-
- // TODO: Implement project paging.
- projects.getItems = () => {
- const items = [];
- projects.items.forEach(({
- modified, hash, title, description, name, ...project
- }) => {
- items.push({
- hash,
- title,
- description,
- modified,
- name,
- preview: projects.getRelativePreview(project),
- });
+// Get the relative preview SVG path from a project object.
+export function getRelativePreview(project) {
+ const absPath = path.resolve(project.dir, PREVIEW_SVG);
+ const cncserverHome = path.resolve(homedir(), 'cncserver');
+ return fs.existsSync(absPath) ? absPath.replace(cncserverHome, '/home') : null;
+}
+
+// TODO: Implement project paging.
+export function getItems() {
+ const newItems = [];
+ items.forEach(({
+ modified, hash, title, description, name, ...project
+ }) => {
+ newItems.push({
+ hash,
+ title,
+ description,
+ modified,
+ name,
+ preview: getRelativePreview(project),
});
- return items;
- };
+ });
+ return newItems;
+}
- // Get the relative preview SVG path from a project object.
- projects.getRelativePreview = (project) => {
- const absPath = path.resolve(project.dir, PREVIEW_SVG);
- const cncserverHome = path.resolve(homedir(), 'cncserver');
- return fs.existsSync(absPath) ? absPath.replace(cncserverHome, '/home') : null;
- };
+export const getCurrentHash = () => state.current;
- projects.getCurrentHash = () => projects.current;
+// Customize the stored object to be more appropriate for responses.
+export function getResponseItem(hash = state.current) {
+ const project = { ...items.get(hash) };
+ delete project.dir;
+ return project;
+}
- // Customize the stored object to be more appropriate for responses.
- projects.getResponseItem = (hash) => {
- const project = { ...projects.items.get(hash) };
- delete project.dir;
- return project;
- };
+// Assume schema has been checked by the time we get here.
+// TODO: add support for adding content on creation.
+// TODO: When does creating a project not set it as current?
+export function addItem({
+ title, description = '', name, open, options = {},
+}) {
+ const cDate = new Date();
+
+ const optionsWithDefaults = fitShape({
+ title,
+ description,
+ name: utils.getMachineName(name || title, 15),
+ open,
+ colorset: colors.set.name,
+ options,
+ });
- // Assume schema has been checked by the time we get here.
- // TODO: add support for adding content on creation.
- // TODO: When does creating a project not set it as current?
- projects.addItem = ({
- title, description = '', name, open,
- }) => {
- const cDate = new Date();
+ // Compute the entire new object.
+ const item = {
+ cncserverProject: 'v3',
+ hash: utils.getHash({ title, description, name }, 'date'),
+ created: cDate.toISOString(),
+ modified: cDate.toISOString(),
+ ...optionsWithDefaults,
+ content: {},
+ };
+ item.dir = getProjectDirName(item);
- // Compute the entire new object.
- const item = {
- cncserverProject: 'v3',
- hash: utils.getHash({ title, description, name }, 'date'),
- title,
- description,
- name: utils.getMachineName(name || title, 15),
- created: cDate.toISOString(),
- modified: cDate.toISOString(),
- content: {},
- };
- item.dir = getProjectDirName(item);
-
- projects.items.set(item.hash, item);
-
- // If we're opening by default.
- if (open) {
- projects.open(item.hash);
- }
+ items.set(item.hash, item);
- return projects.getResponseItem(item.hash);
- };
+ // If we're opening by default.
+ if (open) {
+ openProject(item.hash);
+ }
- // Convert a relative file path and project hash into a full file path.
- projects.getContentFilePath = (name, projectHash) => {
- const project = projects.items.get(projectHash);
- return path.resolve(project.dir, name);
- };
+ return getResponseItem(item.hash);
+}
- // Actually save the files out for a project.
- projects.saveProjectFiles = (hash = projects.current) => {
- const item = projects.items.get(hash);
- item.modified = new Date().toISOString();
- projects.items.set(hash, item);
+// Convert a relative file path and project hash into a full file path.
+export function getContentFilePath(name, projectHash) {
+ const project = items.get(projectHash);
+ return path.resolve(project.dir, name);
+}
- const dir = utils.getDir(getProjectDirName(item));
+// Set the colorset for a project.
+// Currently only happens when a colorset preset is loaded.
+export function setColorset(colorset, hash = state.current) {
+ const item = items.get(hash);
+ if (item.colorset !== colorset) {
+ item.colorset = colorset;
+ saveProjectFiles(hash);
+ }
+}
- // Make an ammended version of item for saving, don't store some keys.
- const saveItem = { ...item };
- delete saveItem.dir;
+// Actually save the files out for a project.
+export function saveProjectFiles(hash = state.current) {
+ const item = items.get(hash);
+ item.modified = new Date().toISOString();
+ items.set(hash, item);
- // Save the preview.
- fs.writeFileSync(path.resolve(dir, PREVIEW_SVG), cncserver.drawing.stage.getPreviewSVG());
+ const dir = utils.getDir(getProjectDirName(item));
- // Write the final settings file.
- fs.writeFileSync(path.resolve(dir, PROJECT_JSON), JSON.stringify(saveItem, null, 2));
- };
+ // Make an ammended version of item for saving, don't store some keys.
+ const saveItem = { ...item };
+ delete saveItem.dir;
- // Load a project from a file. Assume hash is validated.
- projects.open = (hash) => {
- const { content } = cncserver;
- const project = projects.items.get(hash);
- projects.current = hash;
-
- content.items.clear();
- cncserver.drawing.stage.clearAll();
-
- // Get all the info loaded into the content items, and get the file data.
- Object.entries(project.content).forEach(([, item]) => {
- const filePath = projects.getContentFilePath(item.source.content, hash);
- let data = '';
-
- // TODO: What if a file isn't there?
- if (item.source.type === 'raster') {
- const datauri = new DataURI(filePath);
- data = datauri.content;
- } else {
- data = fs.readFileSync(filePath).toString();
- }
+ // Save the preview.
+ fs.writeFileSync(path.resolve(dir, PREVIEW_SVG), stage.getPreviewSVG());
- content.loadFromFile(item, data);
- });
+ // Write the final settings file.
+ fs.writeFileSync(path.resolve(dir, PROJECT_JSON), JSON.stringify(saveItem, null, 2));
- return projects.getResponseItem(hash);
- };
+ trigger('projects.update', getResponseItem());
+}
- // Add (or update) a content instance entry for a project.
- // Assume project hash validation.
- projects.saveContentData = (item, projectHash) => {
- const project = projects.items.get(projectHash);
- if (!project.content) project.content = {};
- project.content[item.hash] = item;
- projects.items.set(projectHash, project);
- projects.saveProjectFiles(projectHash);
- };
+// Add (or update) a content instance entry for a project.
+// Assume project hash validation.
+export function saveContentData(item, projectHash) {
+ const project = items.get(projectHash);
+ if (!project.content) project.content = {};
+ project.content[item.hash] = item;
+ items.set(projectHash, project);
+ saveProjectFiles(projectHash);
+}
- // Remove the data for a piece of content.
- projects.removeContentData = (contentHash, projectHash) => {
- const project = projects.items.get(projectHash);
- delete project.content[contentHash];
- projects.items.set(projectHash, project);
- projects.saveProjectFiles(projectHash);
- };
+// Remove the data for a piece of content.
+export function removeContentData(contentHash, projectHash) {
+ const project = items.get(projectHash);
+ delete project.content[contentHash];
+ items.set(projectHash, project);
+ saveProjectFiles(projectHash);
+}
- // Save content to a file/project.
- projects.saveContentFile = (source, projectHash) => new Promise((resolve, reject) => {
- const ext = cncserver.content.limits.extensions[source.mimetype];
+// Save content to a file/project.
+export function saveContentFile(source, projectHash) {
+ return new Promise((resolve, reject) => {
+ const ext = content.limits.extensions[source.mimetype];
const fileName = `${utils.getHash(source.content, null)}.${ext}`;
// If this is the first time content is being added to a project, we need to
// create the destination dir first and save all the parts.
- const project = projects.items.get(projectHash);
+ const project = items.get(projectHash);
if (!fs.existsSync(project.dir)) {
- projects.saveProjectFiles(projectHash);
+ saveProjectFiles(projectHash);
}
// Use a buffer for Data URI raster, or a string for the rest.
@@ -215,33 +253,55 @@ module.exports = (cncserver) => {
// TODO: Don't bother writing the file if it's the same.
// EDGECASE: file bytes don't match and it needs a rewrite.
- fs.writeFile(projects.getContentFilePath(fileName, projectHash), data, (err) => {
+ fs.writeFile(getContentFilePath(fileName, projectHash), data, err => {
if (err) { reject(err); } else { resolve(fileName); }
});
});
+}
- // Get a fully filled out merged settings object including project overrides.
- projects.getFullSettings = (settings, projectHash = projects.current) => {
- if (projectHash) {
- const project = projects.items.get(projectHash);
- return cncserver.schemas.getDataDefault(
- 'settings',
- utils.merge(project.settings || {}, settings)
- );
- }
+// Get a copy of the raw internal item for the current project.
+export const getCurrent = () => ({ ...items.get(state.current) });
- return cncserver.schemas.getDataDefault('settings', utils.merge(settings));
- };
+// Get a fully filled out merged settings object including project overrides.
+export function getFullSettings(settings, projectHash = state.current) {
+ if (projectHash) {
+ const project = items.get(projectHash);
+ return getDataDefault(
+ 'settings',
+ utils.merge(project.settings || {}, settings)
+ );
+ }
- // The only thing we actually allow editing of here is the Title, name and desc.
- projects.editItem = ({ hash }, { name, title, description, settings }) => new Promise((resolve, reject) => {
+ return getDataDefault('settings', utils.merge(settings));
+}
+
+// The only thing we actually allow editing of here is the Title, name and desc.
+export function editItem({ hash }, {
+ name, title, description, settings,
+}) {
+ return new Promise((resolve, reject) => {
let changes = false;
- const project = projects.items.get(hash);
+ const project = items.get(hash);
- // Change name.
+ // Change name (must rename folder).
if (name) {
changes = true;
- project.name = utils.getMachineName(name || title, 15);
+ const newName = utils.getMachineName(name || title, 15);
+
+ // Name change? Rename the dest folder.
+ if (newName !== project.name) {
+ const oldPath = getProjectDirName({ hash: project.hash, name: project.name });
+ const newPath = getProjectDirName({ hash: project.hash, name: newName });
+
+ // If the old dir exists, rename it.
+ if (fs.existsSync(oldPath)) {
+ fs.renameSync(oldPath, newPath);
+ }
+
+ project.dir = newPath;
+ }
+
+ project.name = newName;
}
// Change title
@@ -263,91 +323,101 @@ module.exports = (cncserver) => {
}
if (changes) {
- projects.items.set(hash, project);
- projects.saveProjectFiles(hash);
- resolve(projects.getResponseItem(hash));
+ items.set(hash, project);
+ trigger('projects.update', project);
+ saveProjectFiles(hash);
+ resolve(getResponseItem(hash));
} else {
- reject(new Error('Edits to existing projects can only change title, name, description, or settings.'));
+ reject(new Error(utils.singleLineString`Edits to existing projects can only
+ change title, name, description, or settings.`));
}
});
+}
- // Remove a project.
- projects.removeItem = hash => new Promise((resolve, reject) => {
- const project = projects.items.get(hash);
- const trashDir = path.resolve(utils.getUserDir('trash'), `project-${project.name}-${hash}`);
- fs.rename(project.dir, trashDir, (err) => {
- if (err) {
- reject(err);
- } else {
- projects.items.delete(hash);
- resolve();
- }
- });
- });
-
- // Wait till after Paper.js and schemas are loaded and get home folder, load projects.
- cncserver.binder.bindTo('schemas.loaded', projects.id, () => {
- projects.homeDir = cncserver.utils.getUserDir('projects');
- loadProjects();
- initProject();
- });
-
- // Rendering and print state management.
- projects.getPrintingState = () => projects.printing;
- projects.getRenderingState = () => projects.rendering;
-
- projects.setRenderingState = (newState, specificHash = null) => {
- if (projects.rendering === newState) return;
-
- if (newState) {
- projects.renderCurrentContent(specificHash).then(() => {
- // TODO: Send async stream update for render completion.
- // ...and render start?
- projects.rendering = false;
-
- // Clear out the temp layer to free memory.
- // TODO: Move this to a binder event?
- cncserver.drawing.temp.clearAll();
- });
- } else {
- // TODO: Stop the render...somehow?
- }
- projects.rendering = newState;
- };
-
- projects.setPrintingState = (newState) => {
- if (projects.printing === newState) return;
-
- if (newState) {
- // TODO:
- } else {
- // TODO:
- }
- projects.printing = newState;
- };
-
- // Render all loaded items to preview, or just one.
- projects.renderCurrentContent = (specificHash) => {
- // Clear out the preview.
- if (specificHash) {
- cncserver.drawing.preview.remove(specificHash, true);
- } else {
- cncserver.drawing.preview.clearAll(specificHash);
- }
-
- const renderPromises = [];
- if (specificHash) {
- const item = cncserver.content.items.get(specificHash);
- renderPromises.push(cncserver.content.renderContentItem(item));
+// Remove a project.
+export const removeItem = hash => new Promise((resolve, reject) => {
+ // TODO: When deleting an open project, default to this.
+ const project = items.get(hash);
+ const trashDir = path.resolve(
+ utils.getUserDir('trash'), `project-${project.name}-${hash}`
+ );
+ fs.rename(project.dir, trashDir, err => {
+ if (err) {
+ reject(err);
} else {
- cncserver.content.items.forEach((item) => {
- renderPromises.push(cncserver.content.renderContentItem(item));
- });
+ items.delete(hash);
+ resolve();
}
+ });
+});
+
+// Wait till after Paper.js and schemas are loaded and get home folder, load projects.
+bindTo('paper.ready', 'projects', () => {
+ state.homeDir = utils.getUserDir('projects');
+ loadProjects();
+ initProject();
+
+ // Load last. DEBUG
+ setTimeout(() => {
+ openProject('6a1253d8aaefb233');
+ }, 1);
+});
+
+// Rendering and print state management.
+export const getPrintingState = () => state.printing;
+export const getRenderingState = () => state.rendering;
+
+export function setRenderingState(newState, specificHash = null) {
+ if (state.rendering === newState) return;
+
+ if (newState) {
+ renderCurrentContent(specificHash).then(() => {
+ // TODO: Send async stream update for render completion.
+ // ...and render start?
+ state.rendering = false;
+
+ // Clear out the temp layer to free memory.
+ // TODO: Move this to a binder event?
+ temp.clearAll();
+ });
+ } else {
+ // TODO: Stop the render...somehow?
+ }
+ state.rendering = newState;
+}
- return Promise.all(renderPromises);
- };
+export function setPrintingState(newState) {
+ if (state.printing === newState) return;
+
+ if (newState) {
+ // TODO:
+ console.log('Start printing!');
+ // renderPathsToMoves();
+ } else {
+ // TODO:
+ console.log('Stop printing!');
+ }
+ state.printing = newState;
+}
+// Render all loaded items to preview, or just one.
+export function renderCurrentContent(specificHash) {
+ // Clear out the preview.
+ if (specificHash) {
+ preview.remove(specificHash, true);
+ } else {
+ preview.clearAll(specificHash);
+ }
+
+ const renderPromises = [];
+ if (specificHash) {
+ const item = content.items.get(specificHash);
+ renderPromises.push(content.renderContentItem(item));
+ } else {
+ content.items.forEach(item => {
+ renderPromises.push(content.renderContentItem(item));
+ });
+ }
- return projects;
-};
+ return Promise.all(renderPromises);
+}
diff --git a/src/components/core/control/cncserver.tools.js b/src/components/core/control/cncserver.tools.js
new file mode 100644
index 00000000..78720f55
--- /dev/null
+++ b/src/components/core/control/cncserver.tools.js
@@ -0,0 +1,328 @@
+/**
+ * @file Abstraction module for tool state and helper methods.
+ */
+import Paper from 'paper';
+import * as utils from 'cs/utils';
+import { botConf } from 'cs/settings';
+import { movePenAbs } from 'cs/control';
+import { setHeight, forceState } from 'cs/pen';
+import { trigger, bindTo } from 'cs/binder';
+import { base, colors } from 'cs/drawing';
+import run from 'cs/run';
+import { sendPaperUpdate } from 'cs/sockets';
+import { resume } from 'cs/buffer';
+
+const { Path, Group, PointText } = Paper;
+
+// Set as part of colorset tool update.
+export const set = {
+ name: '',
+ items: new Map(),
+};
+
+// List all tool presets and their data.
+export function listPresets(t, customOnly) {
+ // TODO: Translate title/description.
+ return customOnly
+ ? utils.getCustomPresets('toolsets')
+ : utils.getPresets('toolsets');
+}
+
+// List custom/overridden machine names.
+export function customKeys() {
+ return Object.keys(utils.getCustomPresets('toolsets'));
+}
+
+// List internal machine names.
+export function internalKeys() {
+ return Object.keys(utils.getInternalPresets('toolsets'));
+}
+
+// Object of preset keys with set tools parents unavailable to this bot.
+export function invalidPresets() {
+ const out = {};
+ const sets = listPresets();
+ const botTools = botConf.get('tools');
+ const botName = botConf.get('name');
+
+ // Move through all sets, check the toolset for missing parents
+ Object.entries(sets).forEach(([name, { items: setItems }]) => {
+ setItems.forEach(item => {
+ if (item.parent && !(item.parent in botTools)) {
+ if (!(name in out)) out[name] = {};
+ out[name][item.parent] = utils.singleLineString`'${botName}' does not supply
+ required parent tool '${item.parent}'`;
+ }
+ });
+ });
+
+ return out;
+}
+
+// Get the current toolset as a translated, array based object.
+export function getResponseSet(t) {
+ // TODO: Translate title/description.
+ return {
+ ...set,
+ items: utils.mapToArray(set.items),
+ };
+}
+
+// Convert a colorset toolset to a flat array
+export function colorsetTools(asMap = false) {
+ return asMap ? set.items : utils.mapToArray(set.items);
+}
+
+// Can we edit the given tool id?
+export function canEdit(id) {
+ const editableList = Array.from(set.items.keys());
+ return id ? editableList.includes(id) : editableList;
+}
+
+// Function to run after the tools have been changed (add, delete, edit).
+export function sendUpdate() {
+ saveCustom();
+ sendPaperUpdate();
+ trigger('tools.update');
+}
+
+// Function for editing tools (from the toolset only).
+export const edit = tool => new Promise(resolve => {
+ set.items.set(tool.id, tool);
+ sendUpdate();
+ resolve(set.items.get(tool.id));
+});
+
+// Function for editing toolset base properties (name, manufacturer, title, desc).
+export const editSet = toolset => new Promise(resolve => {
+ toolset.name = utils.getMachineName(toolset.name, 64);
+ // Name change: Update in colorset.
+ if (set.name !== toolset.name) {
+ colors.set.toolset = toolset.name;
+ colors.saveCustom();
+ }
+ utils.applyObjectTo({ ...toolset, items: set.items }, set);
+ sendUpdate();
+ resolve(getResponseSet());
+});
+
+// Delete the a verified ID.
+export function deletePreset(id) {
+ set.items.delete(id);
+ sendUpdate();
+}
+
+// Add a validated tool.
+export const add = tool => new Promise((resolve, reject) => {
+ if (!set.items.get(tool.id)) {
+ set.items.set(tool.id, tool);
+ sendUpdate();
+ resolve(set.items.get(tool));
+ } else {
+ reject(
+ new Error(utils.singleLineString`Custom colorset tool with id
+ "${tool.id}" already exists, update it directly or choose a different id.`)
+ );
+ }
+});
+
+// Flatten bot tools to array.
+export function getBotTools() {
+ const botTools = botConf.get('tools');
+ const out = [];
+
+ Object.entries(botTools).forEach(([id, tool]) => {
+ out.push({
+ id,
+ parent: '',
+ ...tool,
+ x: parseFloat(tool.x),
+ y: parseFloat(tool.y),
+ width: tool.width ? parseFloat(tool.width) : 0,
+ height: tool.height ? parseFloat(tool.height) : 0,
+ });
+ });
+ return out;
+}
+
+// Get a flat array of tools.
+export function items() {
+ return [
+ ...getBotTools(),
+ ...colorsetTools(),
+ ];
+}
+
+export const getNames = () => items().map(({ id }) => id);
+
+// Get a single item, undefined if invalid.
+export const getItem = name => items().find(({ id }) => id === name);
+
+// Automatically set the internal "tools.set" from "colors.set.toolset".
+export function setFromColors() {
+ const preset = utils.getPreset('toolsets', colors.set.toolset);
+
+ utils.applyObjectTo({ ...preset, items: utils.arrayToIDMap(preset.items) }, set);
+}
+
+// Save changes to the current toolset
+export function saveCustom() {
+ // Special failover to prevent squashing empty "default".
+ if (set.name === 'default') {
+ set.name = 'default-custom';
+ colors.set.toolset = set.name;
+ colors.saveCustom();
+ }
+
+ utils.savePreset('toolsets', {
+ ...set,
+ items: utils.mapToArray(set.items),
+ });
+}
+
+/**
+ * Run the operation to change the current tool (and any aggregate operations
+ * required) into the buffer
+ *
+ * @param name
+ * The machine name of the tool (as defined in the bot config file).
+ * @param index
+ * Index for notifying user of what the manual tool change is for.
+ * @param callback
+ * Triggered when the full tool change is to have been completed, or on
+ * failure.
+ * @param waitForCompletion
+ * Pass false to call callback immediately after calculation, true to
+ * callback only after physical movement is complete.
+ *
+ * @returns {boolean}
+ * True if success, false on failure.
+ */
+export const changeTo = (
+ name, index = null, callback = () => { }, waitForCompletion = false
+) => {
+ // Get the matching tool object from the bot configuration.
+ const tool = getItem(name);
+
+ // No tool found with that name? Augh! Run AWAY!
+ if (!tool) {
+ run('callback', callback);
+ return false;
+ }
+
+ // For wait=false/"resume" tools, we really just resume the buffer.
+ // It should be noted, this is obviously NOT a queable toolchange.
+ // This should ONLY be called to restart the queue after a swap.
+ if (tool.wait !== undefined && tool.wait === false) {
+ resume();
+ callback(1);
+ return true;
+ }
+
+ // Pen Up
+ setHeight('up');
+
+ // Figure out the final position:
+ let toolPos = { x: tool.x, y: tool.y };
+
+ // Is there a parent? Offset for that.
+ const parent = getItem(tool.parent);
+ if (parent) {
+ toolPos.x += parseFloat(parent.x);
+ toolPos.y += parseFloat(parent.y);
+ }
+
+ // Convert MM to Abs steps.
+ toolPos = utils.absToSteps(toolPos, 'mm', true);
+
+ // Prevent out of bounds moves.
+ toolPos = utils.sanityCheckAbsoluteCoord(toolPos);
+
+ // Move to the tool
+ movePenAbs(toolPos);
+
+ // Set the tip of state pen to the tool now that the change is done.
+ forceState({ tool: name });
+
+ if (index && tool.wait) {
+ const { implement } = colors.getColor(index);
+ forceState({ implement, colorsetItem: index });
+ }
+
+ // Run force state in as the next step to be resumed.
+ /* if (index && tool.wait) {
+ console.log('BUFFERING implement forcestate ===============');
+ run('callback', () => {
+
+ console.log('RUNNING implement forcestate ===============', implement, index);
+ });
+ } */
+
+ // Trigger the binder event.
+ trigger('tool.change', { ...tool, index, name });
+
+ // Finish up.
+ if (waitForCompletion) { // Run inside the buffer
+ run('callback', callback);
+ } else { // Run as soon as items have been buffered
+ callback(1);
+ }
+
+ return true;
+};
+
+// Check for an ID.
+export function has(checkID) {
+ return !!items().filter(({ id }) => id === checkID).length;
+}
+
+// Bind to tools.update to redraw the tools layer.
+bindTo('tools.update', 'tools', () => {
+ const { layers } = base;
+ const toolGroup = new Group();
+
+ setFromColors();
+
+ const toolItems = items();
+
+ layers.tools.removeChildren();
+
+ // Create a representation path for each tool.
+ toolItems.forEach(tool => {
+ const toolPos = { x: tool.x, y: tool.y };
+
+ // Offset for center positions.
+ if (tool.position === 'center') {
+ toolPos.x -= tool.width / 2;
+ toolPos.y -= tool.height / 2;
+ }
+
+ // Apply parent offset.
+ const parent = getItem(tool.parent);
+ if (parent) {
+ toolPos.x += parent.x;
+ toolPos.y += parent.y;
+ }
+
+ // Don't try to display tools without size.
+ if (tool.width && tool.height) {
+ const path = new Path.Rectangle({
+ ...toolPos,
+ width: tool.width,
+ height: tool.height,
+ radius: tool.radius,
+ name: tool.id,
+ strokeWidth: 1,
+ strokeColor: 'black',
+ fillColor: colors.getToolColor(tool.id),
+ });
+
+ const label = new PointText({ fontSize: 8, content: tool.id, opacity: 0.5 });
+ label.fitBounds(path.bounds);
+ toolGroup.addChild(new Group([path, label]));
+ }
+ });
+
+ layers.tools.addChild(toolGroup);
+ sendPaperUpdate('tools');
+});
diff --git a/src/components/core/drawing/cncserver.drawing.accell.js b/src/components/core/drawing/cncserver.drawing.accell.js
index 4109e7db..38e03a83 100644
--- a/src/components/core/drawing/cncserver.drawing.accell.js
+++ b/src/components/core/drawing/cncserver.drawing.accell.js
@@ -1,20 +1,23 @@
/**
* @file Code for determining path drawing accelleration planning.
*/
-const zodiac = require('zodiac-ts');
+import zodiac from 'zodiac-ts';
+import { bindTo } from 'cs/binder';
-// Conglomerated feature export.
-const accell = { id: 'drawing.accell', state: 'idle' };
+export const state = {
+ renderStatus: 'idle',
+};
+
+// Time before work is sent to the callback for long operations.
+const splitTimeout = 2500;
// Path planning Settings
const s = {
- accelRate: 10, // Percentage increase over distance.
- speedMultiplyer: 0.55, // Conversion of moment length to velocity.
- minSpeed: 5,
+ accelRate: 25, // 10, // Percentage increase over distance.
+ speedMultiplyer: 0.75, // 0.55 // Conversion of moment length to velocity.
+ minSpeed: 15, // 5,
resolution: 0.5, // Steps to check along path by
- maxDeflection: 5,
- // Time before work is sent to the callback for long operations.
- splitTimeout: 2500,
+ maxDeflection: 10, // 5,
};
// Path planning:
@@ -40,7 +43,6 @@ function getCurvatureBetween(path, from, to, curvatureThreshold) {
// CLone the working path into memory.
let p = path.clone({ insert: false });
-
// Split the path at the from and to locations.
p = p.splitAt(from); // Returns the part after from.
@@ -90,13 +92,11 @@ function getCurvatureBetween(path, from, to, curvatureThreshold) {
return { curvature: startVector.getDirectedAngle(endVector), maxPointDistance };
}
-
function stepCalc(path, inputOffset) {
if (inputOffset > path.length) {
return null;
}
-
let offset = inputOffset;
if (inputOffset < 0) {
offset = 0;
@@ -133,125 +133,121 @@ function stepCalc(path, inputOffset) {
return { point, tangent, speed };
}
-module.exports = (cncserver, drawing) => {
- function getSmoothed(vals, rawResults) {
- const results = rawResults;
- try {
- const alpha = 0.3;
- const ses = new zodiac.SimpleExponentialSmoothing(vals, alpha);
- const forecast = ses.predict(0);
-
- // Repair forecast ends, always start/end on 0.
- forecast.pop();
- /*
- forecast[0] = 0;
- forecast[forecast.length - 1] = 0;
- */
-
- // Reinsert smoothed values back to results.
- forecast.forEach((smoothedSpeed, index) => {
- results[index].speed = Math.round(smoothedSpeed * 10) / 10;
- });
- } catch (error) {
- // Oh well.
- }
-
- return results;
+function getSmoothed(vals, rawResults) {
+ const results = rawResults;
+ try {
+ const alpha = 0.3;
+ const ses = new zodiac.SimpleExponentialSmoothing(vals, alpha);
+ const forecast = ses.predict(0);
+
+ // Repair forecast ends, always start/end on 0.
+ forecast.pop();
+ /*
+ forecast[0] = 0;
+ forecast[forecast.length - 1] = 0;
+ */
+
+ // Reinsert smoothed values back to results.
+ forecast.forEach((smoothedSpeed, index) => {
+ results[index].speed = Math.round(smoothedSpeed * 10) / 10;
+ });
+ } catch (error) {
+ // Oh well.
}
- // Allow external cancelling of accell process.
- accell.cancel = () => {
- accell.state = 'idle';
- };
+ return results;
+}
- // Bind to Cancel.
- cncserver.binder.bindTo('buffer.clear', accell.id, accell.cancel);
+// Allow external cancelling of accell process.
+export function cancel() {
+ state.renderStatus = 'idle';
+}
- // SYNC - Get and return a list of accell points directly.
- // WARNING: Will entirely block event loop on long paths.
- accell.getPointsSync = (path) => {
- // Accell should only be doing work on one path at a time.
- if (accell.state !== 'idle') {
- throw new Error('Can only accell one path at a time.');
- }
+// Bind to Cancel.
+bindTo('buffer.clear', 'drawing.accell', cancel);
- accell.state = 'processing';
- const results = [];
- const vals = [];
- const traverseLength = path.length + s.resolution;
+// SYNC - Get and return a list of accell points directly.
+// WARNING: Will entirely block event loop on long paths.
+export function getPointsSync(path) {
+ // Accell should only be doing work on one path at a time.
+ if (state.renderStatus !== 'idle') {
+ throw new Error('Can only accell one path at a time.');
+ }
- // Reset speed follow soft global.
- speed = 0;
- for (let offset = 0; offset <= traverseLength; offset += s.resolution) {
- const v = stepCalc(path, offset);
- if (v) {
- vals.push(v.speed);
- results.push(v);
- }
+ state.renderStatus = 'processing';
+ const results = [];
+ const vals = [];
+ const traverseLength = path.length + s.resolution;
+
+ // Reset speed follow soft global.
+ speed = 0;
+ for (let offset = 0; offset <= traverseLength; offset += s.resolution) {
+ const v = stepCalc(path, offset);
+ if (v) {
+ vals.push(v.speed);
+ results.push(v);
}
+ }
- accell.state = 'idle';
- return getSmoothed(vals, results);
- };
-
- // ASYNC - Get a list of accelleration points, splitting the work up into batches.
- accell.getPoints = (path, resultCallback) => {
- // Accell should only be doing work on one path at a time.
- if (accell.state !== 'idle') {
- throw new Error('Can only accell one path at a time.');
- }
+ state.renderStatus = 'idle';
+ return getSmoothed(vals, results);
+}
- speed = 0; // Reset speed follow soft global.
+// ASYNC - Get a list of accelleration points, splitting the work up into batches.
+export function getPoints(path, resultCallback) {
+ // Accell should only be doing work on one path at a time.
+ if (state.renderStatus !== 'idle') {
+ throw new Error('Can only accell one path at a time.');
+ }
- let splitTimer = new Date();
- const results = [];
- const vals = [];
- const traverseLength = path.length + s.resolution;
- accell.state = 'processing';
+ speed = 0; // Reset speed follow soft global.
- // Start range of where to return results.
- let returnResultIndex = 0;
+ let splitTimer = new Date();
+ const results = [];
+ const vals = [];
+ const traverseLength = path.length + s.resolution;
+ state.renderStatus = 'processing';
- let offset = 0;
- const nextOffset = () => {
- // If the state changes here then processing has been canceled.
- if (accell.state === 'idle') {
- resultCallback(null);
- return;
- }
+ // Start range of where to return results.
+ let returnResultIndex = 0;
- // Process along the offset in the path.
- if (offset <= traverseLength) {
- const v = stepCalc(path, offset);
- if (v) {
- vals.push(v.speed);
- results.push(v);
- }
+ let offset = 0;
+ const nextOffset = () => {
+ // If the state changes here then processing has been canceled.
+ if (state.renderStatus === 'idle') {
+ resultCallback(null);
+ return;
+ }
- // Is this taking too long? Split the work up.
- if (new Date() - splitTimer > s.splitTimeout) {
- resultCallback(
- getSmoothed(vals, results).slice(returnResultIndex)
- );
- returnResultIndex = vals.length;
- splitTimer = new Date();
- }
- offset += s.resolution;
- setTimeout(nextOffset, 0);
- } else {
- accell.state = 'idle';
+ // Process along the offset in the path.
+ if (offset <= traverseLength) {
+ const v = stepCalc(path, offset);
+ if (v) {
+ vals.push(v.speed);
+ results.push(v);
+ }
- // We're completely done with the path.
+ // Is this taking too long? Split the work up.
+ if (new Date() - splitTimer > splitTimeout) {
resultCallback(
getSmoothed(vals, results).slice(returnResultIndex)
);
- resultCallback([]);
+ returnResultIndex = vals.length;
+ splitTimer = new Date();
}
- };
-
- // Initialize getting the first offset.
- setTimeout(nextOffset, 0);
+ offset += s.resolution;
+ setTimeout(nextOffset, 0);
+ } else {
+ state.renderStatus = 'idle';
+
+ // We're completely done with the path.
+ resultCallback(
+ getSmoothed(vals, results).slice(returnResultIndex)
+ );
+ resultCallback([]);
+ }
};
- return accell;
-};
+ // Initialize getting the first offset.
+ setTimeout(nextOffset, 0);
+}
diff --git a/src/components/core/drawing/cncserver.drawing.base.js b/src/components/core/drawing/cncserver.drawing.base.js
index 0110c621..f3f08b6c 100644
--- a/src/components/core/drawing/cncserver.drawing.base.js
+++ b/src/components/core/drawing/cncserver.drawing.base.js
@@ -1,310 +1,341 @@
/**
* @file Drawing base code used by other drawing utils.
*/
-const {
- Point, Size, Project, Rectangle, Group, Path, CompoundPath, Layer,
-} = require('paper');
-
-// Central drawing base export
-const base = { id: 'drawing.base', layers: {} };
-module.exports = (cncserver) => {
- base.project = {};
-
- cncserver.binder.bindTo('controller.setup', base.id, () => {
- const { settings: { bot } } = cncserver;
- // Setup the project with the max cavas size in mm.
- base.size = new Size(bot.maxAreaMM.width, bot.maxAreaMM.height);
-
- // Setup the actual printable work space as a rectangle.
- base.workspace = new Rectangle({
- from: [bot.workAreaMM.left, bot.workAreaMM.top],
- to: [bot.workAreaMM.right, bot.workAreaMM.bottom],
- });
+// Paper does everything with getters and settings attached to object params.
+/* eslint-disable no-param-reassign */
- base.project = new Project(base.size);
+import Paper from 'paper';
+import { trigger, bindTo } from 'cs/binder';
+import { bot } from 'cs/settings';
+import { sendPaperUpdate } from 'cs/sockets';
- // Setup layers: temp, working
- // Whatever the last layer added was, will be default.
- const layers = ['import', 'temp', 'stage', 'preview'];
- layers.forEach((name) => {
- base.layers[name] = new Layer({ name });
- });
+const {
+ Point, Size, Project, Rectangle, Group, Path, CompoundPath, Layer,
+} = Paper;
- // Trigger paper ready.
- cncserver.binder.trigger('paper.ready');
- });
+// Central drawing base export
+const bindID = 'drawing.base';
- // Clear preview canvas on cancel/clear.
- cncserver.binder.bindTo('buffer.clear', base.id, () => {
- base.layers.preview.removeChildren();
- cncserver.sockets.sendPaperUpdate('preview');
+export const layers = {};
+export const workspace = {};
+export const state = {
+ size: {},
+ project: {},
+};
- // TODO: We likely don't want to do this here, but it helps for now.
- base.layers.stage.removeChildren();
- cncserver.sockets.sendPaperUpdate('stage');
+bindTo('schemas.loaded', bindID, () => {
+ // Setup the project with the max cavas size in mm.
+ state.size = new Size(bot.maxAreaMM.width, bot.maxAreaMM.height);
+
+ // Setup the actual printable work space as a rectangle.
+ workspace.left = bot.workAreaMM.left;
+ workspace.top = bot.workAreaMM.top;
+ workspace.bottom = bot.workAreaMM.bottom;
+ workspace.right = bot.workAreaMM.right;
+
+ state.project = new Project(state.size);
+
+ // Setup layers:
+ // Whatever the last layer added was, will be default.
+ const createLayers = [
+ 'import', // Raw content, cleared on each import.
+ 'temp', // Temporary working space, cleared before each operation.
+ 'stage', // Project imported groups of items.
+ 'tools', // Helper visualization of tool positions.
+ 'preview', // Render destination, item groups of lines w/color data only (no fills).
+ 'print', // Final print source, grouped by colorset work groupings.
+ ];
+ createLayers.forEach(name => {
+ layers[name] = new Layer({ name });
});
- // Get a list of all simple paths from all children as an array.
- base.getPaths = (parent = base.layers.preview, items = []) => {
- if (parent.children && parent.children.length && !(parent instanceof CompoundPath)) {
- let moreItems = [];
- parent.children.forEach((child) => {
- moreItems = base.getPaths(child, moreItems);
- });
- return [...items, ...moreItems];
- }
- return [...items, parent];
- };
-
- // Just the object for the rectangle (for JSON).
- base.defaultBoundsRaw = (margin = 10) => ({
+ // Trigger paper ready.
+ trigger('paper.ready', null, true);
+});
+
+// Clear preview canvas on cancel/clear.
+bindTo('buffer.clear', bindID, () => {
+ layers.preview.removeChildren();
+ sendPaperUpdate('preview');
+
+ // TODO: We likely don't want to do this here, but it helps for now.
+ layers.stage.removeChildren();
+ sendPaperUpdate('stage');
+});
+
+// Get a list of all simple paths from all children as an array.
+export function getPaths(parent = layers.preview, items = []) {
+ if (parent.children) {
+ let moreItems = [];
+ parent.children.forEach(child => {
+ moreItems = getPaths(child, moreItems);
+ });
+ return [...items, ...moreItems];
+ }
+ return [...items, parent];
+}
+
+// Just the object for the rectangle (for JSON).
+export function defaultBoundsRaw(margin = 10) {
+ return {
x: margin,
y: margin,
- width: base.workspace.width - margin * 2,
- height: base.workspace.height - margin * 2,
- });
-
- // Get a default bound for high level drawings.
- base.defaultBounds = margin => new Rectangle(base.defaultBoundsRaw(margin));
-
- // Get the snapped stroke color ID of an item through its parentage.
- base.getColorID = (item) => {
- if (item.data.colorID) return item.data.colorID;
- if (item.parent) return base.getColorID(item.parent);
- return null;
- };
-
- // Offset any passed bounds to fit within the workspace.
- base.fitToWorkspace = (bounds) => {
- const adjBounds = bounds.clone();
-
- // No negative bounds positions.
- if (adjBounds.point.x < 0) adjBounds.point.x = 0;
- if (adjBounds.point.y < 0) adjBounds.point.y = 0;
-
- // Offset for top/left workspaces.
- adjBounds.point = adjBounds.point.add(base.workspace);
-
- // Keep width/height from overflowing.
- if (adjBounds.right > base.workspace.right) {
- // console.log('Too far right!', adjBounds, adjBounds.right - base.workspace.right);
- adjBounds.width -= adjBounds.right - base.workspace.right;
- }
-
- if (adjBounds.bottom > base.workspace.bottom) {
- adjBounds.height -= adjBounds.bottom - base.workspace.bottom;
- // console.log('Too far down!', adjBounds);
- }
-
- return adjBounds;
+ width: workspace.width - margin * 2,
+ height: workspace.height - margin * 2,
};
-
- // Verify a set of given bounds.
- base.validateBounds = (rawBounds) => {
- let bounds = rawBounds;
-
- if (!bounds) {
- bounds = base.defaultBounds();
- } else if (!(bounds instanceof Rectangle)) {
- bounds = new Rectangle(bounds);
- }
-
- return base.fitToWorkspace(bounds);
- };
-
- // Fit the given item within either the drawing bounds, or custom one.
- base.fitBounds = (item, rawBounds) => {
- const bounds = base.validateBounds(rawBounds);
- item.fitBounds(bounds);
- return bounds;
- };
-
- // Get the position of item via anchor from relative offset EG {x:0, y:-1}
- base.getAnchorPos = (item, anchor, relative = true) => {
- const { bounds } = item;
- const halfW = bounds.width / 2;
- const halfH = bounds.height / 2;
- let center = { x: 0, y: 0 };
-
- if (!relative) {
- center = bounds.center;
- }
-
- return new Point({
- x: center.x + (halfW * anchor.x),
- y: center.y + (halfH * anchor.y),
+}
+
+// Get a default bound for high level drawings.
+export const defaultBounds = margin => new Rectangle(defaultBoundsRaw(margin));
+
+// Get the snapped stroke color ID of an item through its parentage.
+export function getColorID(item) {
+ if (item.data.colorID) return item.data.colorID;
+ if (item.parent) return getColorID(item.parent);
+ return null;
+}
+
+// Offset any passed bounds to fit within the workspace.
+export function fitToWorkspace(bounds) {
+ const adjBounds = bounds.clone();
+
+ // No negative bounds positions.
+ if (adjBounds.point.x < 0) adjBounds.point.x = 0;
+ if (adjBounds.point.y < 0) adjBounds.point.y = 0;
+
+ // Offset for top/left workspaces.
+ adjBounds.point = adjBounds.point.add(workspace);
+
+ // Keep width/height from overflowing.
+ if (adjBounds.right > workspace.right) {
+ // console.log('Too far right!', adjBounds, adjBounds.right - workspace.right);
+ adjBounds.width -= adjBounds.right - workspace.right;
+ }
+
+ if (adjBounds.bottom > workspace.bottom) {
+ adjBounds.height -= adjBounds.bottom - workspace.bottom;
+ // console.log('Too far down!', adjBounds);
+ }
+
+ return adjBounds;
+}
+
+// Verify a set of given bounds.
+export function validateBounds(rawBounds) {
+ let bounds = rawBounds;
+
+ if (!bounds) {
+ bounds = defaultBounds();
+ } else if (!(bounds instanceof Rectangle)) {
+ bounds = new Rectangle(bounds);
+ }
+
+ return fitToWorkspace(bounds);
+}
+
+// Fit the given item within either the drawing bounds, or custom one.
+export function fitBounds(item, rawBounds) {
+ const bounds = validateBounds(rawBounds);
+ item.fitBounds(bounds);
+ return bounds;
+}
+
+// Get the position of item via anchor from relative offset EG {x:0, y:-1}
+export function getAnchorPos(item, anchor, relative = true) {
+ const { bounds } = item;
+ const halfW = bounds.width / 2;
+ const halfH = bounds.height / 2;
+ let center = { x: 0, y: 0 };
+
+ if (!relative) {
+ center = bounds.center;
+ }
+
+ return new Point({
+ x: center.x + (halfW * anchor.x),
+ y: center.y + (halfH * anchor.y),
+ });
+}
+
+// Set the position of item via anchor from relative offset EG {x:0, y:-1}
+export function setPosFromAnchor(item, position, anchor) {
+ const offset = getAnchorPos(item, anchor);
+
+ // eslint-disable-next-line no-param-reassign
+ item.position = position.subtract(offset);
+}
+
+// Return true if the layer contains any groups at the top level
+export function layerContainsGroups(layer = state.project.activeLayer) {
+ for (const i in layer.children) {
+ if (layer.children[i] instanceof Group) return true;
+ }
+ return false;
+}
+
+// Ungroup any groups recursively
+export function ungroupAllGroups(layer = state.project.activeLayer) {
+ // Remove all groups
+ while (layerContainsGroups(layer)) {
+ layer.children.forEach(path => {
+ if (path instanceof Group) {
+ path.parent.insertChildren(0, path.removeChildren());
+ path.remove();
+ }
});
- };
-
- // Set the position of item via anchor from relative offset EG {x:0, y:-1}
- base.setPosFromAnchor = (item, position, anchor) => {
- const offset = base.getAnchorPos(item, anchor);
-
- item.position = position.subtract(offset);
- };
-
- // Return true if the layer contains any groups at the top level
- base.layerContainsGroups = (layer = base.project.activeLayer) => {
- for (const i in layer.children) {
- if (layer.children[i] instanceof Group) return true;
- }
- return false;
- };
-
- // Ungroup any groups recursively
- base.ungroupAllGroups = (layer = base.project.activeLayer) => {
- // Remove all groups
- while (base.layerContainsGroups(layer)) {
- layer.children.forEach((path) => {
- if (path instanceof Group) {
- path.parent.insertChildren(0, path.removeChildren());
- path.remove();
- }
- });
- }
- };
-
- // Standardize path names to ensure everything has one.
- base.setName = (item) => {
- // eslint-disable-next-line no-param-reassign
- item.name = item.name || `draw_path_${item.id}`;
- return item;
- };
-
- // Simple helper to check if the path is one of the parsable types.
- base.isDrawable = item => item instanceof Path || item instanceof CompoundPath;
-
- // Normalize a 'd' string, JSON or path input into a compound path.
- base.normalizeCompoundPath = (importPath) => {
- let path = importPath;
-
- // Attempt to detect 'd' string or JSON import.
- if (typeof path === 'string') {
- if (path.includes('{')) {
- // If this fails the paper error will bubble up to the implementor.
- path = cncserver.drawing.base.layers.temp.importJSON(path);
+ }
+}
+
+// SVG content can have paths with NO fill or strokes, they're assumed to be black fill.
+export function validateFills(item) {
+ item.children.forEach(child => {
+ if (!child.fillColor && !child.strokeColor) {
+ if (child.children && child.children.length) {
+ validateFills(child);
} else {
- // D string, create the compound path directly
- return base.setName(new CompoundPath(path));
+ // TODO: This likely needs more rules for cleanup.
+ child.fillColor = 'black';
}
}
-
- // If we don't have a path at this point we failed to import the JSON.
- if (!path || !path.length) {
- throw new Error('Invalid path source, verify input content and try again.');
- }
-
- // If the passed object already is compounnd, return it directly.
- if (path instanceof CompoundPath) {
- return base.setName(path);
+ });
+}
+
+// Standardize path names to ensure everything has one.
+export function setName(item) {
+ // eslint-disable-next-line no-param-reassign
+ item.name = item.name || `draw_path_${item.id}`;
+ return item;
+}
+
+// Simple helper to check if the path is one of the parsable types.
+export const isDrawable = item => item instanceof Path || item instanceof CompoundPath;
+
+// Normalize a 'd' string, JSON or path input into a compound path.
+export function normalizeCompoundPath(importPath) {
+ let path = importPath;
+
+ // Attempt to detect 'd' string or JSON import.
+ if (typeof path === 'string') {
+ if (path.includes('{')) {
+ // If this fails the paper error will bubble up to the implementor.
+ path = layers.temp.importJSON(path);
+ } else {
+ // D string, create the compound path directly
+ return setName(new CompoundPath(path));
}
-
- // Standard path, create a compound path from it.
- return base.setName(new CompoundPath({
- children: [path],
- fillColor: path.fillColor,
- strokeColor: path.strokeColor,
- }));
- };
-
- // Check if a fill/stroke color is "real".
- // Returns True if not null or fully transparent.
- base.hasColor = (color) => {
- if (!color) {
- return false;
+ }
+
+ // If we don't have a path at this point we failed to import the JSON.
+ if (!path || !path.length) {
+ throw new Error('Invalid path source, verify input content and try again.');
+ }
+
+ // If the passed object already is compounnd, return it directly.
+ if (path instanceof CompoundPath) {
+ return setName(path);
+ }
+
+ // Standard path, create a compound path from it.
+ return setName(new CompoundPath({
+ children: [path],
+ fillColor: path.fillColor,
+ strokeColor: path.strokeColor,
+ }));
+}
+
+// Check if a fill/stroke color is "real".
+// Returns True if not null or fully transparent.
+export function hasColor(color) {
+ if (!color) {
+ return false;
+ }
+ return color.alpha !== 0;
+}
+
+// At this point, simply removing anything that isn't a path.
+export function removeNonPaths(layer = state.project.activeLayer) {
+ // If you modify the child list, you MUST operate on a COPY
+ const kids = [...layer.children];
+ kids.forEach(path => {
+ if (!isDrawable(path)) {
+ path.remove();
+ } else if (!path.length) {
+ path.remove();
+ } else {
+ setName(path);
}
- return color.alpha !== 0;
- };
-
- // Takes an item with children, and cleans up all the first level children to
- // verify they have either: Stroke with no fill, stroke with fill (closed),
- // fill only. Removes or fixes all paths that don't fit here.
- base.cleanupInput = (layer, settings) => {
- const { trace: fillStroke } = settings.fill;
- const { hasColor } = base;
- base.removeNonPaths(layer);
-
- const kids = [...layer.children];
- kids.forEach((item) => {
- const { name } = item;
-
- // Maybe a fill?
- if (hasColor(item.fillColor)) {
- // console.log('Fill', item.name, item.fillColor.toCSS());
- item.closed = true;
- item.fillColor.alpha = 1;
-
- // Has a stroke?
- if (hasColor(item.strokeColor) || item.strokeWidth) {
- if (!hasColor(item.strokeColor)) {
- item.strokeColor = item.fillColor || settings.path.fillColor;
- item.strokeColor.alpha = 1;
- }
- if (!item.strokeWidth) {
- item.strokeWidth = 1;
- }
- } else if (fillStroke) {
- // Add a stroke to fills if requested.
- // console.log('Adding stroke to fill', item.name, item.fillColor.toCSS());
- item.strokeColor = item.fillColor;
- item.strokeWidth = 1;
- }
- } else if (hasColor(item.strokeColor) || item.strokeWidth) {
+ });
+}
+
+// Takes an item with children, and cleans up all the first level children to
+// verify they have either: Stroke with no fill, stroke with fill (closed),
+// fill only. Removes or fixes all paths that don't fit here.
+export function cleanupInput(layer, settings) {
+ const { trace: fillStroke } = settings.fill;
+ removeNonPaths(layer);
+
+ const kids = [...layer.children];
+ kids.forEach(item => {
+ // const { name } = item;
+
+ // Maybe a fill?
+ if (hasColor(item.fillColor)) {
+ // console.log('Fill', item.name, item.fillColor.toCSS());
+ item.closed = true;
+ item.fillColor.alpha = 1;
+
+ // Has a stroke?
+ if (hasColor(item.strokeColor) || item.strokeWidth) {
if (!hasColor(item.strokeColor)) {
item.strokeColor = item.fillColor || settings.path.fillColor;
- } else {
item.strokeColor.alpha = 1;
}
-
if (!item.strokeWidth) {
item.strokeWidth = 1;
}
- } else {
- console.log('Removing No fill/stroke', item.name);
- item.remove();
+ } else if (fillStroke) {
+ // Add a stroke to fills if requested.
+ // console.log('Adding stroke to fill', item.name, item.fillColor.toCSS());
+ item.strokeColor = item.fillColor;
+ item.strokeWidth = 1;
}
- });
- };
-
- base.setPathOption = (path, options) => {
- Object.entries(options).forEach(([key, value]) => {
- path[key] = value;
- });
- };
-
- // Prepare a layer to remove anything that isn't a fill.
- base.cleanupFills = (tmp) => {
- const kids = [...tmp.children];
- kids.forEach((path) => {
- if (!base.hasColor(path.fillColor)) {
- path.remove();
+ } else if (hasColor(item.strokeColor) || item.strokeWidth) {
+ if (!hasColor(item.strokeColor)) {
+ item.strokeColor = item.fillColor || settings.path.fillColor;
} else {
- // Bulk set path options.
- base.setPathOption(path, {
- closed: true,
- strokeWidth: 0,
- strokeColor: null,
- });
+ item.strokeColor.alpha = 1;
}
- });
- };
- // At this point, simply removing anything that isn't a path.
- base.removeNonPaths = (layer = base.project.activeLayer) => {
- // If you modify the child list, you MUST operate on a COPY
- const kids = [...layer.children];
- kids.forEach((path) => {
- if (!base.isDrawable(path)) {
- path.remove();
- } else if (!path.length) {
- path.remove();
- } else {
- base.setName(path);
+ if (!item.strokeWidth) {
+ item.strokeWidth = 1;
}
- });
- };
-
+ } else {
+ console.log('Removing No fill/stroke', item.name);
+ item.remove();
+ }
+ });
+}
- return base;
-};
+export function setPathOption(path, options) {
+ Object.entries(options).forEach(([key, value]) => {
+ path[key] = value;
+ });
+}
+
+// Prepare a layer to remove anything that isn't a fill.
+export function cleanupFills(tmp) {
+ const kids = [...tmp.children];
+ kids.forEach(path => {
+ if (!hasColor(path.fillColor)) {
+ path.remove();
+ } else {
+ // Bulk set path options.
+ setPathOption(path, {
+ closed: true,
+ strokeWidth: 0,
+ strokeColor: null,
+ });
+ }
+ });
+}
diff --git a/src/components/core/drawing/cncserver.drawing.colors.js b/src/components/core/drawing/cncserver.drawing.colors.js
index 20251e14..383540d6 100644
--- a/src/components/core/drawing/cncserver.drawing.colors.js
+++ b/src/components/core/drawing/cncserver.drawing.colors.js
@@ -1,247 +1,391 @@
/**
- * @file Code for drawing color and "tool" change management.
- */
-const glob = require('glob');
-const path = require('path');
-const nc = require('nearest-color');
-const { Color } = require('paper');
-
-const defaultColor = { id: 'color0', name: 'Black', color: '#000000' };
-const ignoreWhite = { id: 'ignore', name: 'White', color: '#FFFFFF' };
-const defaultPreset = {
- manufacturer: 'default',
- media: 'pen',
- machineName: 'default',
- weight: -10,
- colors: { black: '#000000' },
+* @file Code for drawing color and "tool" change management.
+*/
+
+// Paper.js is all about that parameter reassignment life.
+/* eslint-disable no-param-reassign */
+
+import Paper from 'paper';
+import chroma from 'chroma-js';
+import { get as getImplement, IMPLEMENT_PARENT } from 'cs/drawing/implements';
+import * as utils from 'cs/utils';
+import * as tools from 'cs/tools';
+import * as projects from 'cs/projects';
+import * as matcher from 'cs/drawing/colors/matcher';
+import { layers } from 'cs/drawing/base';
+import { bindTo, trigger } from 'cs/binder';
+import { getDataDefault } from 'cs/schemas';
+import { sendPaperUpdate } from 'cs/sockets';
+
+const { Color, Group } = Paper;
+
+const DEFAULT_PRESET = 'default-single-pen';
+const IGNORE_ITEM = '[IGNORE]';
+const bindID = 'drawing.colors';
+
+export const set = { // Set via initial preset or colorset loader.
+ name: '', // Machine name for loading/folder storage
+ title: '', // Clean name.
+ description: '', // Description of what it is beyond title.
+ implement: '', // Implement preset string.
+ items: new Map(), // Items mapped by id.
+ toolset: '', // Extra Toolset by machine name.
};
-const colors = { id: 'drawing.colors', presets: { default: defaultPreset }, set: [] };
-const presetDir = path.resolve(
- global.__basedir, 'components', 'core', 'drawing', 'colorsets'
-);
-
-module.exports = (cncserver, drawing) => {
- // What is this and why?
- //
- // When we draw, we assume a color: color0. It's assumed this is "black", but
- // if there's only one color in use, no color switching will occur so this
- // definition is moot unless we have more than one color in our set.
- //
- // A "colorset" is a set of colors that can be applied to the available colors
- // in `cncserver.drawing.colors.set`
-
- // Load all color presets into the presets key.
- const files = glob.sync(path.join(presetDir, '*.json'));
- files.forEach((setPath) => {
- try {
- // eslint-disable-next-line global-require, import/no-dynamic-require
- const sets = require(setPath);
- sets.forEach((set) => {
- const key = `${set.manufacturer}-${set.media}-${set.machineName}`;
- colors.presets[key] = set;
- });
- } catch (error) {
- console.error(`Problem loading color preset: '${setPath}'`);
- }
- });
- // Function to render presets for the API, translating human readable strings.
- colors.listPresets = (t) => {
- const out = {};
- Object.entries(colors.presets).forEach(([key, p]) => {
- const basekey = `colorsets:${p.manufacturer}.${p.machineName}`;
- out[key] = {
- manufacturer: p.manufacturer,
- media: p.media,
- machineName: p.machineName,
- manufacturerName: t(`${basekey}.manufacturer`),
- name: t(`${basekey}.name`),
- description: t(`${basekey}.description`),
- mediaName: t(`colorsets:media.${p.media}`),
- colors: {},
- };
-
- Object.entries(p.colors).forEach(([id, color]) => {
- out[key].colors[id] = {
- color,
- name: t(`colorsets:colors.${id}`),
- };
- });
- });
+// Setup matcher Chroma library, colorset and project settings.
+matcher.setup({ chroma });
- return out;
- };
+bindTo('colors.update', bindID, colorset => {
+ matcher.setup({ colorset });
+});
- colors.getIDs = () => {
- const items = [];
- colors.set.forEach(({ id }) => {
- items.push(id);
- });
- return items;
- };
+bindTo('projects.update', bindID, ({ options }) => {
+ matcher.setup({ options });
+});
- colors.getIndex = (findID) => {
- let findIndex = null;
- colors.set.forEach(({ id }, index) => {
- if (id === findID) {
- findIndex = index;
- }
+// Fully translate a given non-map based colorset.
+export function translateSet(inputSet, t = tx => tx) {
+ let transSet = null;
+ if (inputSet) {
+ transSet = { ...inputSet };
+ transSet.title = t(transSet.title);
+ transSet.description = t(transSet.description);
+ transSet.manufacturer = t(transSet.manufacturer);
+
+ transSet.items.forEach(item => {
+ item.name = t(item.name);
});
- return findIndex;
- };
+ }
- colors.delete = ({ id }) => {
- const index = colors.getIndex(id);
- colors.set.splice(index, 1);
+ return transSet;
+}
- if (colors.set.length === 0) {
- colors.set.push(defaultColor);
- }
- cncserver.sockets.sendPaperUpdate();
- };
+// Render presets for the API, translating human readable strings.
+export function listPresets(t) {
+ const sets = Object.values(utils.getPresets('colorsets'));
+ const sorted = sets.sort((a, b) => (a.sortWeight > b.sortWeight ? 1 : -1));
+ const out = {};
- colors.add = ({ id, name, color }) => {
- if (colors.getIndex(id) === null) {
- colors.set.push({ id, name, color });
- cncserver.sockets.sendPaperUpdate();
- return true;
- }
- return null;
- };
+ // Translate strings.
+ sorted.forEach(sortSet => {
+ out[sortSet.name] = translateSet(sortSet, t);
+ });
- colors.update = (id, { name, color }) => {
- const index = colors.getIndex(id);
- colors.set[index] = { id, name, color };
- cncserver.sockets.sendPaperUpdate();
- return colors.set[index];
- };
+ return out;
+}
- colors.getColor = (findID) => {
- let color = colors.getIndex(findID);
- if (color !== null) color = colors.set[color];
- return color;
- };
+// List custom/overridden machine names.
+export function customKeys() {
+ return Object.keys(utils.getCustomPresets('colorsets'));
+}
- /**
- * Mutate the set array to match a preset by machine name.
- *
- * @param {string} presetName
- *
- * @returns {boolean}
- * Null for failure, true if success.
- */
- colors.applyPreset = (presetName) => {
- const preset = colors.setFromPreset(presetName);
- if (preset) {
- colors.set = preset;
- cncserver.sockets.sendPaperUpdate();
- return true;
- }
- return null;
- };
+// List internal machine names.
+export function internalKeys() {
+ return Object.keys(utils.getInternalPresets('colorsets'));
+}
- /**
- * Get colorset array from a preset name.
- *
- * @param {string} presetName
- *
- * @returns {array}
- * Colorset style array with default toolnames
- */
- colors.setFromPreset = (presetName) => {
- if (colors.presets[presetName]) {
- const set = [];
- Object.entries(colors.presets[presetName].colors).forEach(([name, color]) => {
- set.push({
- id: `color${set.length}`,
- name, // cncserver.i18n.t(`colorsets:colors.${name}`),
- color,
- });
- });
-
- // TODO: Allow this to be set somewhere?
- set.push({
- ...ignoreWhite,
- // name: cncserver.i18n.t('colorsets:colors.white'),
- });
- return set;
+// Object of preset keys with colorset toolset tool parents unavailable to this bot.
+export function invalidPresets() {
+ const out = {};
+ const sets = listPresets();
+ const invalidToolsets = tools.invalidPresets();
+
+ // Move through all sets, report invalid toolsets
+ Object.entries(sets).forEach(([name, { toolset }]) => {
+ if (toolset in invalidToolsets) {
+ out[name] = invalidToolsets[toolset];
}
+ });
- return null;
- };
+ return out;
+}
- /**
- * Run at setup, allows machine specific colorset defaults.
- */
- colors.setDefault = () => {
- const defaultSet = cncserver.binder.trigger('colors.setDefault', [defaultColor, ignoreWhite]);
- colors.set = defaultSet;
+// Take in a colorset and convert the items/tools to a map
+export function setAsMap(mapSet = {}) {
+ return {
+ ...mapSet,
+ items: utils.arrayToIDMap(mapSet.items),
};
+}
- // Bind to when bot/controller is configured and setup, set default.
- cncserver.binder.bindTo('controller.setup', colors.id, () => {
- colors.setDefault();
+// Return a flat array version of the set.
+export function setAsArray(arrSet = set) {
+ return getDataDefault('colors', {
+ ...arrSet,
+ items: Array.from(arrSet.items.values()),
});
+}
+
+// Save custom from set.
+export function saveCustom() {
+ const arrColorSet = setAsArray();
+ trigger('colors.update', arrColorSet);
+ utils.savePreset('colorsets', arrColorSet);
+}
+
+/**
+ * Get a flat list of valid colorset key ids.
+ *
+ * @returns {array}
+ * Array of colorset item keys, empty array if none.
+ */
+export const getIDs = () => Array.from(set.items.keys());
+
+// Make changes to the colorset object.
+export const editSet = changedSet => new Promise(resolve => {
+ delete changedSet.items;
+
+ // Enforce clean machine name.
+ changedSet.name = utils.getMachineName(changedSet.name, 64);
+
+ utils.applyObjectTo(changedSet, set);
+ saveCustom();
+ resolve();
+});
+
+// Load and return a correctly mapped colorset from a preset name.
+// Pass translate function to rewrite translatable fields.
+export function getPreset(presetName, t) {
+ const getSet = translateSet(utils.getPreset('colorsets', presetName), t);
+ return getSet ? setAsMap(getSet) : getSet;
+}
+
+// Edit an item.
+export const edit = item => new Promise(resolve => {
+ const { id } = item;
+ set.items.set(id, item);
+ sendPaperUpdate();
+ saveCustom();
+ resolve(set.items.get(id));
+});
+
+// Delete a color by id.
+export function deleteColor(id) {
+ set.items.delete(id);
+
+ if (set.items.size === 0) {
+ // Load in the default 1 color preset.
+ const color = getPreset(DEFAULT_PRESET).items.get('color0');
+ set.items.set(color.id, color);
+ }
+ sendPaperUpdate();
+ saveCustom();
+}
+
+// Add a color by all of its info, appended to the end.
+export const add = ({ id, ...item }) => new Promise((resolve, reject) => {
+ id = utils.getMachineName(id, 64);
+ if (!getColor(id)) {
+ set.items.set(id, { id, ...item });
+ sendPaperUpdate();
+ saveCustom();
+ resolve();
+ } else {
+ reject(
+ new Error(`Color with id "${id}" already exists, update it directly or change id`)
+ );
+ }
+});
+
+// Get a renderable color from a tool name. 'transparent' if no match.
+export function getToolColor(name) {
+ const item = set.items.get(name);
+ return item ? item.color : 'transparent';
+}
+
+// Get the full implement object for a given colorset item id.
+export function getItemImplement(id) {
+ const item = set.items.get(id);
+ const preset = item.implement === IMPLEMENT_PARENT
+ ? set.implement : item.implement;
+ return getImplement(preset);
+}
+
+// Get non-reference copy of colorset item by id.
+export function getColor(id, applyInheritance = false, withImplement = false) {
+ const rawItem = set.items.get(id);
+ let item = null;
- // Figure out if we should even parse color work
- colors.doColorParsing = () => {
- const items = {};
- colors.set.forEach((item) => {
- if (item.id !== 'ignore') {
- items[item.id] = true;
+ if (rawItem) {
+ item = { ...rawItem };
+
+ // If the implementor wants it, and the item wants inheritance...
+ if (item.implement === IMPLEMENT_PARENT) {
+ if (applyInheritance) {
+ item.implement = withImplement ? getImplement(set.implement) : set.implement;
}
- });
- return Object.keys(items).length > 1;
- };
+ } else if (withImplement) {
+ item.implement = getImplement(item.implement);
+ }
+ }
+
+ return item;
+}
+
+/**
+ * Mutate the set object to match a preset by machine name.
+ *
+ * @param {string} presetName
+ *
+ * @returns {Promise}
+ * Rejects on failure, resolves on success (no return value).
+ */
+export const applyPreset = (presetName, t) => new Promise((resolve, reject) => {
+ const newSet = getPreset(presetName);
+ if (newSet?.items) {
+ utils.applyObjectTo(newSet, set);
+ tools.sendUpdate();
+ trigger('colors.update', setAsArray());
+ projects.setColorset(presetName);
+ resolve();
+ } else {
+ const err = new Error(
+ utils.singleLineString`Colorset preset with machine name ID '${presetName}' not
+ found or failed to load.`
+ );
+ err.allowedValues = Object.keys(utils.getPresets('colorsets'));
+ reject(err);
+ }
+});
+
+// Get the current colorset as a JSON ready object.
+export const getCurrentSet = t => translateSet(setAsArray(), t);
+
+/**
+ * Run at setup, allows machine specific colorset defaults.
+ */
+export function setDefault() {
+ // Trigger on schema loaded for schema & validation defaults.
+ bindTo('schemas.loaded', bindID, () => {
+ let defaultSet = utils.getPreset('colorsets', 'default-single-pen');
+ defaultSet = trigger('colors.setDefault', defaultSet);
+ utils.applyObjectTo(setAsMap(defaultSet), set);
+ trigger('colors.update', defaultSet);
+ trigger('tools.update');
+ });
+}
+
+// Bind to when bot/controller is configured and setup, set default.
+bindTo('controller.setup', bindID, () => {
+ setDefault();
+});
+
+// Figure out if we should even parse color work
+// TODO: this kinda sucks.
+export function doColorParsing() {
+ const items = {};
+ set.items.forEach(item => {
+ if (item.id !== 'ignore') {
+ items[item.id] = true;
+ }
+ });
+ return Object.keys(items).length > 1;
+}
- // Get a luminosity sorted list of colors.
- colors.getSortedSet = () => colors.set.sort(
- (a, b) => new Color(b.color).gray - new Color(a.color).gray
+// Apply luminosity sorting (for presets without weighting info).
+export function applyDefaultPrintSorting(fromSet) {
+ const luminositySorted = Array
+ .from(fromSet.items.values())
+ .sort((a, b) => new Color(b.color).gray - new Color(a.color).gray);
+
+ let weight = -15;
+ luminositySorted.forEach(({ id }) => {
+ weight += 3;
+ fromSet.items.get(id).printWeight = weight;
+ });
+}
+
+// Get the print weight sorted list of colors.
+export function getSortedSet(baseSet = set) {
+ return Array.from(baseSet.items.values()).sort(
+ (a, b) => b.printWeight - a.printWeight
);
+}
- // Get an object keyed by color work ID, ordered by luminosity light->dark.
- colors.getWorkGroups = () => {
- const groups = {};
- const sorted = colors.getSortedSet();
+// Get an object keyed by color work ID, ordered by print weight of empty arrays.
+export function getWorkGroups() {
+ const groups = {};
+ const sorted = getSortedSet().reverse();
- sorted.forEach((color) => {
- if (color.id !== 'ignore') {
- groups[color.id] = [];
- }
- });
- return groups;
- };
+ sorted.forEach(({ id }) => {
+ if (id !== IGNORE_ITEM) {
+ groups[id] = [];
+ }
+ });
+ return groups;
+}
- /**
- * Snap all the paths in the given layer to a particular color.
- *
- * @param {*} layer
- */
- colors.snapPathColors = (layer) => {
- // Build Nearest Color matcher
- const c = {};
- colors.set.forEach(({ id, color }) => {
- c[id] = color;
- });
+// Apply preview styles to an item.
+export function applyPreview(item, color) {
+ // If item matched to "ignore", hide it.
+ if (color.id === IGNORE_ITEM) {
+ item.strokeWidth = 0;
+ } else {
+ // Match colorset item effective implement size.
+ item.strokeWidth = color.implement.width;
+
+ // Save/set new color and matched ID.
+ // IMPORTANT: This is how tool set swaps are rendered.
+ // TODO: Set swaps from print groupings.
+ // item.data.colorID = colorID;
+ item.strokeColor = color.color;
- const nearestColor = nc.from(c);
- layer.children.forEach((group) => {
- group.children.forEach((path) => {
- if (path.strokeColor) {
- // If we've never touched this path before, save the original color.
- if (!path.data.originalColor) {
- path.data.originalColor = path.strokeColor;
- }
+ // Assume less than full opacity with brush/watercolor paintings.
+ item.opacity = color.implement.type === 'brush' ? 0.8 : 1;
- // Find nearest color.
- const nearest = nearestColor(path.data.originalColor.toCSS(true));
+ // Prevent sharp corners messing up render.
+ item.strokeCap = 'round';
+ item.strokeJoin = 'round';
+ }
+}
- path.data.colorID = nearest.name;
- path.strokeColor = nearest.value;
+/**
+ * Snap all the paths in the given layer to a particular colorset item.
+ *
+ * @param {paper.Layer} layer
+ */
+export function snapPathsToColorset(layer) {
+ // This gets called every time there's an update to the render "preview" layer.
+ // - Layer children is a list of content Group() items by hash
+ // - Each group contains all the paths
+
+ // To snap all the paths to a color, we should move through each path
+ // Priorty?
+ // - Line color
+ // - Line thickness (not transferred yet, need to add)
+ // - Line Transparency (not transferred, need to add)
+
+ // Remove everything on print, rebuild it from here.
+ layers.print.removeChildren();
+
+ // Setup all the destination groups within layers.
+ const sorted = getSortedSet().reverse();
+ const printGroups = {}; // ID keyed set of Paper Groups.
+ const colorsetItems = {}; // Static cache of items in groups.
+ sorted.forEach(({ id }) => {
+ printGroups[id] = new Group({ name: id });
+ layers.print.addChild(printGroups[id]);
+ colorsetItems[id] = getColor(id, true, true);
+ });
+
+ console.log('MATCHING ITEMS =====================================================');
+
+ // Move through all preview groups, then all items within them.
+ layer.children.forEach(group => {
+ group.children.forEach(item => {
+ if (item.strokeColor) {
+ // If we've never touched this path before, save the original color.
+ if (!item.data.originalColor) {
+ item.data.originalColor = item.strokeColor;
}
- });
- });
- };
- return colors;
-};
+ // Find nearest color.
+ const nearestID = matcher.matchItemToColor(item);
+ if (nearestID !== IGNORE_ITEM) {
+ applyPreview(item, colorsetItems[nearestID]);
+ printGroups[nearestID].addChild(item.clone());
+ }
+ }
+ });
+ });
+}
diff --git a/src/components/core/drawing/cncserver.drawing.fill.js b/src/components/core/drawing/cncserver.drawing.fill.js
index 1df52247..722c658a 100644
--- a/src/components/core/drawing/cncserver.drawing.fill.js
+++ b/src/components/core/drawing/cncserver.drawing.fill.js
@@ -1,8 +1,27 @@
/**
* @file Code for drawing fill management.
*/
-module.exports = (cncserver, drawing) => {
- const fill = (path, hash, bounds = null, settings, subIndex) => new Promise((success, error) => {
+import path from 'path';
+import { fitBounds } from 'cs/drawing/base';
+import spawner from 'cs/drawing/spawner';
+import { addRenderJSON } from 'cs/drawing/preview';
+import { __basedir } from 'cs/utils';
+
+export default function fill(
+ fillPath, hash, bounds = null, settings, subIndex
+) {
+ return new Promise((success, error) => {
+ const { method } = settings;
+ const script = path.resolve(
+ __basedir,
+ 'components',
+ 'core',
+ 'drawing',
+ 'fillers',
+ method,
+ `cncserver.drawing.fillers.${method}.js`
+ );
+
// Add in computed settings values here.
if (settings.randomizeRotation) {
settings.rotation = Math.round(Math.random() * 360);
@@ -10,32 +29,24 @@ module.exports = (cncserver, drawing) => {
// TODO: Should we fitbounds here? Or earlier?
if (bounds) {
- drawing.base.fitBounds(path, bounds);
+ fitBounds(fillPath, bounds);
}
- const { method } = settings;
- const script = `${__dirname}/fillers/${method}/cncserver.drawing.fillers.${method}.js`;
-
// Use spawner to run fill process.
- drawing
- .spawner({
- type: 'filler',
- hash,
- script,
- settings,
- object: path.exportJSON(),
- subIndex,
- })
- .then((result) => {
- drawing.preview.addRenderJSON(result, hash, {
- fillColor: null,
- strokeWidth: 1,
- strokeColor: path.fillColor,
- });
- success();
- })
- .catch(error);
+ spawner({
+ type: 'filler',
+ hash,
+ script,
+ settings,
+ object: fillPath.exportJSON(),
+ subIndex,
+ }).then(result => {
+ addRenderJSON(result, hash, {
+ fillColor: null,
+ strokeWidth: 1,
+ strokeColor: fillPath.fillColor,
+ });
+ success();
+ }).catch(error);
});
-
- return fill;
-};
+}
diff --git a/src/components/core/drawing/cncserver.drawing.implements.js b/src/components/core/drawing/cncserver.drawing.implements.js
new file mode 100644
index 00000000..aade2e4e
--- /dev/null
+++ b/src/components/core/drawing/cncserver.drawing.implements.js
@@ -0,0 +1,63 @@
+/**
+* @file Code for drawing implement presets and management.
+*/
+import * as utils from 'cs/utils';
+
+// Inherit tag.
+export const IMPLEMENT_PARENT = '[inherit]';
+
+// List Existing Presets.
+export function listPresets(t, customOnly) {
+ const sets = customOnly
+ ? utils.getCustomPresets('implements')
+ : utils.getPresets('implements');
+ const items = Object.values(sets);
+ const sorted = items.sort((a, b) => (a.sortWeight > b.sortWeight ? 1 : -1));
+ const out = {};
+
+ // TODO: Translate implement titles.
+ sorted.forEach(item => {
+ out[item.name] = item;
+ });
+ return out;
+}
+
+// List custom/overridden machine names.
+export function customKeys() {
+ return Object.keys(utils.getCustomPresets('implements'));
+}
+
+// List internal machine names.
+export function internalKeys() {
+ return Object.keys(utils.getInternalPresets('implements'));
+}
+
+// Get a single preset directly from the files.
+export function get(name, customOnly) {
+ return utils.getPreset('implements', name, customOnly);
+}
+
+// Edit an implement.
+export const edit = item => new Promise(resolve => {
+ utils.savePreset('implements', item);
+ resolve(item);
+});
+
+// Add a new custom override implement.
+export const add = item => new Promise((resolve, reject) => {
+ item.name = utils.getMachineName(item.name, 64);
+ if (!get(item.name, true)) {
+ utils.savePreset('implements', item);
+ resolve();
+ } else {
+ reject(
+ new Error(utils.singleLineString`Custom implement with name "${item.name}"
+ already exists, update it directly or change new item name.`)
+ );
+ }
+});
+
+// Remove a custom preset.
+export function deleteImplement(name) {
+ utils.deletePreset('implements', name);
+}
diff --git a/src/components/core/drawing/cncserver.drawing.occlusion.js b/src/components/core/drawing/cncserver.drawing.occlusion.js
index c1d10e52..5259f117 100644
--- a/src/components/core/drawing/cncserver.drawing.occlusion.js
+++ b/src/components/core/drawing/cncserver.drawing.occlusion.js
@@ -1,44 +1,45 @@
/**
* @file Code for drawing occlusion path segmentation management.
*/
-// const { Path, Rectangle } = require('paper');
-module.exports = (cncserver, drawing) => {
- const occlusion = (type, layer = drawing.base.project.activeLayer) => {
- // console.log('Layer children:', layer.children);
- for (let srcIndex = 0; srcIndex < layer.children.length; srcIndex++) {
- let srcPath = layer.children[srcIndex];
- if (type === 'fill' && !srcPath.closed) {
+import { state } from 'cs/drawing/base';
+
+export default function occlusion(type, layer = state.project.activeLayer) {
+ // console.log('Layer children:', layer.children);
+ for (let srcIndex = 0; srcIndex < layer.children.length; srcIndex++) {
+ let srcPath = layer.children[srcIndex];
+ if (type === 'fill' && !srcPath.closed) {
+ // eslint-disable-next-line no-continue
+ continue;
+ }
+
+ srcPath.data.processed = true;
+
+ // Replace this path with a subtract for every intersecting path,
+ // starting at the current index (lower paths don't subtract from
+ // higher ones)
+ const tmpLen = layer.children.length;
+ for (let destIndex = srcIndex; destIndex < tmpLen; destIndex++) {
+ const destPath = layer.children[destIndex];
+ if (!destPath.closed) {
// eslint-disable-next-line no-continue
continue;
}
+ if (destIndex !== srcIndex) {
+ const tmpPath = srcPath; // Hold onto the original path
- srcPath.data.processed = true;
-
- // Replace this path with a subtract for every intersecting path,
- // starting at the current index (lower paths don't subtract from
- // higher ones)
- const tmpLen = layer.children.length;
- for (let destIndex = srcIndex; destIndex < tmpLen; destIndex++) {
- const destPath = layer.children[destIndex];
- if (!destPath.closed) {
- // eslint-disable-next-line no-continue
- continue;
- }
- if (destIndex !== srcIndex) {
- const tmpPath = srcPath; // Hold onto the original path
+ // Set the new srcPath to the subtracted one inserted at the
+ // same index.
+ // console.log('Source subtract from:', srcPath.name, destPath.name);
+ srcPath = layer.insertChild(srcIndex, srcPath.subtract(destPath));
- // Set the new srcPath to the subtracted one inserted at the
- // same index.
- // console.log('Source subtract from:', srcPath.name, destPath.name);
- srcPath = layer.insertChild(srcIndex, srcPath.subtract(destPath));
- srcPath.name = tmpPath.name ? tmpPath.name : `auto_path_${srcPath.id}`;
- srcPath.data = { ...tmpPath.data };
- tmpPath.remove(); // Remove the old srcPath
- }
+ // TODO: This might not be correct. But we need something liek it or all
+ // combined open paths end up closed.
+ srcPath.closed = tmpPath.closed;
+ srcPath.name = tmpPath.name ? tmpPath.name : `auto_path_${srcPath.id}`;
+ srcPath.data = { ...tmpPath.data };
+ tmpPath.remove(); // Remove the old srcPath
}
}
- };
-
- return occlusion;
-};
+ }
+}
diff --git a/src/components/core/drawing/cncserver.drawing.preview.js b/src/components/core/drawing/cncserver.drawing.preview.js
index 4b1c7f90..ca01c1e9 100644
--- a/src/components/core/drawing/cncserver.drawing.preview.js
+++ b/src/components/core/drawing/cncserver.drawing.preview.js
@@ -1,56 +1,54 @@
/**
* @file Code for drawing preview/render layer management.
*/
-/* eslint-disable implicit-arrow-linebreak */
-const { Group } = require('paper');
-
-const preview = { id: 'drawing.preview' };
-
-module.exports = (cncserver, drawing) => {
- const { layers } = drawing.base;
-
- // Clear all items off the preview and update.
- preview.clearAll = () => {
- layers.preview.removeChildren();
- cncserver.sockets.sendPaperUpdate('preview');
- };
-
- // Remove an item from the stage, returns true if it worked.
- preview.remove = (hash, sendUpdate = false) => {
- if (layers.preview.children[hash]) {
- layers.preview.children[hash].remove();
- if (sendUpdate) cncserver.sockets.sendPaperUpdate('preview');
- return true;
- }
- return false;
- };
-
- // Get a full preview SVG of the stage layer content.
- preview.getPreviewSVG = () => {
- const svgContent = layers.stage.exportSVG({ asString: true });
- return cncserver.utils.wrapSVG(svgContent);
- };
-
- // Assumes clean internally rendered JSON.
- // Add the JSON and return the item within the hash group.
- preview.addRenderJSON = (json, hash, adjustments = {}) =>
- preview.addRender(layers.preview.importJSON(json), hash, adjustments);
-
- // Import rendered into the hash group.
- preview.addRender = (importItem, hash, adjustments = {}) => {
- layers.preview.activate();
-
- const renderGroup = layers.preview.children[hash] || new Group({ name: hash });
- renderGroup.addChild(importItem);
-
- // Apply adjustments to the item before sending final update.
- Object.entries(adjustments).forEach(([key, value]) => { importItem[key] = value; });
-
- // Send final update for the addition of this item.
- cncserver.sockets.sendPaperUpdate('preview');
-
- return importItem;
- };
-
- return preview;
-};
+import Paper from 'paper';
+import { layers } from 'cs/drawing/base';
+import { sendPaperUpdate } from 'cs/sockets';
+import { wrapSVG } from 'cs/utils';
+
+const { Group } = Paper;
+
+// Clear all items off the preview and update.
+export function clearAll() {
+ layers.preview.removeChildren();
+ sendPaperUpdate('preview');
+}
+
+// Remove an item from the preview layer, returns true if it worked.
+export function remove(hash, sendUpdate = false) {
+ if (layers.preview.children[hash]) {
+ layers.preview.children[hash].remove();
+ if (sendUpdate) sendPaperUpdate('preview');
+ return true;
+ }
+ return false;
+}
+
+// Get a full preview SVG of the preview layer content.
+export function getPreviewSVG() {
+ const svgContent = layers.stage.exportSVG({ asString: true });
+ return wrapSVG(svgContent);
+}
+
+// Import rendered content into the hash group.
+export function addRender(importItem, hash, adjustments = {}) {
+ layers.preview.activate();
+
+ const renderGroup = layers.preview.children[hash] || new Group({ name: hash });
+ renderGroup.addChild(importItem);
+
+ // Apply adjustments to the item before sending final update.
+ // eslint-disable-next-line no-param-reassign
+ Object.entries(adjustments).forEach(([key, value]) => { importItem[key] = value; });
+
+ // Send final update for the addition of this item.
+ sendPaperUpdate('preview');
+
+ return importItem;
+}
+
+// Assumes clean internally rendered JSON.
+// Add the JSON and return the item within the hash group.
+export function addRenderJSON(json, hash, adjustments = {}) {
+ return addRender(layers.preview.importJSON(json), hash, adjustments);
+}
diff --git a/src/components/core/drawing/cncserver.drawing.spawner.js b/src/components/core/drawing/cncserver.drawing.spawner.js
index 1cd1ee59..162b9cc3 100644
--- a/src/components/core/drawing/cncserver.drawing.spawner.js
+++ b/src/components/core/drawing/cncserver.drawing.spawner.js
@@ -3,138 +3,139 @@
*
* Manages secondary processes for work hash based functions.
*/
-const { spawn } = require('child_process'); // Process spawner.
-const ipc = require('node-ipc'); // Inter Process Comms (shared with ).
-const path = require('path'); // File System path management.
-const fs = require('fs');
+import { spawn } from 'child_process'; // Process spawner.
+import ipc from 'node-ipc'; // Inter Process Comms (shared with ).
+import path from 'path'; // File System path management.
+import fs from 'fs';
+import { state as drawingBase } from 'cs/drawing/base';
+import { bindTo } from 'cs/binder';
// Hold onto paths and settings to be injected, keyed on job hashes & work type.
const workingQueue = {};
-module.exports = (cncserver, drawing) => {
- // Generate a spawn key using a data object.
- function getSpawnKey({ hash, type, subIndex }) {
- const spawnKeys = [type, hash];
- if (subIndex || subIndex === 0) spawnKeys.push(subIndex);
-
- return spawnKeys.join('-');
+// Generate a spawn key using a data object.
+function getSpawnKey({ hash, type, subIndex }) {
+ const spawnKeys = [type, hash];
+ if (subIndex || subIndex === 0) spawnKeys.push(subIndex);
+
+ return spawnKeys.join('-');
+}
+
+// IPC recieve message handler.
+const gotMessage = (packet, socket) => {
+ const { command, data } = packet;
+ const spawnKey = getSpawnKey(data);
+ const workingQueueItem = workingQueue[spawnKey];
+
+ // Catch any non-matching spawn data. This shouldn't happen, but could.
+ if (!workingQueueItem) {
+ // TODO: If this happens, need to clear out process source SOMEHOW.
+ // throw new Error(`Spawn data item mismatch: ${spawnKey}`);
+ console.error(`Spawn data item mismatch: ${spawnKey}, killing process`);
+ ipc.server.emit(socket, 'cancel');
+ return;
}
- // IPC recieve message handler.
- const gotMessage = (packet, socket) => {
- const { command, data } = packet;
- const spawnKey = getSpawnKey(data);
- const workingQueueItem = workingQueue[spawnKey];
-
- // Catch any non-matching spawn data. This shouldn't happen, but could.
- if (!workingQueueItem) {
- // TODO: If this happens, need to clear out process source SOMEHOW.
- // throw new Error(`Spawn data item mismatch: ${spawnKey}`);
- console.error(`Spawn data item mismatch: ${spawnKey}, killing process`);
+ const timeTaken = Math.round((new Date() - workingQueueItem.start) / 100) / 10;
+ switch (command) {
+ case 'ready':
+ // Our spawned worker module is ready! Send it the initial data.
+ ipc.server.emit(socket, 'spawner.init', {
+ size: {
+ width: drawingBase.size.width,
+ height: drawingBase.size.height,
+ },
+ object: workingQueueItem.object,
+ settings: workingQueueItem.settings,
+ });
+
+ console.log(`SPAWN ${spawnKey}: Spawned in ${timeTaken} secs`);
+ break;
+
+ case 'progress':
+ // TODO: This:
+ break;
+ case 'complete':
+ // Fulfull the promise with the worker returned result.
+ workingQueueItem.success(data.result);
+
+ // End the process, we're done with it now.
ipc.server.emit(socket, 'cancel');
- }
-
- const timeTaken = Math.round((new Date() - workingQueueItem.start) / 100) / 10;
- switch (command) {
- case 'ready':
- // Our spawned worker module is ready! Send it the initial data.
- ipc.server.emit(socket, 'spawner.init', {
- size: {
- width: drawing.base.size.width,
- height: drawing.base.size.height,
- },
- object: workingQueueItem.object,
- settings: workingQueueItem.settings,
- });
-
- console.log(`SPAWN ${spawnKey}: Spawned in ${timeTaken} secs`);
- break;
-
- case 'progress':
- // TODO: This:
- break;
- case 'complete':
- // Fulfull the promise with the worker returned result.
- workingQueueItem.success(data.result);
-
- // End the process, we're done with it now.
- workingQueueItem.process.kill('SIGHUP');
- console.log(`SPAWN ${spawnKey}: Completed in ${timeTaken} secs`);
-
- // Free up the working queue memory.
- delete workingQueue[`${data.type}-${data.hash}`];
- break;
-
- default:
- break;
- }
- };
+ workingQueueItem.process.kill('SIGHUP');
+ console.log(`SPAWN ${spawnKey}: Completed in ${timeTaken} secs`);
- // Bind to IPC serve init.
- cncserver.binder.bindTo('ipc.serve', 'spawner', () => {
- // IPC server that manages the runner should be going, just bind it.
- ipc.server.on('spawner.message', gotMessage);
- });
+ // Free up the working queue memory.
+ delete workingQueue[`${data.type}-${data.hash}`];
+ break;
+ default:
+ break;
+ }
+};
- /**
- * Process spawn wrapper for work management!
- *
- * @param {string} arg.hash
- * The hash that identifies the work.
- * @param {string} arg.type
- * The identifier for what kind of process this is, EG 'filler'.
- * @param {string} arg.subIndex
- * An extra identifier for delineating a sub fill within a base hash.
- * @param {string} arg.script
- * The full path of the script to pass to the node binary.
- * @param {object} arg.object
- * The object to be worked on, likely a Paper path.
- * @param {object} arg.settings
- * The settings to be passed along to the spawned worker.
- *
- * @return {Promise}
- * Promise that on success, returns processed object & original passed data,
- * otherwise results in error if spawn process dies with non-zero exit code.
- */
- const spawner = ({
- hash, type, script, object, settings, subIndex = '',
- }) => new Promise((success, error) => {
- const spawnKey = getSpawnKey({ hash, type, subIndex });
- const spawnData = {
- start: new Date(),
- settings,
- object,
- success,
- error,
- subIndex,
- };
-
- const resolvedScript = path.resolve(script);
- if (!fs.existsSync(resolvedScript)) {
- error(new Error(`"${type}" spawn entry doesn't exist: ${resolvedScript}`));
- }
-
- // Spawn process and bind basic i/o.
- spawnData.process = spawn('node', [resolvedScript, hash, subIndex]);
- spawnData.process.stdout.on('data', (rawData) => {
- rawData.toString().split('\n').forEach((line) => {
- if (line.length) console.log(`SPAWN ${spawnKey}: ${line}`);
- });
- });
+// Bind to IPC serve init.
+bindTo('ipc.serve', 'spawner', () => {
+ // IPC server that manages the runner should be going, just bind it.
+ ipc.server.on('spawner.message', gotMessage);
+});
- spawnData.process.stderr.on('data', (err) => {
- console.error(`SPAWN ERROR ${spawnKey}: ${err}`);
- spawnData.error(err);
- spawnData.process.kill('SIGHUP');
- });
+/**
+ * Process spawn wrapper for work management!
+ *
+ * @param {string} arg.hash
+ * The hash that identifies the work.
+ * @param {string} arg.type
+ * The identifier for what kind of process this is, EG 'filler'.
+ * @param {string} arg.subIndex
+ * An extra identifier for delineating a sub fill within a base hash.
+ * @param {string} arg.script
+ * The full path of the script to pass to the node binary.
+ * @param {object} arg.object
+ * The object to be worked on, likely a Paper path.
+ * @param {object} arg.settings
+ * The settings to be passed along to the spawned worker.
+ *
+ * @return {Promise}
+ * Promise that on success, returns processed object & original passed data,
+ * otherwise results in error if spawn process dies with non-zero exit code.
+ */
+const spawner = ({
+ hash, type, script, object, settings, subIndex = '',
+}) => new Promise((success, error) => {
+ const spawnKey = getSpawnKey({ hash, type, subIndex });
+ const spawnData = {
+ start: new Date(),
+ settings,
+ object,
+ success,
+ error,
+ subIndex,
+ };
+
+ const resolvedScript = path.resolve(script);
+ if (!fs.existsSync(resolvedScript)) {
+ error(new Error(`"${type}" spawn entry doesn't exist: ${resolvedScript}`));
+ }
- spawnData.process.on('exit', (exitCode) => {
- if (exitCode) console.log(`SPAWN EXIT ${spawnKey}: ${exitCode}`);
+ // Spawn process and bind basic i/o.
+ spawnData.process = spawn('node', [resolvedScript, hash, subIndex]);
+ spawnData.process.stdout.on('data', rawData => {
+ rawData.toString().split('\n').forEach(line => {
+ if (line.length) console.log(`SPAWN ${spawnKey}: ${line}`);
});
+ });
- workingQueue[spawnKey] = spawnData;
+ spawnData.process.stderr.on('data', err => {
+ console.error(`SPAWN ERROR ${spawnKey}: ${err}`);
+ spawnData.error(err);
+ spawnData.process.kill('SIGHUP');
});
- return spawner;
-};
+ spawnData.process.on('exit', exitCode => {
+ if (exitCode) console.log(`SPAWN EXIT ${spawnKey}: ${exitCode}`);
+ });
+
+ workingQueue[spawnKey] = spawnData;
+});
+
+export default spawner;
diff --git a/src/components/core/drawing/cncserver.drawing.stage.js b/src/components/core/drawing/cncserver.drawing.stage.js
index 758d18d5..a361a2d8 100644
--- a/src/components/core/drawing/cncserver.drawing.stage.js
+++ b/src/components/core/drawing/cncserver.drawing.stage.js
@@ -1,106 +1,105 @@
+/* eslint-disable no-param-reassign */
/**
* @file Code for drawing stage layer management.
*/
-const { Group, Path } = require('paper');
-
-const stage = { id: 'drawing.stage' };
-
-module.exports = (cncserver, drawing) => {
- const { layers } = drawing.base;
-
- // Default projects settings.
- stage.defaultSettings = () => ({
- // TODO
+import Paper from 'paper';
+import { layers, fitBounds, workspace } from 'cs/drawing/base';
+import { sendPaperUpdate } from 'cs/sockets';
+import { wrapSVG } from 'cs/utils';
+
+const { Group, Path } = Paper;
+
+// Default projects settings.
+export function defaultSettings() {
+ // TODO
+}
+
+// Clear all items off the stage and update.
+export function clearAll() {
+ layers.stage.removeChildren();
+ sendPaperUpdate('stage');
+}
+
+// Remove an item from the stage, returns true if it worked.
+export function remove(hash) {
+ if (layers.stage.children[hash]) {
+ layers.stage.children[hash].remove();
+ return true;
+ }
+ return false;
+}
+
+// Toggle the visibility of the bound rects.
+export function toggleRects(state) {
+ layers.stage.children.forEach(group => {
+ group.children.bounds.opacity = state ? 1 : 0;
+ group.children['content-bounds'].opacity = state ? 1 : 0;
});
-
- // Clear all items off the stage and update.
- stage.clearAll = () => {
- layers.stage.removeChildren();
- cncserver.sockets.sendPaperUpdate('stage');
- };
-
- // Remove an item from the stage, returns true if it worked.
- stage.remove = (hash) => {
- if (layers.stage.children[hash]) {
- layers.stage.children[hash].remove();
- return true;
- }
- return false;
- };
-
- // Get a full preview SVG of the stage layer content.
- stage.getPreviewSVG = () => {
- // Hide bounds rects.
- stage.toggleRects(false);
-
- const svgContent = layers.stage.exportSVG({ asString: true });
-
- // Show bounds rects.
- stage.toggleRects(true);
-
- return cncserver.utils.wrapSVG(svgContent, drawing.base.workspace);
- };
-
- // Toggle the visibility of the bound rects.
- stage.toggleRects = (state) => {
- layers.stage.children.forEach((group) => {
- group.children.bounds.opacity = state ? 1 : 0;
- group.children['content-bounds'].opacity = state ? 1 : 0;
- });
- };
-
- // Import an imported paper item into the stage layer.
- stage.import = (importItem, hash, bounds) => {
- const finalBounds = drawing.base.fitBounds(importItem, bounds);
-
- // console.log(importItem);
- // console.log(importItem.bounds);
-
- // If an existing item with a matching hash exists, remove it first.
- stage.remove(hash);
-
- // Build a group with the name/id of the hash, containing the content and a
- // path rectangle matching the bounds.
-
- // eslint-disable-next-line no-param-reassign
- importItem.name = 'content';
- layers.stage.addChild(
- new Group({
- name: hash,
- children: [
- new Path.Rectangle({
- name: 'bounds',
- point: finalBounds.point,
- size: finalBounds.size,
- strokeWidth: 1,
- strokeColor: 'green',
- dashArray: [2, 2],
- }),
- new Path.Rectangle({
- name: 'content-bounds',
- point: importItem.bounds.point,
- size: importItem.bounds.size,
- strokeWidth: 1,
- strokeColor: 'red',
- opacity: 0.5,
- dashArray: [2, 2],
- }),
- importItem,
- ],
- })
- );
- cncserver.sockets.sendPaperUpdate('stage');
- return importItem;
- };
-
- // Update an item on the stage with its action item.
- stage.updateItem = (item) => {
- // Update bounds.
- const stageGroup = layers.stage.children[item.hash];
- if (stageGroup) {
- return stage.import(stageGroup.children.content, item.hash, item.bounds);
- }
- };
-
- return stage;
-};
+}
+
+// Import an imported paper item into the stage layer.
+export function importGroup(importItem, hash, bounds) {
+ const finalBounds = fitBounds(importItem, bounds);
+
+ // console.log(importItem);
+ // console.log(importItem.bounds);
+
+ // If an existing item with a matching hash exists, remove it first.
+ remove(hash);
+
+ // Build a group with the name/id of the hash, containing the content and a
+ // path rectangle matching the bounds.
+
+ // eslint-disable-next-line no-param-reassign
+ importItem.name = 'content';
+ layers.stage.addChild(
+ new Group({
+ name: hash,
+ children: [
+ new Path.Rectangle({
+ name: 'bounds',
+ point: finalBounds.point,
+ size: finalBounds.size,
+ strokeWidth: 1,
+ strokeColor: 'green',
+ dashArray: [2, 2],
+ }),
+ new Path.Rectangle({
+ name: 'content-bounds',
+ point: importItem.bounds.point,
+ size: importItem.bounds.size,
+ strokeWidth: 1,
+ strokeColor: 'red',
+ opacity: 0.5,
+ dashArray: [2, 2],
+ }),
+ importItem,
+ ],
+ })
+ );
+ sendPaperUpdate('stage');
+ return importItem;
+}
+
+// Get a full preview SVG of the stage layer content.
+export function getPreviewSVG() {
+ // Hide bounds rects.
+ toggleRects(false);
+
+ const svgContent = layers.stage.exportSVG({ asString: true });
+
+ // Show bounds rects.
+ toggleRects(true);
+
+ return wrapSVG(svgContent, workspace);
+}
+
+// Update an item on the stage with its action item.
+export function updateItem(item) {
+ // Update bounds.
+ const stageGroup = layers.stage.children[item.hash];
+ if (stageGroup) {
+ return importGroup(stageGroup.children.content, item.hash, item.bounds);
+ }
+ return null;
+}
diff --git a/src/components/core/drawing/cncserver.drawing.temp.js b/src/components/core/drawing/cncserver.drawing.temp.js
index d51ebfdc..7a6c1bb7 100644
--- a/src/components/core/drawing/cncserver.drawing.temp.js
+++ b/src/components/core/drawing/cncserver.drawing.temp.js
@@ -1,39 +1,35 @@
/**
* @file Code for drawing temp layer management.
*/
-/* eslint-disable implicit-arrow-linebreak */
-const { Group } = require('paper');
+import Paper from 'paper';
+import { layers } from 'cs/drawing/base';
+import { sendPaperUpdate } from 'cs/sockets';
-const temp = { id: 'drawing.temp' };
+const { Group } = Paper;
-module.exports = (cncserver, drawing) => {
- const { layers } = drawing.base;
+// Clear all items off the temp layer.
+export function clearAll() {
+ layers.temp.removeChildren();
+}
- // Clear all items off the temp layer.
- temp.clearAll = () => {
- layers.temp.removeChildren();
- };
+// Import temp item into a hash group, return group with item(s).
+export function addItem(importItem, hash, adjustments = {}) {
+ layers.temp.activate();
+ const item = importItem.copyTo(layers.temp);
- // Add the JSON and return the item within the hash group.
- temp.addJSON = (json, hash, adjustments = {}) =>
- temp.addItem(layers.temp.importJSON(json), hash, adjustments);
+ const tempGroup = layers.temp.children[hash] || new Group({ name: hash });
+ tempGroup.addChild(item);
- // Import temp item into a hash group, return group with item(s).
- temp.addItem = (importItem, hash, adjustments = {}) => {
- layers.temp.activate();
- const item = importItem.copyTo(layers.temp);
+ // Apply adjustments to the item before sending final update.
+ Object.entries(adjustments).forEach(([key, value]) => { item[key] = value; });
- const tempGroup = layers.temp.children[hash] || new Group({ name: hash });
- tempGroup.addChild(item);
+ // Send final update for the addition of this item.
+ sendPaperUpdate('preview');
- // Apply adjustments to the item before sending final update.
- Object.entries(adjustments).forEach(([key, value]) => { item[key] = value; });
+ return tempGroup;
+}
- // Send final update for the addition of this item.
- cncserver.sockets.sendPaperUpdate('preview');
-
- return tempGroup;
- };
-
- return temp;
-};
+// Add the JSON and return the item within the hash group.
+export function addJSON(json, hash, adjustments = {}) {
+ return addItem(layers.temp.importJSON(json), hash, adjustments);
+}
diff --git a/src/components/core/drawing/cncserver.drawing.text.js b/src/components/core/drawing/cncserver.drawing.text.js
index d6174897..51bcbe38 100644
--- a/src/components/core/drawing/cncserver.drawing.text.js
+++ b/src/components/core/drawing/cncserver.drawing.text.js
@@ -1,172 +1,206 @@
+/* eslint-disable no-param-reassign */
/**
* @file Code for text rendering.
*/
-const hershey = require('hersheytext');
-const vectorizeText = require('vectorize-text');
-const { createCanvas } = require('canvas');
-
-const {
- Path, CompoundPath, Point, Group,
-} = require('paper');
-
-const text = {};
-
-module.exports = (cncserver, drawing) => {
- text.fonts = hershey.svgFonts;
-
- /**
- * Returns a group of lines and characters rendered in single line hersheyfont
- */
- function renderHersheyPaths(textContent, bounds, settings) {
- // Render the text array.
- const textArray = hershey.renderTextArray(textContent, {
- // TODO: Find out differences in what this takes vs our settings object
- // and make it EXPLICIT.
- ...settings,
- pos: { x: 0, y: 0 },
- });
-
- const { view } = drawing.base.project;
+import hershey from 'hersheytext';
+import Paper from 'paper';
+import vectorizeText from 'vectorize-text';
+import Canvas from 'canvas';
+import { getUserDirFiles } from 'cs/utils';
+import { layers, fitBounds } from 'cs/drawing/base';
+import { trace, fill } from 'cs/drawing';
+import { trigger } from 'cs/binder';
+
+const { CompoundPath, Point, Group } = Paper;
+const { createCanvas } = Canvas;
+
+export const fonts = hershey.svgFonts;
+
+// Add any custom fonts.
+getUserDirFiles('fonts', '*.svg', hershey.addSVGFont);
+
+// Trigger font load complete.
+// TODO: This might need to move.
+trigger('drawing.text.setup', fonts, true);
+
+// Problem: With really long strings, they never break on their own, so if by
+// default we wrap strings, lets format the text that goes in the stage
+// preview to have manual breaks in it.
+export function format(text) {
+ const words = text.split(' ');
+ const out = [];
+ let lineLength = 0;
+ words.forEach(word => {
+ lineLength += word.length;
+ if (lineLength > 60) {
+ lineLength = 0;
+ out.push(`${word}\n`);
+ } else {
+ out.push(word);
+ }
+ });
+ return out.join(' ');
+}
- // Move through each char and build out chars and lines.
- const caretPos = new Point(0, 50);
- const chars = new Group(); // Hold output lines groups
- const lines = [new Group({ name: 'line-0' })]; // Hold chars in lines
-
- let cLine = 0;
- textArray.forEach((char, index) => {
- if (char.type === 'space' || char.type === 'newline') {
- caretPos.x += settings.spaceWidth;
-
- // Allow line wrap on space
- if (caretPos.x > settings.wrapWidth || char.type === 'newline') {
- // Before wrapping, reverse the order of the chars.
- lines[cLine].reverseChildren();
-
- caretPos.x = 0;
- caretPos.y += settings.lineHeight;
-
- cLine++;
- lines.push(new Group({ name: `line-${cLine}` }));
- }
- } else {
- // Create the compound path of the character.
- const c = new CompoundPath({
- data: { char: char.type },
- pathData: char.d,
- name: `letter-${char.type}-${index}-${cLine}`,
- });
-
- lines[cLine].insertChild(0, c);
-
- // TODO: Add this to settings with some kind of validation schema.
- if (settings.smooth) c.smooth(settings.smooth);
-
- // Rotate chararcters.
- if (settings.character.rotation) c.rotate(settings.character.rotation);
-
- // Align to the top left as expected by the font system
- const b = c.bounds;
- c.pivot = new Point(0, 0);
- c.position = caretPos;
-
- // Move the caret to the next position based on width and char spacing
- caretPos.x += b.width + settings.character.spacing;
+/**
+ * Returns a group of lines and characters rendered in single line hershey/SVG font.
+ *
+ * All raw values are in the font's number system before import scaling.
+ */
+function renderHersheyPaths(textContent, bounds, settings) {
+ // Render the text array.
+ const textArray = hershey.renderTextArray(textContent, settings);
+
+ // const { view } = project;
+
+ // Move through each char and build out chars and lines.
+ const caretPos = new Point(0, 50);
+ const chars = new Group(); // Hold output lines groups
+ const lines = [new Group({ name: 'line-0' })]; // Hold chars in lines
+
+ let cLine = 0;
+ textArray.forEach((char, index) => {
+ if (char.name === 'space' || char.name === 'newline') {
+ caretPos.x += settings.spaceWidth + char.width;
+
+ // Allow for wrapping based on wrapChars setting.
+ let passedWrapWidth = false;
+ if (char.name === 'space' && settings.wrapLines) {
+ passedWrapWidth = caretPos.x > settings.wrapChars * char.width;
}
- });
- // Reverse the chars in the line if only one line.
- if (cLine === 0) {
- lines[0].reverseChildren();
- }
+ // Allow line wrap on space, or forced via newline.
+ if (passedWrapWidth || char.name === 'newline') {
+ // Before wrapping, reverse the order of the chars.
+ lines[cLine].reverseChildren();
- // Add all lines of text to the final group.
- chars.addChildren(lines);
+ caretPos.x = 0;
+ // TODO: Get the actual base line height from the font.
+ caretPos.y += 1000 + settings.lineHeight;
- // Align the lines
- if (settings.align.paragraph === 'center') {
- lines.forEach((line) => {
- line.position.x = chars.position.x;
- });
- } else if (settings.align.paragraph === 'right') {
- lines.forEach((line) => {
- line.pivot = new Point(line.bounds.width, line.bounds.height / 2);
- line.position.x = chars.bounds.width;
+ cLine++;
+ lines.push(new Group({ name: `line-${cLine}` }));
+ }
+ } else {
+ // Create the compound path of the character.
+ const c = new CompoundPath({
+ data: { char: char.type },
+ pathData: char.d,
+ name: `letter-${char.type}-${index}-${cLine}`,
});
+
+ lines[cLine].insertChild(0, c);
+
+ // TODO: Add this to settings with some kind of validation schema.
+ if (settings.smooth) c.smooth(settings.smooth);
+
+ // Rotate chararcters.
+ if (settings.character.rotation) c.rotate(settings.character.rotation);
+
+ // Align to the top left as expected by the font system.
+ c.pivot = new Point(0, 0);
+ c.position = [caretPos.x, caretPos.y];
+
+ // Move the caret to the next position based on width and char spacing.
+ caretPos.x += char.width + settings.character.spacing;
+
+ // Flip the glyph over vertically.
+ c.scale([1, -1]);
}
+ });
- return chars;
+ // Reverse the chars in the line if only one line.
+ if (cLine === 0) {
+ lines[0].reverseChildren();
}
- /**
- * Return a group characters rendered in filled compound path system font.
- */
- function renderFilledText(textContent, hash, bounds, settings) {
- const canvas = createCanvas(8192, 1024);
- const {
- fontStyle, fontVariant, fontWeight, textBaseline = 'hanging', textAlign, systemFont,
- } = settings.text;
- const polygons = vectorizeText(textContent, {
- polygons: true,
- width: 200,
- font: systemFont,
- context: canvas.getContext('2d'),
- fontStyle,
- fontVariant,
- fontWeight,
- textBaseline,
- textAlign,
- canvas,
+ // Add all lines of text to the final group.
+ chars.addChildren(lines);
+
+ // Align the lines
+ if (settings.align.paragraph === 'center') {
+ lines.forEach(line => {
+ line.position.x = chars.position.x;
+ });
+ } else if (settings.align.paragraph === 'right') {
+ lines.forEach(line => {
+ line.pivot = new Point(line.bounds.width, line.bounds.height / 2);
+ line.position.x = chars.bounds.width;
});
+ }
- const chars = new Group();
- polygons.forEach((loops) => {
- let d = '';
- loops.forEach((loop) => {
- loop.forEach(([x, y], index) => {
- const op = index === 0 ? 'M' : 'L';
- d = `${d} ${op} ${x} ${y}`;
- });
-
- // Add the first point back as end point.
- d = `${d} L ${loop[0][0]} ${loop[0][1]}`;
+ return chars;
+}
+
+/**
+ * Return a group characters rendered in filled compound path system font.
+ */
+function renderFilledText(textContent, hash, bounds, settings) {
+ const canvas = createCanvas(8192, 1024);
+ const {
+ fontStyle, fontVariant, fontWeight, textBaseline = 'hanging', textAlign, systemFont,
+ } = settings.text;
+ const polygons = vectorizeText(textContent, {
+ polygons: true,
+ width: 200,
+ font: systemFont,
+ context: canvas.getContext('2d'),
+ fontStyle,
+ fontVariant,
+ fontWeight,
+ textBaseline,
+ textAlign,
+ canvas,
+ });
+
+ const chars = new Group();
+ polygons.forEach(loops => {
+ let d = '';
+ loops.forEach(loop => {
+ loop.forEach(([x, y], index) => {
+ const op = index === 0 ? 'M' : 'L';
+ d = `${d} ${op} ${x} ${y}`;
});
- chars.addChild(new CompoundPath(d));
+
+ // Add the first point back as end point.
+ d = `${d} L ${loop[0][0]} ${loop[0][1]}`;
});
+ chars.addChild(new CompoundPath(d));
+ });
+ // Text sizing and position!
+ if (bounds) {
+ // Scale and fit within the given bounds rectangle.
+ fitBounds(chars, bounds);
+ } else {
+ // Position off from center, or at exact position.
+ /* const anchorPos = drawing.base.getAnchorPos(chars, settings.anchor);
+ const { view } = drawing.base.project;
- // Text sizing and position!
- if (bounds) {
- // Scale and fit within the given bounds rectangle.
- drawing.base.fitBounds(chars, bounds);
+ if (settings.position) {
+ chars.position = new Point(settings.position).subtract(anchorPos);
} else {
- // Position off from center, or at exact position.
- /* const anchorPos = drawing.base.getAnchorPos(chars, settings.anchor);
- const { view } = drawing.base.project;
-
- if (settings.position) {
- chars.position = new Point(settings.position).subtract(anchorPos);
- } else {
- chars.position = view.center.add(new Point(settings.hCenter, settings.vCenter));
- }
- chars.scale(settings.scale, anchorPos); */
- }
-
- // Fill each character if settings are given.
- if (settings.fill.render) {
- chars.children.forEach((char, subIndex) => {
- char.fillColor = settings.path.fillColor;
- cncserver.drawing.fill(char, hash, null, settings.fill, subIndex);
- });
+ chars.position = view.center.add(new Point(settings.hCenter, settings.vCenter));
}
+ chars.scale(settings.scale, anchorPos); */
+ }
- return chars;
+ // Fill each character if settings are given.
+ if (settings.fill.render) {
+ chars.children.forEach((char, subIndex) => {
+ char.fillColor = settings.path.fillColor;
+ fill(char, hash, null, settings.fill, subIndex);
+ });
}
- // Actually build the paths for drawing.
- text.draw = (textContent, hash, bounds, settings) => new Promise((resolve, reject) => {
+ return chars;
+}
+
+// Actually build the paths for drawing.
+export function draw(textContent, hash, bounds, settings) {
+ return new Promise((resolve, reject) => {
// Render content to the temp layer.
- drawing.base.layers.temp.activate();
+ layers.temp.activate();
let chars;
try {
@@ -184,14 +218,12 @@ module.exports = (cncserver, drawing) => {
if (settings.stroke.render) {
chars.strokeColor = settings.path.strokeColor;
- drawing.trace(chars, hash, bounds, settings.stroke);
+ trace(chars, hash, bounds, settings.stroke);
} else if (settings.fill.trace) {
chars.strokeColor = settings.path.fillColor;
- drawing.trace(chars, hash, bounds, settings.stroke);
+ trace(chars, hash, bounds, settings.stroke);
}
}
resolve();
});
-
- return text;
-};
+}
diff --git a/src/components/core/drawing/cncserver.drawing.trace.js b/src/components/core/drawing/cncserver.drawing.trace.js
index f984083c..a7c545b3 100644
--- a/src/components/core/drawing/cncserver.drawing.trace.js
+++ b/src/components/core/drawing/cncserver.drawing.trace.js
@@ -1,11 +1,14 @@
/**
* @file Trace code for drawing base, pretty much just imports it into Paper!
*/
-module.exports = (cncserver, drawing) => {
- const trace = (inputPath, hash, bounds = null, settings = {}) => new Promise((resolve, reject) => {
+import { fitBounds } from 'cs/drawing/base';
+import { addRender } from 'cs/drawing/preview';
+
+export default function trace(inputPath, hash, bounds = null, settings = {}) {
+ return new Promise(resolve => {
// If bounds set, resize the path.
if (bounds) {
- drawing.base.fitBounds(inputPath, bounds);
+ fitBounds(inputPath, bounds);
}
// TODO: Actually render if we have dash or other options.
@@ -13,16 +16,13 @@ module.exports = (cncserver, drawing) => {
// TODO: This.
} else {
// Take normalized path and add it to the preview layer.
- drawing.preview.addRender(inputPath.clone(), hash, {
- strokeWidth: 1,
+ addRender(inputPath.clone(), hash, {
+ strokeWidth: inputPath.strokeWidth || 1,
strokeColor: inputPath.strokeColor,
fillColor: null,
});
}
-
resolve();
});
-
- return trace;
-};
+}
diff --git a/src/components/core/drawing/cncserver.drawing.vectorize.js b/src/components/core/drawing/cncserver.drawing.vectorize.js
index 01d47838..18923ed0 100644
--- a/src/components/core/drawing/cncserver.drawing.vectorize.js
+++ b/src/components/core/drawing/cncserver.drawing.vectorize.js
@@ -1,40 +1,42 @@
/**
* @file Code for image vectorizor management.
*/
-const path = require('path');
+import path from 'path';
+import spawner from 'cs/drawing/spawner';
+import { renderGroup } from 'cs/content';
+import { addJSON } from 'cs/drawing/temp';
+import { __basedir } from 'cs/utils';
-module.exports = (cncserver, drawing) => {
- const vectorize = (imagePath, hash, bounds, settings) => new Promise((success, error) => {
+export default function vectorize(imagePath, hash, bounds, settings) {
+ return new Promise((success, error) => {
const { method } = settings.vectorize;
const script = path.resolve(
- __dirname,
+ __basedir,
+ 'components',
+ 'core',
+ 'drawing',
'vectorizers',
method,
`cncserver.drawing.vectorizers.${method}.js`
);
// Use spawner to run vectorizer process.
- drawing
- .spawner({
- type: 'vectorizer',
- hash,
- script,
- settings: { ...settings.vectorize, bounds },
- object: imagePath,
- })
- .then((result) => {
- // Because some vectorizers produce filled shapes, we need to process
- // these in the temp layer before moving them to render preview.
- const group = drawing.temp.addJSON(result, hash);
+ spawner({
+ type: 'vectorizer',
+ hash,
+ script,
+ settings: { ...settings.vectorize, bounds },
+ object: imagePath,
+ }).then(result => {
+ // Because some vectorizers produce filled shapes, we need to process
+ // these in the temp layer before moving them to render preview.
+ const group = addJSON(result, hash);
- console.log('Returned items', group.children[0].children.length);
+ console.log('Returned items', group.children[0].children.length);
- // We'll NEVER have occlusion if converting a raster to a vector.
- cncserver.content.renderGroup(group, hash, settings, true);
- success();
- })
- .catch(error);
+ // We'll NEVER have occlusion if converting a raster to a vector.
+ renderGroup(group, hash, settings, true);
+ success();
+ }).catch(error);
});
-
- return vectorize;
-};
+}
diff --git a/src/components/core/drawing/colorsets/crayola.json b/src/components/core/drawing/colorsets/crayola.json
deleted file mode 100644
index 7b4b5894..00000000
--- a/src/components/core/drawing/colorsets/crayola.json
+++ /dev/null
@@ -1,52 +0,0 @@
-[{
- "manufacturer": "crayola",
- "media": "watercolor",
- "machineName": "secondary",
- "weight": 1,
- "colors": {
- "maroon": "#bb39b9",
- "orangered": "#e14a35",
- "tangerine": "#f3e730",
- "yellowgreen": "#b5d02e",
- "teal": "#2ed07e",
- "skyblue": "#53bee7",
- "indigo": "#535ee7",
- "white": "#EEE"
- }
-},
-
-{
- "manufacturer": "crayola",
- "media":"watercolor",
- "machineName": "tertiary",
- "weight": 5,
- "colors": {
- "grey": "#716d78",
- "pink": "#ff61bc",
- "salmon": "#ffb260",
- "lemon": "#ffff30",
- "lime": "#b2f743",
- "turquoise": "#00c0a3",
- "fuchsia": "#d55ce9",
- "beige": "#de896d"
- }
-},
-
-{
- "manufacturer": "crayola",
- "media":"pen",
- "machineName": "classic",
- "weight": 5,
- "colors": {
- "brown":"#8B4513",
- "purple":"#800080",
- "red":"#FF0000",
- "orange":"#FFA500",
- "yellow":"#FFFF00",
- "green":"#008000",
- "blue":"#0000FF",
- "black":"#000000",
- "pink": "#ff61bc",
- "grey": "#716d78"
- }
-}]
diff --git a/src/components/core/drawing/colorsets/generic.json b/src/components/core/drawing/colorsets/generic.json
deleted file mode 100644
index aac34b8c..00000000
--- a/src/components/core/drawing/colorsets/generic.json
+++ /dev/null
@@ -1,16 +0,0 @@
-[{
- "manufacturer": "generic",
- "media": "watercolor",
- "machineName": "generic",
- "weight": -10,
- "colors": {
- "black":"#000000",
- "red":"#FF0000",
- "orange":"#FFA500",
- "yellow":"#FFFF00",
- "green":"#008000",
- "blue":"#0000FF",
- "purple":"#800080",
- "brown":"#8B4513"
- }
-}]
diff --git a/src/components/core/drawing/colorsets/prang.json b/src/components/core/drawing/colorsets/prang.json
deleted file mode 100644
index e6bddd13..00000000
--- a/src/components/core/drawing/colorsets/prang.json
+++ /dev/null
@@ -1,48 +0,0 @@
-[{
- "manufacturer": "prang",
- "media": "watercolor",
- "machineName": "glitter",
- "weight": 11,
- "colors": {
- "gold":"#aba66d",
- "silver":"#b1bcac",
- "lilac":"#cdb2d0",
- "sapphire":"#32c0c4",
- "emerald":"#2ca440",
- "canary":"#dede5d",
- "tangerine":"#efa038",
- "ruby":"#f0846c"
- }
-},
-{
- "manufacturer": "prang",
- "media": "watercolor",
- "machineName": "metallic",
- "weight": 12,
- "colors": {
- "gold":"#d9bf81",
- "silver":"#569aaa",
- "copper":"#a74837",
- "blue":"#0082ad",
- "green":"#00ae4d",
- "yellow":"#ffe30c",
- "orange":"#f35400",
- "red":"#c0080b"
- }
-},
-{
- "manufacturer": "prang",
- "media": "watercolor",
- "machineName": "secondary",
- "weight": 10,
- "colors": {
- "vermilion":"#ff5a59",
- "yelloworange":"#ffa300",
- "yellowgreen":"#9aea24",
- "bluegreen":"#009aa6",
- "turquoiseblue":"#00aadf",
- "blueviolet":"#00aadf",
- "redviolet":"#d842ce",
- "white":"#eee"
- }
-}]
diff --git a/src/components/core/drawing/colorsets/sargent.json b/src/components/core/drawing/colorsets/sargent.json
deleted file mode 100644
index 64936e9f..00000000
--- a/src/components/core/drawing/colorsets/sargent.json
+++ /dev/null
@@ -1,16 +0,0 @@
-[{
- "manufacturer": "sargent",
- "media": "watercolor",
- "machineName": "secondary",
- "weight": 20,
- "colors": {
- "yelloworange":"#ffbe2c",
- "vermilion":"#ff451f",
- "redviolet":"#c02987",
- "blueviolet":"#212aa9",
- "green":"#a3d130",
- "teal":"#00a6b8",
- "teal2":"#00a6b8",
- "white":"#eee"
- }
-}]
diff --git a/src/components/core/drawing/fillers/cncserver.drawing.fillers.util.js b/src/components/core/drawing/fillers/cncserver.drawing.fillers.util.js
index dbee5197..71b72bca 100644
--- a/src/components/core/drawing/fillers/cncserver.drawing.fillers.util.js
+++ b/src/components/core/drawing/fillers/cncserver.drawing.fillers.util.js
@@ -4,12 +4,17 @@
* Holds all standardized processes for managing IPC, paper setup, and export
* so the fill algorithm can do whatever it needs.
*/
+import Paper from 'paper';
+import clipperLib from 'js-angusj-clipper';
+import ipc from 'node-ipc';
+
const {
Project, Size, Path, CompoundPath,
-} = require('paper');
-const clipperLib = require('js-angusj-clipper');
-const ipc = require('node-ipc');
+} = Paper;
+export const state = {
+ project: {}, // Placeholder for paper project to work off of.
+};
const hostname = 'cncserver';
const ipcBase = {
hash: process.argv[2],
@@ -17,6 +22,12 @@ const ipcBase = {
type: 'filler',
};
+// Catch uncaught exceptions to clean things up.
+process.addListener('uncaughtException', err => {
+ console.error(err);
+ process.exit(1);
+});
+
// Config IPC.
ipc.config.silent = true;
@@ -27,7 +38,7 @@ const send = (command, data = {}) => {
};
// Clipper helper utilities.
-const clipper = {
+export const clipper = {
scalePrecision: 10000,
getInstance: async () => clipperLib.loadNativeClipperLibInstanceAsync(
clipperLib.NativeClipperLibRequestedFormat.WasmWithAsmJsFallback
@@ -51,7 +62,7 @@ const clipper = {
c.flatten(resolution);
geometries[pathIndex] = [];
- c.segments.forEach((s) => {
+ c.segments.forEach(s => {
geometries[pathIndex].push(clipper.translatePoint(s.point));
});
@@ -71,7 +82,7 @@ const clipper = {
geometries[0] = [];
p.flatten(resolution);
- p.segments.forEach((s) => {
+ p.segments.forEach(s => {
geometries[0].push(clipper.translatePoint(s.point));
});
}
@@ -98,9 +109,9 @@ const clipper = {
const out = [];
if (result && result.length) {
- result.forEach((subPathPoints) => {
+ result.forEach(subPathPoints => {
const subPath = new Path();
- subPathPoints.forEach((point) => {
+ subPathPoints.forEach(point => {
subPath.add({
x: point.x / clipper.scalePrecision,
y: point.y / clipper.scalePrecision,
@@ -117,99 +128,87 @@ const clipper = {
},
};
-const exp = {
- connect: (initCallback) => {
- ipc.connectTo(hostname, () => {
- // Setup bindings now that the socket is ready.
- ipc.of[hostname].on('connect', () => {
- // Connected! Tell the server we're ready for data.
- send('ready', ipcBase);
- });
-
- // Bind central init, this gives us everything we need to do the work!
- ipc.of[hostname].on('spawner.init', ({ size, object, settings }) => {
- exp.project = new Project(new Size(size));
- const item = exp.project.activeLayer.importJSON(object);
- // console.log('Path:', item.name, `${Math.round(item.length * 10) / 10}mm long`);
-
- // For any non-zero value, we want to inset the object before it gets in.
- if (settings.inset) {
- clipper.getInstance().then((clipperInstance) => {
- const geo = clipper.getPathGeo(item, settings.flattenResolution);
- const paths = clipper.getOffsetPaths(geo, settings.inset, clipperInstance);
-
- initCallback(new CompoundPath(paths), settings);
- });
- } else {
- initCallback(item, settings);
- }
- });
-
- // Cancel/quit the process.
- ipc.of[hostname].on('cancel', () => { process.exit(0); });
+export function connect(initCallback) {
+ ipc.connectTo(hostname, () => {
+ // Setup bindings now that the socket is ready.
+ ipc.of[hostname].on('connect', () => {
+ // Connected! Tell the server we're ready for data.
+ send('ready', ipcBase);
});
- },
-
- // Get information about this spawn process.
- info: { ...ipcBase },
- // Report progress on processing.
- progress: (status, value) => {
- send('progress', { ...ipcBase, status, value });
- },
-
- // Final fill paths! Send and host will shutdown when done.
- finish: (paths = {}) => {
- send('complete', {
- ...ipcBase,
- result: paths.exportJSON(), // exp.project.activeLayer.exportJSON()
- });
- },
+ // Bind central init, this gives us everything we need to do the work!
+ ipc.of[hostname].on('spawner.init', ({ size, object, settings }) => {
+ state.project = new Project(new Size(size));
+ const item = state.project.activeLayer.importJSON(object);
+ // console.log('Path:', item.name, `${Math.round(item.length * 10) / 10}mm long`);
- // Get only the ID of closest point in an intersection array.
- getClosestIntersectionID: (srcPoint, points) => {
- let closestID = 0;
- let closest = srcPoint.getDistance(points[0].point);
+ // For any non-zero value, we want to inset the object before it gets in.
+ if (settings.inset) {
+ clipper.getInstance().then(clipperInstance => {
+ const geo = clipper.getPathGeo(item, settings.flattenResolution);
+ const paths = clipper.getOffsetPaths(geo, settings.inset, clipperInstance);
- points.forEach((destPoint, index) => {
- const dist = srcPoint.getDistance(destPoint.point);
- if (dist < closest) {
- closest = dist;
- closestID = index;
+ initCallback(new CompoundPath(paths), settings);
+ });
+ } else {
+ initCallback(item, settings);
}
});
- return closestID;
- },
-
- // Will return true if the given point is in either the top left or bottom
- // right otuside the realm of the bound rect:
- // |
- // (true)| (false)
- // ----------------+
- // | Bounds|
- // (false) |(false)| (false)
- // +----------------
- // (false) | (true)
- // |
- pointBeyond: (point, bounds) => {
- // Outside top left
- if (point.x < bounds.left && point.y < bounds.top) return true;
-
- // Outside bottom right
- if (point.x > bounds.right && point.y > bounds.bottom) return true;
-
- // Otherwise, not.
- return false;
- },
-
- // Add in the Clipper utilities.
- clipper,
-};
-
-process.addListener('uncaughtException', (err) => {
- console.error(err);
- process.exit(1);
-});
-
-module.exports = exp;
+ // Cancel/quit the process.
+ ipc.of[hostname].on('cancel', () => { process.exit(0); });
+ });
+}
+
+// Get information about this spawn process.
+export const info = { ...ipcBase };
+
+// Report progress on processing.
+export function progress(status, value) {
+ send('progress', { ...ipcBase, status, value });
+}
+
+// Final fill paths! Send and host will shutdown when done.
+export function finish(paths = {}) {
+ send('complete', {
+ ...ipcBase,
+ result: paths.exportJSON(), // exp.project.activeLayer.exportJSON()
+ });
+}
+
+// Get only the ID of closest point in an intersection array.
+export function getClosestIntersectionID(srcPoint, points) {
+ let closestID = 0;
+ let closest = srcPoint.getDistance(points[0].point);
+
+ points.forEach((destPoint, index) => {
+ const dist = srcPoint.getDistance(destPoint.point);
+ if (dist < closest) {
+ closest = dist;
+ closestID = index;
+ }
+ });
+
+ return closestID;
+}
+
+// Will return true if the given point is in either the top left or bottom
+// right otuside the realm of the bound rect:
+// |
+// (true)| (false)
+// ----------------+
+// | Bounds|
+// (false) |(false)| (false)
+// +----------------
+// (false) | (true)
+// |
+export function pointBeyond(point, bounds) {
+ // Outside top left
+ if (point.x < bounds.left && point.y < bounds.top) return true;
+
+ // Outside bottom right
+ if (point.x > bounds.right && point.y > bounds.bottom) return true;
+
+ // Otherwise, not.
+ return false;
+}
diff --git a/src/components/core/drawing/fillers/offset/cncserver.drawing.filler.schema.js b/src/components/core/drawing/fillers/offset/cncserver.drawing.filler.schema.js
index 8136af29..80fbf4e6 100644
--- a/src/components/core/drawing/fillers/offset/cncserver.drawing.filler.schema.js
+++ b/src/components/core/drawing/fillers/offset/cncserver.drawing.filler.schema.js
@@ -4,10 +4,28 @@
* This schema defines the fill method specific settings schema for the
* application to import and use for settings validation and documentation.
*/
-
-module.exports = {
+/* eslint-disable max-len */
+const schema = {
+ // === Pattern fill method keys, found @ settings.fill.offset ==============
type: 'object',
- title: 'Offset method options',
+ title: 'Offset Shape Fill',
options: { collapsed: true },
- properties: {},
+ properties: {
+ connectShells: {
+ type: 'boolean',
+ title: 'Connect shells',
+ description: 'If checked, each offset shell will be connected to the previous one, making a spiral. Does not connect multi-shells.',
+ default: false,
+ format: 'checkbox',
+ },
+ fillGaps: {
+ type: 'boolean',
+ title: 'Fill Gaps',
+ description: 'If checked, the fill method will check for single width gaps and fill them.',
+ default: false,
+ format: 'checkbox',
+ },
+ },
};
+
+export default schema;
diff --git a/src/components/core/drawing/fillers/offset/cncserver.drawing.fillers.offset.js b/src/components/core/drawing/fillers/offset/cncserver.drawing.fillers.offset.js
index 54df0903..77624634 100644
--- a/src/components/core/drawing/fillers/offset/cncserver.drawing.fillers.offset.js
+++ b/src/components/core/drawing/fillers/offset/cncserver.drawing.fillers.offset.js
@@ -3,29 +3,44 @@
* "cam" style offset fill utilizing the "clipper" and cam.js libraries.
*/
-const { Group } = require('paper');
-const fillUtil = require('../cncserver.drawing.fillers.util');
+import Paper from 'paper';
+import { connect, clipper, finish } from '../cncserver.drawing.fillers.util.js';
-let settings = {};
-let exportGroup = {};
+const { Group } = Paper;
// Connect to the main process, start the fill operation.
-fillUtil.connect((path, settingsOverride) => {
- fillUtil.clipper.getInstance().then((clipper) => {
- settings = { ...settings, ...settingsOverride };
- exportGroup = new Group();
+connect((path, { flattenResolution, spacing, offset }) => {
+ clipper.getInstance().then(clipperInstance => {
+ const exportGroup = new Group();
let items = [];
let increment = 0;
- const geo = fillUtil.clipper.getPathGeo(path, settings.flattenResolution);
+ const geo = clipper.getPathGeo(path, flattenResolution);
+ let angle = 0;
while (items) {
- items = fillUtil.clipper.getOffsetPaths(geo, settings.spacing + increment, clipper);
+ items = clipper.getOffsetPaths(geo, spacing + increment, clipperInstance);
if (items) {
- exportGroup.addChildren(items);
- increment += settings.spacing;
+ // Try to connect the shells.
+ if (offset.connectShells && exportGroup.lastChild && items.length === 1) {
+ exportGroup.lastChild.closed = false;
+ angle += 4.2;
+ items[0].rotate(angle);
+ items[0].segments.forEach(({ point: { x, y } }) => {
+ exportGroup.lastChild.add([x, y]);
+ });
+ } else {
+ angle += 4.2;
+ items[0].rotate(angle);
+ items[0].smooth();
+ exportGroup.addChildren(items);
+ }
+ increment += spacing;
}
}
- fillUtil.finish(exportGroup);
+ // TODO: Add support for settings.offset.fillGaps
+ // - Add the centerline of the last fill shape(s).
+
+ finish(exportGroup);
});
});
diff --git a/src/components/core/drawing/fillers/pattern/cncserver.drawing.filler.schema.js b/src/components/core/drawing/fillers/pattern/cncserver.drawing.filler.schema.js
index 03f9737c..61b3f036 100644
--- a/src/components/core/drawing/fillers/pattern/cncserver.drawing.filler.schema.js
+++ b/src/components/core/drawing/fillers/pattern/cncserver.drawing.filler.schema.js
@@ -5,11 +5,14 @@
* application to import and use for settings validation and documentation.
*/
/* eslint-disable max-len */
-const fs = require('fs');
-const path = require('path');
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
const svgPatterns = [];
-fs.readdirSync(path.resolve(__dirname, 'patterns')).map((file) => {
+fs.readdirSync(path.resolve(__dirname, 'patterns')).map(file => {
svgPatterns.push(file.split('.')[0]);
});
@@ -90,9 +93,11 @@ const properties = {
},
};
-module.exports = {
+const schema = {
type: 'object',
- title: 'Pattern method options',
+ title: 'Pattern Fill',
options: { collapsed: true },
properties,
};
+
+export default schema;
diff --git a/src/components/core/drawing/fillers/pattern/cncserver.drawing.fillers.pattern.js b/src/components/core/drawing/fillers/pattern/cncserver.drawing.fillers.pattern.js
index 1234c40d..79e17640 100644
--- a/src/components/core/drawing/fillers/pattern/cncserver.drawing.fillers.pattern.js
+++ b/src/components/core/drawing/fillers/pattern/cncserver.drawing.fillers.pattern.js
@@ -2,10 +2,12 @@
* @file Path fill algortihm module: Node filler app for running the pattern
* path fill (overlaying a given large space filling path over the fill object).
*/
-const { Path, Group } = require('paper');
-const fs = require('fs');
-const path = require('path');
-const fillUtil = require('../cncserver.drawing.fillers.util');
+import Paper from 'paper';
+import fs from 'fs';
+import path from 'path';
+import * as fillUtil from '../cncserver.drawing.fillers.util.js';
+
+const { Path, Group } = Paper;
let viewBounds = {};
let settings = {}; // Globalize settings from settings.fill >
@@ -82,7 +84,7 @@ function generateFromLib(name, fillPath) {
let pattern = root.clone();
- // With our pattern, we have to tile it along X and y until it fits the bounds of our path.
+ // With our pattern, we tile it along X and y until it fits the bounds of our path.
const { bounds } = fillPath;
// Tile till we reach the width.
@@ -221,7 +223,7 @@ function applyDensity(density, singlePath) {
break;
}
- densityArray.forEach((angle) => {
+ densityArray.forEach(angle => {
patternGroup.addChild(singlePath.clone().rotate(angle));
});
@@ -273,9 +275,9 @@ function generatePattern(name, fillPath) {
// Actually connect to the main process, start the fill operation.
fillUtil.connect((fillPath, rawSettings) => {
- fillUtil.clipper.getInstance().then((clipper) => {
+ fillUtil.clipper.getInstance().then(clipper => {
settings = { ...rawSettings };
- viewBounds = fillUtil.project.view.bounds;
+ viewBounds = fillUtil.state.project.view.bounds;
const pattern = generatePattern(settings.pattern.pattern, fillPath);
// Convert the paths to clipper geometry.
diff --git a/src/components/core/drawing/index.js b/src/components/core/drawing/index.js
index 6b82cbaf..afbb64b2 100644
--- a/src/components/core/drawing/index.js
+++ b/src/components/core/drawing/index.js
@@ -1,22 +1,17 @@
/**
* @file Index for all high level drawing components.
*/
-/* eslint-disable global-require */
-const drawing = {}; // Conglomerated drawing export.
-module.exports = (cncserver) => {
- // TODO: build with an array loop
- drawing.base = require('./cncserver.drawing.base.js')(cncserver);
- drawing.occlusion = require('./cncserver.drawing.occlusion.js')(cncserver, drawing);
- drawing.trace = require('./cncserver.drawing.trace.js')(cncserver, drawing);
- drawing.spawner = require('./cncserver.drawing.spawner.js')(cncserver, drawing);
- drawing.fill = require('./cncserver.drawing.fill.js')(cncserver, drawing);
- drawing.vectorize = require('./cncserver.drawing.vectorize.js')(cncserver, drawing);
- drawing.text = require('./cncserver.drawing.text.js')(cncserver, drawing);
- drawing.accell = require('./cncserver.drawing.accell.js')(cncserver, drawing);
- drawing.colors = require('./cncserver.drawing.colors.js')(cncserver, drawing);
- drawing.stage = require('./cncserver.drawing.stage.js')(cncserver, drawing);
- drawing.preview = require('./cncserver.drawing.preview.js')(cncserver, drawing);
- drawing.temp = require('./cncserver.drawing.temp.js')(cncserver, drawing);
- return drawing;
-};
+export * as base from 'cs/drawing/base';
+export { default as fill } from 'cs/drawing/fill';
+export { default as vectorize } from 'cs/drawing/vectorize';
+export { default as occlusion } from 'cs/drawing/occlusion';
+export { default as trace } from 'cs/drawing/trace';
+export { default as spawner } from 'cs/drawing/spawner';
+export * as text from 'cs/drawing/text';
+export * as accell from 'cs/drawing/accell';
+export * as colors from 'cs/drawing/colors';
+export * as implements from 'cs/drawing/implements';
+export * as stage from 'cs/drawing/stage';
+export * as preview from 'cs/drawing/preview';
+export * as temp from 'cs/drawing/temp';
diff --git a/src/components/core/drawing/vectorizers/basic/cncserver.drawing.vectorizer.schema.js b/src/components/core/drawing/vectorizers/basic/cncserver.drawing.vectorizer.schema.js
index fc29fa24..09643b45 100644
--- a/src/components/core/drawing/vectorizers/basic/cncserver.drawing.vectorizer.schema.js
+++ b/src/components/core/drawing/vectorizers/basic/cncserver.drawing.vectorizer.schema.js
@@ -48,9 +48,11 @@ const properties = {
},
};
-module.exports = {
+const schema = {
type: 'object',
- title: 'Basic method options',
+ title: 'Basic Autotrace Vectorization',
options: { collapsed: true },
properties,
};
+
+export default schema;
diff --git a/src/components/core/drawing/vectorizers/basic/cncserver.drawing.vectorizers.basic.js b/src/components/core/drawing/vectorizers/basic/cncserver.drawing.vectorizers.basic.js
index 690a1d46..4753374f 100644
--- a/src/components/core/drawing/vectorizers/basic/cncserver.drawing.vectorizers.basic.js
+++ b/src/components/core/drawing/vectorizers/basic/cncserver.drawing.vectorizers.basic.js
@@ -1,19 +1,21 @@
/**
* @file Basic vectorizer using autotrace.
*/
-
-const { Group, CompoundPath, Color } = require('paper');
-const { exec } = require('child_process');
-const { tmpdir } = require('os');
-const fs = require('fs');
-const path = require('path');
-const jsdom = require('jsdom');
-const util = require('../cncserver.drawing.vectorizers.util');
-
+import Paper from 'paper';
+import { exec } from 'child_process';
+import { tmpdir } from 'os';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import jsdom from 'jsdom';
+import { connect, finish, info } from '../cncserver.drawing.vectorizers.util.js';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
const { JSDOM } = jsdom;
+const { Group, CompoundPath, Color } = Paper;
const bin = path.resolve(__dirname, 'bin', process.platform, 'autotrace');
const tmp = path.resolve(tmpdir());
-const outputFile = path.resolve(tmp, `autotrace_output_${util.info.hash}.svg`);
+const outputFile = path.resolve(tmp, `autotrace_output_${info.hash}.svg`);
let settings = { }; // Globalize contents of filler.vectorize >
@@ -59,7 +61,7 @@ const buildOptions = () => {
};
// Connect to the main process, start the vectorization operation.
-util.connect('bmp', (inputImagePath, rawSettings) => {
+connect('bmp', (inputImagePath, rawSettings) => {
settings = { ...rawSettings };
// Remove any previous run output file.
@@ -71,11 +73,11 @@ util.connect('bmp', (inputImagePath, rawSettings) => {
const child = exec(fullExec);
- child.stderr.on('data', (data) => {
+ child.stderr.on('data', data => {
console.log('ERROR:', data);
});
- child.stdout.on('data', (data) => {
+ child.stdout.on('data', data => {
console.log('Output:', data);
});
@@ -88,12 +90,12 @@ util.connect('bmp', (inputImagePath, rawSettings) => {
const svg = fs.readFileSync(outputFile, 'utf8');
const dom = new JSDOM(svg, { contentType: 'text/xml' });
const paths = dom.window.document.querySelectorAll('path');
- paths.forEach((pathItem) => {
+ paths.forEach(pathItem => {
const styleParts = pathItem.getAttribute('style').split(';');
const styles = { fill: null, stroke: null };
- styleParts.forEach((item) => {
+ styleParts.forEach(item => {
const part = item.split(':');
- styles[part[0]] = part[1];
+ [, styles[part[0]]] = part;
});
exportGroup.addChild(
@@ -107,10 +109,10 @@ util.connect('bmp', (inputImagePath, rawSettings) => {
});
exportGroup.fitBounds(settings.bounds);
- util.finish(exportGroup);
+ finish(exportGroup);
} else {
console.log('No output file 😢', outputFile);
- util.finish(new Group());
+ finish(new Group());
}
});
});
diff --git a/src/components/core/drawing/vectorizers/cncserver.drawing.vectorizers.util.js b/src/components/core/drawing/vectorizers/cncserver.drawing.vectorizers.util.js
index 6640b432..bb97f71c 100644
--- a/src/components/core/drawing/vectorizers/cncserver.drawing.vectorizers.util.js
+++ b/src/components/core/drawing/vectorizers/cncserver.drawing.vectorizers.util.js
@@ -4,14 +4,15 @@
* Holds all standardized processes for managing IPC, paper setup, and export
* so the vectorization algorithm can do whatever it needs.
*/
-const {
- Project, Size, Path, Rectangle, Raster, Color,
-} = require('paper');
-const { tmpdir } = require('os');
-const ipc = require('node-ipc');
-const Jimp = require('jimp');
-const path = require('path');
+import Paper from 'paper';
+import { tmpdir } from 'os';
+import ipc from 'node-ipc';
+import Jimp from 'jimp';
+import path from 'path';
+const {
+ Project, Size, Path, Rectangle, Raster, Color
+} = Paper;
const hostname = 'cncserver';
const ipcBase = {
hash: process.argv[2],
@@ -19,157 +20,161 @@ const ipcBase = {
type: 'vectorizer',
};
+// Vectorizer state.
+export const state = {
+ project: {}, // Paper project placeholder.
+};
+
// Config IPC.
ipc.config.silent = true;
+// Catch errors and quit cleanly.
+process.addListener('uncaughtException', err => {
+ console.error(err);
+ process.exit(1);
+});
+
// Generic message sender.
const send = (command, data = {}) => {
const packet = { command, data };
ipc.of[hostname].emit('spawner.message', packet);
};
-const exp = {
- connect: (outputFormat, initCallback) => {
- ipc.connectTo(hostname, () => {
- // Setup bindings now that the socket is ready.
- ipc.of[hostname].on('connect', () => {
- // Connected! Tell the server we're ready for data.
- send('ready', ipcBase);
- });
+export function connect(outputFormat, initCallback) {
+ ipc.connectTo(hostname, () => {
+ // Setup bindings now that the socket is ready.
+ ipc.of[hostname].on('connect', () => {
+ // Connected! Tell the server we're ready for data.
+ send('ready', ipcBase);
+ });
- // Bind central init, this gives us everything we need to do the work!
- ipc.of[hostname].on('spawner.init', ({ size, object: imagePath, settings }) => {
- exp.project = new Project(new Size(size));
-
- // Use JIMP to apply raster/pixel based modifications & work from that.
- const ext = outputFormat === 'paper' ? 'png' : outputFormat;
- const outputImagePath = path.resolve(
- tmpdir(),
- `cncserver_vectorizer_image_${ipcBase.hash}.${ext}`
- );
- const { raster } = settings;
-
- // eslint-disable-next-line no-eval
- const flattenColor = eval(
- `${new Color(raster.flattenColor).toCSS(true).replace('#', '0x')}ff`
- );
- Jimp.read(imagePath).then(img => new Promise((resolve) => {
- img.brightness(raster.brightness);
- img.contrast(raster.contrast);
-
- if (raster.grayscale) img.greyscale(); // Spelling much? 🤣
- if (raster.invert) img.invert();
- if (raster.normalize) img.normalize();
- if (raster.blur) img.blur(raster.blur);
- if (raster.flatten || ext !== 'png') img.background(flattenColor);
- // TODO: apply image resolution adjustment/resize.
-
- return img.write(outputImagePath, resolve);
- })).then(() => {
- // If the vectorizer requests paper, load that in from the JIMP output.
- if (outputFormat === 'paper') {
- const item = new Raster(outputImagePath);
- item.onLoad = () => {
- item.fitBounds(settings.bounds);
- initCallback(item, settings);
- };
- } else {
- // Otherwise, just hand it the output image path.
- initCallback(outputImagePath, settings);
- }
- });
+ // Bind central init, this gives us everything we need to do the work!
+ ipc.of[hostname].on('spawner.init', ({ size, object: imagePath, settings }) => {
+ state.project = new Project(new Size(size));
+
+ // Use JIMP to apply raster/pixel based modifications & work from that.
+ const ext = outputFormat === 'paper' ? 'png' : outputFormat;
+ const outputImagePath = path.resolve(
+ tmpdir(),
+ `cncserver_vectorizer_image_${ipcBase.hash}.${ext}`
+ );
+ const { raster } = settings;
+
+ // eslint-disable-next-line no-eval
+ const flattenColor = eval(
+ `${new Color(raster.flattenColor).toCSS(true).replace('#', '0x')}ff`
+ );
+ Jimp.read(imagePath).then(img => new Promise(resolve => {
+ img.brightness(raster.brightness);
+ img.contrast(raster.contrast);
+
+ if (raster.grayscale) img.greyscale(); // Spelling much? 🤣
+ if (raster.invert) img.invert();
+ if (raster.normalize) img.normalize();
+ if (raster.blur) img.blur(raster.blur);
+ if (raster.flatten || ext !== 'png') img.background(flattenColor);
+ // TODO: apply image resolution adjustment/resize.
+
+ return img.write(outputImagePath, resolve);
+ })).then(() => {
+ // If the vectorizer requests paper, load that in from the JIMP output.
+ if (outputFormat === 'paper') {
+ const item = new Raster(outputImagePath);
+ item.onLoad = () => {
+ item.fitBounds(settings.bounds);
+ initCallback(item, settings);
+ };
+ } else {
+ // Otherwise, just hand it the output image path.
+ initCallback(outputImagePath, settings);
+ }
});
-
- // Cancel/quit the process.
- ipc.of[hostname].on('cancel', () => { process.exit(0); });
});
- },
- // Get information about this spawn process.
- info: { ...ipcBase },
+ // Cancel/quit the process.
+ ipc.of[hostname].on('cancel', () => { process.exit(0); });
+ });
+}
- // Report progress on processing.
- progress: (status, value) => {
- send('progress', { ...ipcBase, status, value });
- },
+// Get information about this spawn process.
+export const info = { ...ipcBase };
- // Final vectorized paths! Send and shutdown when done.
- finish: (paths = {}) => {
- send('complete', {
- ...ipcBase,
- result: paths.exportJSON(), // exp.project.activeLayer.exportJSON()
- });
- },
-
- /**
- * Map a value in a given range to a new range.
- *
- * @param {number} x
- * The input number to be mapped.
- * @param {number} inMin
- * Expected minimum of the input number.
- * @param {number} inMax
- * Expected maximum of the input number.
- * @param {number} outMin
- * Expected minimum of the output map.
- * @param {number} outMax
- * Expected maximum of the output map.
- *
- * @return {number}
- * The output number after mapping.
- */
- map: (x, inMin, inMax, outMin, outMax) => (x - inMin) * (outMax - outMin) / (inMax - inMin) + outMin,
-
- generateSpiralPath: (inputSpacing, spiralBounds, offset = { x: 0, y: 0 }) => {
- // Calculate a single spiral coordinate given a distance and spacing.
- function calculateSpiral(distance, spacing = 1, center) {
- return {
- x: (spacing * distance * Math.cos(distance)) + center.x,
- y: (spacing * distance * Math.sin(distance)) + center.y,
- };
- }
+// Report progress on processing.
+export function progress(status, value) {
+ send('progress', { ...ipcBase, status, value });
+}
- // Make a list of points along a spiral.
- function makeSpiral(turns, spacing, startOn, center) {
- const start = startOn ? startOn * Math.PI * 2 : 0;
- const points = [];
- const stepAngle = Math.PI / 4; // We want at least 8 points per turn
+// Final vectorized paths! Send and shutdown when done.
+export function finish(paths = {}) {
+ send('complete', {
+ ...ipcBase,
+ result: paths.exportJSON(), // state.project.activeLayer.exportJSON()
+ });
+}
- for (let i = start; i < turns * Math.PI * 2; i += stepAngle) {
- points.push(calculateSpiral(i, spacing, center));
- }
-
- return points;
+/**
+ * Map a value in a given range to a new range.
+ *
+ * @param {number} x
+ * The input number to be mapped.
+ * @param {number} inMin
+ * Expected minimum of the input number.
+ * @param {number} inMax
+ * Expected maximum of the input number.
+ * @param {number} outMin
+ * Expected minimum of the output map.
+ * @param {number} outMax
+ * Expected maximum of the output map.
+ *
+ * @return {number}
+ * The output number after mapping.
+ */
+export function map(x, inMin, inMax, outMin, outMax) {
+ return ((x - inMin) * (outMax - outMin)) / ((inMax - inMin) + outMin)
+}
+
+export function generateSpiralPath(inputSpacing, spiralBounds, offset = { x: 0, y: 0 }) {
+ // Calculate a single spiral coordinate given a distance and spacing.
+ function calculateSpiral(distance, spacing = 1, center) {
+ return {
+ x: (spacing * distance * Math.cos(distance)) + center.x,
+ y: (spacing * distance * Math.sin(distance)) + center.y,
+ };
+ }
+
+ // Make a list of points along a spiral.
+ function makeSpiral(turns, spacing, startOn, center) {
+ const start = startOn ? startOn * Math.PI * 2 : 0;
+ const points = [];
+ const stepAngle = Math.PI / 4; // We want at least 8 points per turn
+
+ for (let i = start; i < turns * Math.PI * 2; i += stepAngle) {
+ points.push(calculateSpiral(i, spacing, center));
}
- // Setup the spiral:
- const viewBounds = new Rectangle(spiralBounds) || exp.project.view.bounds;
- const spiral = new Path();
- const spacing = inputSpacing / (Math.PI * 2);
+ return points;
+ }
- // This is the diagonal distance of the area in which we will be filling.
- let boundsSize = viewBounds.topLeft.getDistance(viewBounds.bottomRight) / 2;
+ // Setup the spiral:
+ const viewBounds = new Rectangle(spiralBounds) || state.project.view.bounds;
+ const spiral = new Path();
+ const spacing = inputSpacing / (Math.PI * 2);
- // Add extra for offset.
- boundsSize += viewBounds.center.getDistance(viewBounds.center.add(offset)) / 2;
+ // This is the diagonal distance of the area in which we will be filling.
+ let boundsSize = viewBounds.topLeft.getDistance(viewBounds.bottomRight) / 2;
- // Estimate the number of turns based on the spacing and boundsSize
- const turns = Math.ceil(boundsSize / (spacing * 2 * Math.PI));
+ // Add extra for offset.
+ boundsSize += viewBounds.center.getDistance(viewBounds.center.add(offset)) / 2;
- spiral.position = viewBounds.center.add(offset);
- spiral.addSegments(makeSpiral(turns, spacing, null, spiral.position));
+ // Estimate the number of turns based on the spacing and boundsSize
+ const turns = Math.ceil(boundsSize / (spacing * 2 * Math.PI));
- spiral.smooth();
+ spiral.position = viewBounds.center.add(offset);
+ spiral.addSegments(makeSpiral(turns, spacing, null, spiral.position));
- // The last few segments are not spiralular so remove them
- spiral.removeSegments(spiral.segments.length - 4);
- return spiral;
- },
-};
-
-process.addListener('uncaughtException', (err) => {
- console.error(err);
- process.exit(1);
-});
+ spiral.smooth();
-module.exports = exp;
+ // The last few segments are not spiralular so remove them
+ spiral.removeSegments(spiral.segments.length - 4);
+ return spiral;
+}
diff --git a/src/components/core/drawing/vectorizers/squiggle/cncserver.drawing.vectorizer.schema.js b/src/components/core/drawing/vectorizers/squiggle/cncserver.drawing.vectorizer.schema.js
index 7988b25b..a206baaa 100644
--- a/src/components/core/drawing/vectorizers/squiggle/cncserver.drawing.vectorizer.schema.js
+++ b/src/components/core/drawing/vectorizers/squiggle/cncserver.drawing.vectorizer.schema.js
@@ -132,9 +132,11 @@ const properties = {
},
};
-module.exports = {
+const schema = {
type: 'object',
- title: 'Squiggle method options',
+ title: 'Squiggle Density Vectorization',
options: { collapsed: true },
properties,
};
+
+export default schema;
diff --git a/src/components/core/drawing/vectorizers/squiggle/cncserver.drawing.vectorizers.squiggle.js b/src/components/core/drawing/vectorizers/squiggle/cncserver.drawing.vectorizers.squiggle.js
index c108f2f4..9464cdd5 100644
--- a/src/components/core/drawing/vectorizers/squiggle/cncserver.drawing.vectorizers.squiggle.js
+++ b/src/components/core/drawing/vectorizers/squiggle/cncserver.drawing.vectorizers.squiggle.js
@@ -1,10 +1,15 @@
/**
* @file Squiggle vectorizer!
*/
-
-const { Group, Path } = require('paper');
-const util = require('../cncserver.drawing.vectorizers.util');
-
+import Paper from 'paper';
+import {
+ connect,
+ finish,
+ generateSpiralPath,
+ map
+} from '../cncserver.drawing.vectorizers.util.js';
+
+const { Group, Path } = Paper;
const components = {
cyan: 'red',
magenta: 'green',
@@ -54,7 +59,7 @@ function renderSquiggleAlongPath(image, path, colorComponent) {
const color = image.getAverageColor(area);
if (color) {
const lum = 1 - bright(color[components[colorComponent]], 0.1);
- const amp = util.map(lum, 0, 1, 0, (spacing + overlap) / 2);
+ const amp = map(lum, 0, 1, 0, (spacing + overlap) / 2);
const density = Math.ceil(maxDensity * lum);
const pointSpacing = sampleWidth / density;
@@ -101,7 +106,7 @@ function renderSquiggleAlongPath(image, path, colorComponent) {
}
// Connect to the main process, start the fill operation.
-util.connect('paper', (image, rawSettings) => {
+connect('paper', (image, rawSettings) => {
settings = { ...rawSettings };
// Convert color components from the schema verifiable object into the array
@@ -122,7 +127,9 @@ util.connect('paper', (image, rawSettings) => {
if (style === 'lines') {
// Build group with double length, double height lines to overlay
const guideLines = new Group();
- for (let y = image.bounds.top + spacing / 2; y < image.bounds.bottom + image.bounds.height; y = y + spacing - overlap) {
+ const yTop = image.bounds.top + spacing / 2;
+ const yBottom = image.bounds.bottom + image.bounds.height;
+ for (let y = yTop; y < yBottom; y = y + spacing - overlap) {
guideLines.addChild(new Path([
[image.bounds.left - image.bounds.width, y],
[image.bounds.right, y],
@@ -137,7 +144,7 @@ util.connect('paper', (image, rawSettings) => {
guideLines.rotation = angle + ((360 / colorComponents.length) * index);
// Render squiggles for each guide line.
- guideLines.children.forEach((guideLine) => {
+ guideLines.children.forEach(guideLine => {
// Flip every other line around for easy reconnection
if (evenOdd === null) {
evenOdd = true;
@@ -161,12 +168,12 @@ util.connect('paper', (image, rawSettings) => {
});
});
} else if (style === 'spiral') {
- const spiral = util.generateSpiralPath(spacing, image.bounds, settings.squiggle.offset);
+ const spiral = generateSpiralPath(spacing, image.bounds, settings.squiggle.offset);
colorComponents.forEach((colorComponent, index) => {
spiral.rotation = angle + ((360 / colorComponents.length) * index);
renderSquiggleAlongPath(image, spiral, colorComponent);
});
}
- util.finish(exportGroup);
+ finish(exportGroup);
});
diff --git a/src/components/core/drawing/vectorizers/stipple/cncserver.drawing.vectorizer.schema.js b/src/components/core/drawing/vectorizers/stipple/cncserver.drawing.vectorizer.schema.js
index 8791082f..fed47395 100644
--- a/src/components/core/drawing/vectorizers/stipple/cncserver.drawing.vectorizer.schema.js
+++ b/src/components/core/drawing/vectorizers/stipple/cncserver.drawing.vectorizer.schema.js
@@ -56,9 +56,11 @@ const properties = {
},
};
-module.exports = {
+const schema = {
type: 'object',
- title: 'Stipple method options',
+ title: 'Stipple Dot Vectorization',
options: { collapsed: true },
properties,
};
+
+export default schema;
diff --git a/src/components/core/drawing/vectorizers/stipple/cncserver.drawing.vectorizers.stipple.js b/src/components/core/drawing/vectorizers/stipple/cncserver.drawing.vectorizers.stipple.js
index 495ed758..5a54f5e4 100644
--- a/src/components/core/drawing/vectorizers/stipple/cncserver.drawing.vectorizers.stipple.js
+++ b/src/components/core/drawing/vectorizers/stipple/cncserver.drawing.vectorizers.stipple.js
@@ -1,15 +1,16 @@
+/* eslint-disable no-param-reassign */
/**
* @file Stipple vectorizer
*/
+import Paper from 'paper';
+import { exec } from 'child_process';
+import { tmpdir } from 'os';
+import fs from 'fs';
+import path from 'path';
+import * as utils from '../cncserver.drawing.vectorizers.util.js';
-const { Group } = require('paper');
-const { exec } = require('child_process');
-const { tmpdir } = require('os');
-const fs = require('fs');
-const path = require('path');
-const util = require('../cncserver.drawing.vectorizers.util');
-
-const bin = path.resolve(__dirname, 'bin', process.platform, 'voronoi_stippler');
+const { Group } = Paper;
+const bin = path.resolve('.', 'bin', process.platform, 'voronoi_stippler');
const tmp = tmpdir();
let settings = { }; // Globalize settings.vectorize >
@@ -41,8 +42,8 @@ const buildOptions = () => {
};
// Connect to the main process, start the vectorization operation.
-util.connect('png', (input, rawSettings) => {
- const output = path.resolve(tmp, `stipple_output_${util.info.hash}.svg`);
+utils.connect('png', (input, rawSettings) => {
+ const output = path.resolve(tmp, `stipple_output_${utils.info.hash}.svg`);
settings = {
...rawSettings.stipple,
input,
@@ -58,11 +59,11 @@ util.connect('png', (input, rawSettings) => {
console.log('Executing:', fullExec);
const child = exec(fullExec);
- child.stderr.on('data', (data) => {
+ child.stderr.on('data', data => {
console.error('ERROR:', data);
});
- child.stdout.on('data', (data) => {
+ child.stdout.on('data', data => {
if (data.includes('% Complete')) {
const progress = Math.min(100, parseInt(data.split('%')[0], 10));
console.log('Progress:', progress);
@@ -73,23 +74,23 @@ util.connect('png', (input, rawSettings) => {
if (fs.existsSync(output)) {
console.log('Importing file', output);
- util.project.importSVG(output, {
+ utils.state.project.importSVG(output, {
expandShapes: true,
- onLoad: (group) => {
+ onLoad: group => {
group.fitBounds(rawSettings.bounds);
// Should be a flat list of circles converted to 4 segment paths.
- group.children.forEach((item) => {
+ group.children.forEach(item => {
item.strokeColor = item.fillColor;
item.strokeWidth = 0.5;
item.fillColor = null;
});
- util.finish(group);
+ utils.finish(group);
},
});
} else {
console.log('No output file 😢', output);
- util.finish(new Group());
+ utils.finish(new Group());
}
});
});
diff --git a/src/components/core/runner/cncserver.runner.ipc.js b/src/components/core/runner/cncserver.runner.ipc.js
index 68ff2779..e0552657 100644
--- a/src/components/core/runner/cncserver.runner.ipc.js
+++ b/src/components/core/runner/cncserver.runner.ipc.js
@@ -2,9 +2,9 @@
* @file CNC Server runner IPC wrapper code, used to manage communication and
* events between processes.
*/
-const ipc = require('node-ipc');
+import ipc from 'node-ipc';
-module.exports = (options) => {
+export default function initIPC(options) {
// Setup passed config.
for (const [key, val] of Object.entries(options.config)) {
ipc.config[key] = val;
@@ -57,4 +57,4 @@ module.exports = (options) => {
};
return exp;
-};
+}
diff --git a/src/components/core/runner/cncserver.runner.js b/src/components/core/runner/cncserver.runner.js
index ff83fc66..40cac0be 100644
--- a/src/components/core/runner/cncserver.runner.js
+++ b/src/components/core/runner/cncserver.runner.js
@@ -7,10 +7,13 @@
* socket messages, always use the API to communicate via serial, not this.
*/
-// REQUIRES ====================================================================
-const { Readable, Writable } = require('stream');
-const serial = require('./cncserver.runner.serial');
-const ipc = require('./cncserver.runner.ipc')({
+// Imports ====================================================================
+import { Readable, Writable } from 'stream';
+
+import * as serial from './cncserver.runner.serial.js';
+import initIPC from './cncserver.runner.ipc.js';
+
+const ipc = initIPC({
ipcHost: 'cncserver',
config: {
id: 'cncrunner',
@@ -56,7 +59,7 @@ function instructionStreamWrite(item, _, callback) {
// Some items don't have any rendered commands, only run those that do!
if (item.commands.length) {
durationTimer = new Date();
- serial.writeMultiple(item.commands, (err) => {
+ serial.writeMultiple(item.commands, err => {
setTimeout(() => {
ipc.sendMessage('buffer.item.done', item.hash);
state.instructionIsExecuting = false;
@@ -135,7 +138,7 @@ const directRenderer = new Writable({
highWaterMark: 1,
write: (item, _, callback) => {
if (global.config.showSerial) console.log('DIRECT Stream writing!', item.commands);
- serial.writeMultiple(item.commands, (err) => {
+ serial.writeMultiple(item.commands, err => {
setTimeout(() => {
callback(err);
}, item.duration);
@@ -167,7 +170,7 @@ state.setPaused = (paused, init = false) => {
/**
* Simulation state setter
*/
-state.setSimulation = (isSimulating) => {
+state.setSimulation = isSimulating => {
state.simulation = isSimulating;
ipc.sendMessage('serial.simulation', isSimulating);
};
@@ -202,7 +205,7 @@ serial.bindAll({
},
// Called on serial close if reconnect fails.
- close: (err) => {
+ close: err => {
console.log(err);
console.log(`Serial Disconnected: ${err.toString()}`);
ipc.sendMessage('serial.disconnected', {
@@ -274,7 +277,7 @@ function gotMessage(packet) {
}
// Catch any uncaught error.
-process.on('uncaughtException', (err) => {
+process.on('uncaughtException', err => {
// Assume Disconnection and kill the process.
serial.triggerBind('disconnect', err);
console.error('Uncaught error, disconnected from server, shutting down');
diff --git a/src/components/core/runner/cncserver.runner.serial.js b/src/components/core/runner/cncserver.runner.serial.js
index 83b472c0..84ef6a3b 100644
--- a/src/components/core/runner/cncserver.runner.serial.js
+++ b/src/components/core/runner/cncserver.runner.serial.js
@@ -3,21 +3,26 @@
* serial writes and batch processes.
*/
// Base serialport module globals.
-const SerialPort = require('serialport');
-
+import SerialPort from 'serialport';
// State vars.
let port;
let simulation = true;
+let bindings = {};
+
// Event bindings object and helpers.
-let bindings = {
- triggerBind: (name, ...args) => {
- if (bindings[name] && typeof bindings[name] === 'function') {
- bindings[name](...args);
- }
- },
-};
+export function triggerBind(name, ...args) {
+ if (bindings[name] && typeof bindings[name] === 'function') {
+ bindings[name](...args);
+ }
+}
+
+bindings = { triggerBind };
+
+export function bindAll(newBindings = {}) {
+ bindings = { ...bindings, ...newBindings };
+}
/**
* Setter for simulation state change.
@@ -27,7 +32,7 @@ let bindings = {
* @param {function} callback
* Callback when it should be sent/drained.
*/
-const setSimulation = (status) => {
+const setSimulation = status => {
simulation = !!status;
bindings.triggerBind('simulation', simulation);
};
@@ -40,7 +45,7 @@ const setSimulation = (status) => {
* @param {function} callback
* Callback when it should be sent/drained.
*/
-const write = (command, callback) => {
+export function write(command, callback) {
if (simulation) {
if (global.config.showSerial) console.info(`Simulating serial write: ${command}`);
setTimeout(() => {
@@ -71,7 +76,7 @@ const write = (command, callback) => {
if (callback) callback(err);
}
}
-};
+}
/**
* Execute a set of commands representing a single buffer action item to write,
@@ -83,7 +88,7 @@ const write = (command, callback) => {
* @returns {boolean}
* True if success, false if failure
*/
-const writeMultiple = (commands, callback, index = 0) => {
+export function writeMultiple(commands, callback, index = 0) {
// Ensure commands is an array if only one sent.
if (typeof commands === 'string') {
// eslint-disable-next-line no-param-reassign
@@ -91,7 +96,7 @@ const writeMultiple = (commands, callback, index = 0) => {
}
// Run the command at the index.
- write(commands[index], (err) => {
+ write(commands[index], err => {
// eslint-disable-next-line no-param-reassign
index++; // Increment the index.
@@ -113,7 +118,7 @@ const writeMultiple = (commands, callback, index = 0) => {
});
return true;
-};
+}
let retries = 0;
const handleConnectionError = (err, options) => {
@@ -124,7 +129,7 @@ const handleConnectionError = (err, options) => {
retries++;
console.log(`Serial connection to "${options.port}" failed, retrying ${retries}/${options.autoReconnectTries}`);
setTimeout(() => {
- module.exports.connect(options);
+ connect(options);
}, options.autoReconnectRate);
} else {
setSimulation(true);
@@ -134,13 +139,13 @@ const handleConnectionError = (err, options) => {
};
// Exported connection function.
-const connect = (options) => {
+export function connect(options) {
if (global.debug) console.log(`Connect to: ${JSON.stringify(options)}`);
global.connectOptions = options;
// Note: runner doesn't do autodetection.
try {
- port = new SerialPort(options.port, options, (err) => {
+ port = new SerialPort(options.port, options, err => {
if (!err) {
retries = 0;
setSimulation(false);
@@ -150,7 +155,7 @@ const connect = (options) => {
// Send setup commands
if (options.setupCommands.length) {
console.log('Sending bot specific board setup...');
- writeMultiple(options.setupCommands, (error) => {
+ writeMultiple(options.setupCommands, error => {
if (global.debug && error) {
console.log(`SerialPort says: ${error.toString()}`);
}
@@ -162,7 +167,7 @@ const connect = (options) => {
// Bind read, reconnect logic and close/disconnect.
parser.on('data', bindings.read);
- port.on('close', (error) => {
+ port.on('close', error => {
if (error.disconnect) bindings.triggerBind('disconnect');
// If we got disconnected, throw to the try/catch for reconnect.
handleConnectionError(error, options);
@@ -174,15 +179,4 @@ const connect = (options) => {
} catch (err) {
handleConnectionError(err, options);
}
-};
-
-// Build direct export object.
-module.exports = {
- connect,
- write,
- writeMultiple,
- triggerBind: bindings.triggerBind,
- bindAll: (newBindings = {}) => {
- bindings = { ...bindings, ...newBindings };
- },
-};
+}
diff --git a/src/components/core/runner/runner.html b/src/components/core/runner/runner.html
deleted file mode 100644
index 108e9553..00000000
--- a/src/components/core/runner/runner.html
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
- This file is meant to be the src of a webview tag, run in an Electron Shared node context. It won't do much outside of that :)
- See the implementation within RoboPaint for an idea of how this is meant to work.
-
-
diff --git a/src/components/core/schemas/cncserver.schemas.color.js b/src/components/core/schemas/cncserver.schemas.color.js
new file mode 100644
index 00000000..4a46f7e0
--- /dev/null
+++ b/src/components/core/schemas/cncserver.schemas.color.js
@@ -0,0 +1,121 @@
+/**
+ * @file Colorset item settings schema.
+ *
+ */
+/* eslint-disable max-len */
+const properties = {
+ id: {
+ type: 'string',
+ title: 'ID',
+ description: 'Tool or machine name for the color.',
+ },
+ name: {
+ type: 'string',
+ title: 'Name of colorset item',
+ description: 'Description of this colorset item',
+ },
+ color: {
+ type: 'string',
+ format: 'color',
+ title: 'Color',
+ description: 'Color that this item represents. Will be used to select areas to be printed with it.',
+ },
+ printWeight: {
+ type: 'integer',
+ title: 'Print Weighting',
+ description: 'Sets printing order for each colorset item work grouping. Lower goes first, higher goes last.',
+ format: 'range',
+ minimum: -30,
+ maximum: 30,
+ default: 0,
+ },
+ selectionMethod: {
+ type: 'string',
+ title: 'Selection Method',
+ description: 'When parsing the drawing, how should this be selected?',
+ enum: ['color', 'opacity', 'strokeWidth'],
+ options: { enum_titles: ['Color', 'Opacity', 'Stroke Width'] },
+ default: 'color',
+ },
+ selectionOverrides: {
+ type: 'array',
+ title: 'Selection Overrides',
+ description: 'Set of colors that will override all other selection methods.',
+ items: {
+ type: 'string',
+ format: 'color',
+ title: 'Path Color',
+ },
+ default: [],
+ },
+ colorWeight: {
+ type: 'number',
+ title: 'Color Weighting',
+ description: 'Amount to adjust for color selection preference. Smaller than 0 selects less often, larger than 0 selects more often.',
+ format: 'range',
+ minimum: -1,
+ maximum: 1,
+ step: 0.01,
+ default: 0,
+ options: { dependencies: { selectionMethod: 'color' } },
+ },
+ opacityMin: {
+ type: 'number',
+ title: 'Minimum Opacity',
+ description: 'Opacity must be ABOVE this for opacity selection method.',
+ format: 'range',
+ minimum: 0,
+ maximum: 0.99,
+ step: 0.01,
+ default: 0,
+ options: { dependencies: { selectionMethod: 'opacity' } },
+ },
+ opacityMax: {
+ type: 'number',
+ title: 'Maximum Opacity',
+ description: 'Opacity must be BELOW this for opacity selection method.',
+ format: 'range',
+ minimum: 0.01,
+ maximum: 1,
+ default: 0.75,
+ step: 0.01,
+ options: { dependencies: { selectionMethod: 'opacity' } },
+ },
+ strokeWidthMin: {
+ type: 'number',
+ title: 'Minimum Stroke Width',
+ description: 'Stroke width must be ABOVE this for stroke width selection method.',
+ format: 'range',
+ minimum: 0,
+ maximum: 39.99,
+ step: 0.01,
+ default: 0,
+ options: { dependencies: { selectionMethod: 'strokeWidth' } },
+ },
+ strokeWidthMax: {
+ type: 'number',
+ title: 'Maximum Stroke Width',
+ description: 'Stroke width must be BELOW this for stroke width selection method.',
+ format: 'range',
+ minimum: 0.01,
+ maximum: 40,
+ step: 0.01,
+ default: 2,
+ options: { dependencies: { selectionMethod: 'strokeWidth' } },
+ },
+ implement: {
+ type: 'string',
+ title: 'Implement',
+ description: 'Machine name of valid implement to draw with, or "[inherit]" to use colorset parent implement.',
+ default: '[inherit]',
+ },
+};
+
+const schema = {
+ type: 'object',
+ title: 'Colorset Item',
+ required: ['color', 'name', 'id', 'selectionMethod'],
+ properties,
+};
+
+export default schema;
diff --git a/src/components/core/schemas/cncserver.schemas.colors.js b/src/components/core/schemas/cncserver.schemas.colors.js
new file mode 100644
index 00000000..02152138
--- /dev/null
+++ b/src/components/core/schemas/cncserver.schemas.colors.js
@@ -0,0 +1,66 @@
+/**
+ * @file Colorset settings schema.
+ *
+ */
+
+const properties = {
+ name: {
+ type: 'string',
+ title: 'Machine Name',
+ description: 'Machine name for the colorset, set from title.',
+ },
+ title: {
+ type: 'string',
+ title: 'Title',
+ description: 'Human readable name for the colorset.',
+ },
+ manufacturer: {
+ type: 'string',
+ title: 'Manufacturer',
+ description: 'Creator of this set of colors.',
+ default: '',
+ },
+ description: {
+ type: 'string',
+ title: 'Description',
+ description: 'Extra information about this colorset',
+ format: 'textarea',
+ default: '',
+ },
+ sortWeight: {
+ type: 'integer',
+ title: 'Sort Weighting',
+ description: 'Sets display order for each colorset in the UI. Lower values rise, higher values sink.',
+ format: 'range',
+ minimum: -100,
+ maximum: 100,
+ default: 0,
+ },
+ implement: {
+ type: 'string',
+ title: 'Implement',
+ description: 'Machine name of valid implement to draw with.',
+ default: 'sharpie-ultra-fine-marker',
+ },
+ items: {
+ type: 'array',
+ title: 'Items',
+ description: 'Colorset items representing all the colors/implements used.',
+ items: {}, // Set in indexer as color schema.
+ },
+ toolset: {
+ type: 'string',
+ title: 'Toolset',
+ description: 'Set of additional tools this colorset uses.',
+ default: 'default',
+ },
+};
+
+const schema = {
+ type: 'object',
+ required: ['title'],
+ title: 'Colors',
+ properties,
+};
+
+export default schema;
diff --git a/src/components/core/schemas/cncserver.schemas.content.js b/src/components/core/schemas/cncserver.schemas.content.js
index 70604d2a..a73bc944 100644
--- a/src/components/core/schemas/cncserver.schemas.content.js
+++ b/src/components/core/schemas/cncserver.schemas.content.js
@@ -5,117 +5,145 @@
* interface restrictions and expectations for all data IO.
*/
/* eslint-disable max-len */
-module.exports = (cncserver) => {
- const { base } = cncserver.drawing;
- const bounds = base.defaultBounds();
- const properties = {
- project: {
- type: 'string',
- title: 'Project',
- description: 'Parent project for the content to live in, defaults to currently open project.',
- },
- title: {
- type: 'string',
- title: 'Content Title',
- description: 'Human readable name for the content.',
- default: '',
- },
- autoRender: {
- type: 'boolean',
- format: 'checkbox',
- title: 'Auto Render',
- description: 'Automatically re-render this content when updated.',
- default: true,
- },
- // settings: @see cncserver.schemas.content.settings.js
- source: {
- type: 'object',
- title: 'Content Source',
- properties: {
- type: {
- type: 'string',
- title: 'Content type',
- description: 'The type of content',
- enum: ['svg', 'path', 'paper', 'raster', 'text'],
- },
- url: {
- type: 'string',
- format: 'uri',
- title: 'URL',
- description: 'Location on the internet where the content can be found.',
- },
- content: {
- type: 'string',
- title: 'Content Body',
- description: 'String content of the specified type, or binary data URI for rasters',
+import { defaultBounds, state as drawingBase } from 'cs/drawing/base';
+import { bindTo } from 'cs/binder';
+
+const properties = {
+ project: {
+ type: 'string',
+ title: 'Project',
+ description: 'Parent project for the content to live in, defaults to currently open project.',
+ },
+ title: {
+ type: 'string',
+ title: 'Content Title',
+ description: 'Human readable name for the content.',
+ default: '',
+ },
+ autoRender: {
+ type: 'boolean',
+ format: 'checkbox',
+ title: 'Auto Render',
+ description: 'Automatically re-render this content when updated.',
+ default: true,
+ },
+ // settings: @see cncserver.schemas.content.settings.js
+ source: {
+ type: 'object',
+ title: 'Content Source',
+ properties: {
+ type: {
+ type: 'string',
+ title: 'Content type',
+ description: 'The type of content',
+ enum: ['svg', 'path', 'paper', 'raster', 'text'],
+ options: {
+ enum_titles: [
+ 'SVG XML',
+ 'SVG Path Data',
+ 'Paper.js JSON',
+ 'Raster Image',
+ 'Plain Text',
+ ],
},
},
- /* if: { // TODO: This doesn't work, but it really should.
- properties: {
- type: { const: 'raster' },
- },
+ url: {
+ type: 'string',
+ format: 'uri',
+ title: 'URL',
+ description: 'Location on the internet where the content can be found.',
+ },
+ content: {
+ type: 'string',
+ title: 'Content Body',
+ description: 'String content of the specified type, or binary data URI for rasters',
},
- then: {
- properties: {
- content: { pattern: '^(data:)([\w\/\+]+);(charset=[\w-]+|base64).*,(.*)' },
- },
- }, */
- oneOf: [
- { required: ['type', 'url'] },
- { required: ['type', 'content'] },
- ],
},
- bounds: {
- type: 'object',
- title: 'Bounds rectangle',
- description: 'Definition of a rectangle in mm to size the content to within the canvas.',
+ /* if: { // TODO: This doesn't work, but it really should.
properties: {
- x: {
- type: 'number',
- format: 'number',
- title: 'X Point',
- description: 'X coordinate of top left position of the rectangle.',
- default: bounds.point.x,
- minimum: 0,
- maximum: base.size.width - 1,
- },
- y: {
- type: 'number',
- format: 'number',
- title: 'Y Point',
- description: 'Y coordinate of top left position of the rectangle.',
- default: bounds.point.y,
- minimum: 0,
- maximum: base.size.height - 1,
- },
- width: {
- type: 'number',
- format: 'number',
- title: 'Width',
- description: 'Width of the rectangle.',
- default: bounds.width,
- minimum: 1,
- maximum: base.size.width,
- },
- height: {
- type: 'number',
- format: 'number',
- title: 'Height',
- description: 'Height of the rectangle.',
- default: bounds.height,
- minimum: 1,
- maximum: base.size.height,
- },
+ type: { const: 'raster' },
},
},
- };
-
- return {
+ then: {
+ properties: {
+ content: { pattern: '^(data:)([\w\/\+]+);(charset=[\w-]+|base64).*,(.*)' },
+ },
+ }, */
+ oneOf: [
+ { required: ['type', 'url'] },
+ { required: ['type', 'content'] },
+ ],
+ },
+ bounds: {
type: 'object',
- title: 'Content',
- description: 'Full definition of a single unit of content for a project.',
- properties,
- required: ['source'],
- };
+ title: 'Bounds rectangle',
+ description: 'Definition of a rectangle in mm to size the content to within the canvas.',
+ properties: {
+ x: {
+ type: 'number',
+ format: 'number',
+ title: 'X Point',
+ description: 'X coordinate of top left position of the rectangle.',
+ default: 0, // Set in bindTo below.
+ minimum: 0,
+ maximum: 1, // Set in bindTo below.
+ step: 0.1,
+ },
+ y: {
+ type: 'number',
+ format: 'number',
+ title: 'Y Point',
+ description: 'Y coordinate of top left position of the rectangle.',
+ default: 0, // Set in bindTo below.
+ minimum: 0,
+ maximum: 1, // Set in bindTo below.
+ step: 0.1,
+ },
+ width: {
+ type: 'number',
+ format: 'number',
+ title: 'Width',
+ description: 'Width of the rectangle.',
+ default: 1, // Set in bindTo below.
+ minimum: 1,
+ maximum: 2, // Set in bindTo below.
+ step: 0.1,
+ },
+ height: {
+ type: 'number',
+ format: 'number',
+ title: 'Height',
+ description: 'Height of the rectangle.',
+ default: 1, // Set in bindTo below.
+ minimum: 1,
+ maximum: 2, // Set in bindTo below.
+ step: 0.1,
+ },
+ },
+ },
};
+
+// Set the schema level content boundaries after they've been defined.
+bindTo('paper.ready', 'schemas.content', () => {
+ const bounds = defaultBounds();
+ properties.bounds.properties.x.default = bounds.point.x;
+ properties.bounds.properties.y.default = bounds.point.y;
+ properties.bounds.properties.width.default = bounds.width;
+ properties.bounds.properties.height.default = bounds.height;
+
+ properties.bounds.properties.x.maximum = drawingBase.size.width - 1;
+ properties.bounds.properties.y.maximum = drawingBase.size.height - 1;
+ properties.bounds.properties.width.maximum = drawingBase.size.width;
+ properties.bounds.properties.height.maximum = drawingBase.size.height;
+});
+
+const schema = {
+ type: 'object',
+ title: 'Content',
+ description: 'Full definition of a single unit of content for a project.',
+ properties,
+ required: ['source'],
+};
+
+export default schema;
diff --git a/src/components/core/schemas/cncserver.schemas.content.settings.js b/src/components/core/schemas/cncserver.schemas.content.settings.js
index 0aa20309..26854107 100644
--- a/src/components/core/schemas/cncserver.schemas.content.settings.js
+++ b/src/components/core/schemas/cncserver.schemas.content.settings.js
@@ -3,10 +3,12 @@
*
* This builds a conglomerate render settings schema based on keys passed in.
*/
-module.exports = properties => ({
- type: 'object',
- title: 'Settings',
- format: 'categories',
- description: 'All render specific settings overrides',
- properties,
-});
+export default function settingsSchema(properties) {
+ return {
+ type: 'object',
+ title: 'Settings',
+ format: 'categories',
+ description: 'All render specific settings overrides',
+ properties,
+ };
+}
diff --git a/src/components/core/schemas/cncserver.schemas.fill.js b/src/components/core/schemas/cncserver.schemas.fill.js
index 0d6f0942..f42cd493 100644
--- a/src/components/core/schemas/cncserver.schemas.fill.js
+++ b/src/components/core/schemas/cncserver.schemas.fill.js
@@ -7,104 +7,116 @@
* All supplied fill schemas are merged into this one.
*/
/* eslint-disable max-len */
-const fs = require('fs');
-const path = require('path');
+import fs from 'fs';
+import path from 'path';
+import { __basedir } from 'cs/utils';
const fillerSchema = 'cncserver.drawing.filler.schema.js';
-module.exports = () => {
- const globalSchema = {
- render: {
- type: 'boolean',
- format: 'checkbox',
- title: 'Render',
- description: 'Whether fill should be rendered, set false to skip.',
- default: true,
- },
- cutoutOcclusion: {
- type: 'boolean',
- format: 'checkbox',
- title: 'Cutout Occlusion',
- description: 'Whether fill spaces will be cut out depending on overlapping fills',
- default: true,
- },
- trace: {
- type: 'boolean',
- format: 'checkbox',
- title: 'Trace Fill',
- description: 'Whether fill object will get a stroke of the fill color',
- default: false,
- },
- method: {
- type: 'string',
- title: 'Fill Method',
- description: 'The method used to turn a fill into paths.',
- default: 'offset',
- enum: [], // Filled below from available filler schemas.
- },
- flattenResolution: {
- type: 'number',
- title: 'Flatten Resolution',
- description: 'How much detail is preserved when converting curves. Smaller is higher resolution but less performant.',
- default: 0.25,
- minimum: 0.01,
- maximum: 5,
- },
- rotation: {
- type: 'number',
- format: 'range',
- title: 'Rotation',
- description: 'If applicable, the rotation for a fill method.',
- default: 28,
- minimum: -360,
- maximum: 360,
- },
- randomizeRotation: {
- type: 'boolean',
- format: 'checkbox',
- title: 'Randomize Rotation',
- description: 'If set, the rotation setting will be ignored and a single random angle will be selected for each fill.',
- default: false,
- },
- inset: {
- type: 'number',
- format: 'range',
- title: 'Inset',
- description: 'The number of mm to negatively offset a fill path, allowing for space between outside stroke and internal size.',
- default: 0,
- minimum: -50,
- maximum: 50,
- },
- spacing: {
- type: 'number',
- format: 'range',
- title: 'Spacing',
- description: 'If applicable, the amount of space between items in MM, lower number is higher density.',
- default: 3,
- minimum: 0.1,
- maximum: 100,
- },
- };
+const globalSchema = {
+ render: {
+ type: 'boolean',
+ format: 'checkbox',
+ title: 'Render',
+ description: 'Whether fill should be rendered, set false to skip.',
+ default: true,
+ },
+ cutoutOcclusion: {
+ type: 'boolean',
+ format: 'checkbox',
+ title: 'Cutout Occlusion',
+ description: 'Whether fill spaces will be cut out depending on overlapping fills',
+ default: true,
+ },
+ trace: {
+ type: 'boolean',
+ format: 'checkbox',
+ title: 'Trace Fill',
+ description: 'Whether fill object will get a stroke of the fill color',
+ default: false,
+ },
+ method: {
+ type: 'string',
+ title: 'Fill Method',
+ description: 'The method used to turn a fill into paths.',
+ default: 'offset',
+ enum: [], // Filled below from available filler schemas.
+ options: { enum_titles: [] },
+ },
+ flattenResolution: {
+ type: 'number',
+ title: 'Flatten Resolution',
+ description: 'How much detail is preserved when converting curves. Smaller is higher resolution but less performant.',
+ default: 0.25,
+ minimum: 0.01,
+ step: 0.01,
+ maximum: 5,
+ },
+ rotation: {
+ type: 'number',
+ format: 'range',
+ title: 'Rotation',
+ description: 'If applicable, the rotation for a fill method.',
+ default: 28,
+ minimum: -360,
+ maximum: 360,
+ step: 0.1,
+ },
+ randomizeRotation: {
+ type: 'boolean',
+ format: 'checkbox',
+ title: 'Randomize Rotation',
+ description: 'If set, the rotation setting will be ignored and a single random angle will be selected for each fill.',
+ default: false,
+ },
+ inset: {
+ type: 'number',
+ format: 'range',
+ title: 'Inset',
+ description: 'The number of mm to negatively offset a fill path, allowing for space between outside stroke and internal size.',
+ default: 0,
+ minimum: -50,
+ maximum: 50,
+ step: 0.1,
+ },
+ spacing: {
+ type: 'number',
+ format: 'range',
+ title: 'Spacing',
+ description: 'If applicable, the amount of space between items in MM, lower number is higher density.',
+ default: 3,
+ minimum: 0.1,
+ maximum: 100,
+ step: 0.1,
+ },
+};
+
+// Compile list of all other filler modules and their schemas and merge them.
+const fillerPath = path.resolve(__basedir, 'components', 'core', 'drawing', 'fillers');
+fs.readdirSync(fillerPath).map(dir => {
+ const fullPath = path.resolve(fillerPath, dir);
+ if (fs.lstatSync(fullPath).isDirectory()) {
+ const schemaPath = path.resolve(fullPath, fillerSchema);
+ if (fs.existsSync(schemaPath)) {
+ globalSchema.method.enum.push(dir);
- // Compile list of all other filler modules and their schemas and merge them.
- const fillerPath = path.resolve(__dirname, '..', 'drawing', 'fillers');
- fs.readdirSync(fillerPath).map((dir) => {
- const fullPath = path.resolve(fillerPath, dir);
- if (fs.lstatSync(fullPath).isDirectory()) {
- const schemaPath = path.resolve(fullPath, fillerSchema);
- if (fs.existsSync(schemaPath)) {
- globalSchema.method.enum.push(dir);
- // eslint-disable-next-line
- const fillerSchemaObject = require(schemaPath);
+ import(schemaPath).then(({ default: fillerSchemaObject }) => {
+ globalSchema.method.options.enum_titles.push(fillerSchemaObject.title);
// Only add if filler defines custom props.
if (Object.entries(fillerSchemaObject.properties).length) {
globalSchema[dir] = fillerSchemaObject;
globalSchema[dir].options = { dependencies: { method: dir } };
}
- }
+ });
}
- });
+ }
+});
- return { type: 'object', title: 'Fill', properties: globalSchema };
+const schema = {
+ type: 'object',
+ title: 'Fill',
+ properties: globalSchema,
};
+
+export default schema;
diff --git a/src/components/core/schemas/cncserver.schemas.implements.js b/src/components/core/schemas/cncserver.schemas.implements.js
new file mode 100644
index 00000000..543f4898
--- /dev/null
+++ b/src/components/core/schemas/cncserver.schemas.implements.js
@@ -0,0 +1,109 @@
+/**
+ * @file Colorset item implement schema.
+ *
+ */
+/* eslint-disable max-len */
+const schema = {
+ type: 'object',
+ title: 'Implement Details',
+ required: ['type', 'name'],
+ properties: {
+ type: {
+ type: 'string',
+ title: 'Type',
+ description: 'Type of implement',
+ enum: ['brush', 'pen', 'other'],
+ default: 'pen',
+ },
+ manufacturer: {
+ type: 'string',
+ title: 'Manufacturer',
+ description: 'Creator of this implement.',
+ default: 'generic',
+ },
+ name: {
+ type: 'string',
+ title: 'Name',
+ description: 'Machine readable name of the implement.',
+ },
+ title: {
+ type: 'string',
+ title: 'Title',
+ description: 'Human readable name of the implement.',
+ },
+ sortWeight: {
+ type: 'integer',
+ title: 'Sort Weighting',
+ description: 'Sets display order for each implement in the UI. Higher values sink, lower values rise.',
+ format: 'range',
+ minimum: -100,
+ maximum: 100,
+ default: 0,
+ },
+ handleWidth: {
+ type: 'number',
+ title: 'Handle Width',
+ description: 'Measured width of the handle, to account for center draw position offset.',
+ format: 'range',
+ minimum: 2,
+ maximum: 20,
+ default: 10,
+ step: 0.01,
+ },
+ handleColors: {
+ type: 'array',
+ title: 'Handle Color(s)',
+ description: 'Color of the handle for the implement, helps with physical user selection.',
+ items: {
+ type: 'string',
+ format: 'color',
+ title: 'Color',
+ },
+ default: ['#000000'],
+ },
+ width: {
+ type: 'number',
+ title: 'Width',
+ description: 'Calculated maximum effective width in mm of the implement being used.',
+ format: 'range',
+ minimum: 0.01,
+ maximum: 35,
+ default: 1,
+ step: 0.01,
+ },
+ length: {
+ type: 'number',
+ title: 'Length',
+ description: 'Dynamic fulcrum length for the implement. Set to bristle length in mm for brushes.',
+ format: 'range',
+ minimum: 0,
+ maximum: 35,
+ default: 0,
+ step: 0.01,
+ },
+ stiffness: {
+ type: 'number',
+ title: 'Stiffness',
+ description: 'Stiffness of dynamic fulcrum length. Set to 1 for fully stiff (same as length 0), 1 for fully loose (soft bristles).',
+ format: 'range',
+ minimum: 0,
+ maximum: 1,
+ default: 1,
+ options: { dependencies: { type: ['brush', 'other'] } },
+ step: 0.01,
+ },
+ drawLength: {
+ type: 'number',
+ title: 'Draw Length',
+ description: 'Distance in mm the implement can draw before it needs to be refreshed.',
+ format: 'range',
+ minimum: 0,
+ maximum: 3000,
+ default: 0,
+ options: { dependencies: { type: 'brush' } },
+ step: 0.1,
+ },
+ },
+};
+
+export default schema;
diff --git a/src/components/core/schemas/cncserver.schemas.js b/src/components/core/schemas/cncserver.schemas.js
index bd4d2748..9ebb3627 100644
--- a/src/components/core/schemas/cncserver.schemas.js
+++ b/src/components/core/schemas/cncserver.schemas.js
@@ -1,138 +1,134 @@
/**
* @file Wrapper module for verifying data to schemas, and providing errors.
*/
-const Ajv = require('ajv');
-
-// Global core component export object to be attached.
-const schemas = { id: 'schemas', all: {} };
-
-module.exports = (cncserver) => {
- const ajv = Ajv({
- allErrors: true,
- removeAdditional: true,
- unknownFormats: ['checkbox', 'color', 'range', 'tabs', 'categories', 'number'],
- });
-
- // Format the AJV field error messages.
- function formatMessages(errors) {
- const fields = errors.reduce((acc, e) => {
- if (e.dataPath.length && e.dataPath[0] === '.') {
- const av = e.params.allowedValues ? `: ${e.params.allowedValues.join(', ')}` : '';
- acc[e.dataPath.slice(1)] = [`${e.message}${av}`];
- } else {
- acc[e.dataPath] = [e.message];
- }
- return acc;
- },
- {});
+import Ajv from 'ajv';
+import { trigger, bindTo } from 'cs/binder';
+import schemas from 'cs/schemas/index';
+import { applyObjectTo } from 'cs/utils';
+
+const ajv = Ajv({
+ allErrors: true,
+ removeAdditional: true,
+ unknownFormats: [
+ 'checkbox', 'color', 'range', 'tabs', 'categories', 'number', 'textarea'
+ ],
+});
+
+// Format the AJV field error messages.
+function formatMessages(errors) {
+ const fields = errors.reduce((acc, e) => {
+ if (e.dataPath.length && e.dataPath[0] === '.') {
+ const av = e.params.allowedValues ? `: ${e.params.allowedValues.join(', ')}` : '';
+ acc[e.dataPath.slice(1)] = [`${e.message}${av}`];
+ } else {
+ acc[e.dataPath] = [e.message];
+ }
+ return acc;
+ }, {});
+
+ return { fields };
+}
+
+// Fill in defaults of the entire schema, forked from:
+// https://github.com/harmvandendorpel/json-schema-fill-defaults
+function autoDefaults(data, schema) {
+ function processNode(schemaNode, dataNode) {
+ switch (schemaNode.type) {
+ case 'object':
+ // eslint-disable-next-line no-use-before-define
+ return processObject(schemaNode, dataNode);
+
+ case 'array':
+ // eslint-disable-next-line no-use-before-define
+ return processArray(schemaNode, dataNode);
+
+ default:
+ if (dataNode !== undefined) return dataNode;
+ if (schemaNode.default !== undefined) return schemaNode.default;
+ return undefined;
+ }
+ }
- return { fields };
+ function forOwn(object, callback) {
+ Object.keys(object).map(key => callback(object[key], key, object));
}
- // Fill in defaults of the entire schema, forked from:
- // https://github.com/harmvandendorpel/json-schema-fill-defaults
- function autoDefaults(data, schema) {
- function processNode(schemaNode, dataNode) {
- switch (schemaNode.type) {
- case 'object':
- return processObject(schemaNode, dataNode); // eslint-disable-line no-use-before-define
-
- case 'array':
- return processArray(schemaNode, dataNode); // eslint-disable-line no-use-before-define
-
- default:
- if (dataNode !== undefined) return dataNode;
- if (schemaNode.default !== undefined) return schemaNode.default;
- return undefined;
- }
+ function processObject(schemaNode, dataNode) {
+ const result = {};
+ // If no properties, pick the first oneOf.
+ if (!schemaNode.properties) {
+ // eslint-disable-next-line no-param-reassign
+ schemaNode.properties = schemaNode.oneOf[0].properties;
}
+ forOwn(schemaNode.properties, (propertySchema, propertyName) => {
+ const nodeValue = dataNode !== undefined ? dataNode[propertyName] : undefined;
+ result[propertyName] = processNode(propertySchema, nodeValue);
+ });
- function forOwn(object, callback) {
- Object.keys(object).map(key => callback(object[key], key, object));
+ if (dataNode) {
+ forOwn(dataNode, (propertyValue, propertyName) => {
+ if (result[propertyName] === undefined && propertyValue !== undefined) {
+ result[propertyName] = propertyValue;
+ }
+ });
}
+ return result;
+ }
- function processObject(schemaNode, dataNode) {
- const result = {};
- // If no properties, pick the first oneOf.
- if (!schemaNode.properties) {
- schemaNode.properties = schemaNode.oneOf[0].properties;
+ function processArray(schemaNode, dataNode) {
+ if (dataNode === undefined) {
+ if (schemaNode.default) {
+ return schemaNode.default;
}
- forOwn(schemaNode.properties, (propertySchema, propertyName) => {
- const nodeValue = dataNode !== undefined ? dataNode[propertyName] : undefined;
- result[propertyName] = processNode(propertySchema, nodeValue);
- });
- if (dataNode) {
- forOwn(dataNode, (propertyValue, propertyName) => {
- if (result[propertyName] === undefined && propertyValue !== undefined) {
- result[propertyName] = propertyValue;
- }
- });
- }
- return result;
+ return undefined;
}
- function processArray(schemaNode, dataNode) {
- if (dataNode === undefined) {
- if (schemaNode.default) {
- return schemaNode.default;
- }
-
- return undefined;
- }
+ const result = [];
- const result = [];
-
- for (let i = 0; i < dataNode.length; i++) {
- result.push(processNode(schemaNode.items, dataNode[i]));
- }
- return result;
+ for (let i = 0; i < dataNode.length; i++) {
+ result.push(processNode(schemaNode.items, dataNode[i]));
}
-
- return processNode(schema, data);
+ return result;
}
- // Add all schemas captured via binder.
- schemas.all = cncserver.binder.trigger('schemas.compile', schemas.all);
-
- // After controller load, load all file schemas from index.
- cncserver.binder.bindTo('controller.setup', schemas.id, () => {
- // eslint-disable-next-line global-require
- schemas.all = { ...schemas.all, ...require('./index')(cncserver) };
+ return processNode(schema, data);
+}
- // Compile final list of schemas, registered by their key.
- Object.entries(schemas.all).forEach(([key, schema]) => {
- ajv.addSchema(schema, key);
- });
+// Add all schemas captured via binder.
+// applyObjectTo(trigger('schemas.compile', schemas), schemas);
- cncserver.binder.trigger('schemas.loaded');
+// After controller load, load all file schemas from index.
+bindTo('controller.setup', 'schemas', () => {
+ // Compile final list of schemas, registered by their key.
+ Object.entries(schemas).forEach(([key, schema]) => {
+ ajv.addSchema(schema, key);
});
- // Get a completely filled out default data object, with passed modifier object.
- schemas.getDataDefault = (type, data) => autoDefaults(data, schemas.all[type]);
-
- // Get a full schema by name.
- schemas.getFromRequest = (path) => {
- const type = path.split('/')[2];
- return ajv.getSchema(type) ? ajv.getSchema(type).schema : null;
- };
-
- // Validate data given a specific schema by name from above.
- schemas.validateData = (type, data, provideDefaults = false) => new Promise(
- (resolve, reject) => {
- const isValid = ajv.validate(type, data);
- if (isValid) {
- if (provideDefaults) {
- resolve(schemas.getDataDefault(type, data));
- } else {
- resolve(data);
- }
+ trigger('schemas.loaded', null, true);
+});
+
+// Get a completely filled out default data object, with passed modifier object.
+export const getDataDefault = (type, data) => autoDefaults(data, schemas[type]);
+
+// Get a full schema by name.
+export function getFromRequest(path) {
+ const type = path.split('/')[2];
+ return ajv.getSchema(type) ? ajv.getSchema(type).schema : null;
+}
+
+// Validate data given a specific schema by name from above.
+export const validateData = (type, data, provideDefaults = false) => new Promise(
+ (resolve, reject) => {
+ const isValid = ajv.validate(type, data);
+ if (isValid) {
+ if (provideDefaults) {
+ resolve(getDataDefault(type, data));
} else {
- reject(formatMessages(ajv.errors));
+ resolve(data);
}
+ } else {
+ reject(formatMessages(ajv.errors));
}
- );
-
-
- return schemas;
-};
+ }
+);
diff --git a/src/components/core/schemas/cncserver.schemas.path.js b/src/components/core/schemas/cncserver.schemas.path.js
index fa2b5f21..b93ead94 100644
--- a/src/components/core/schemas/cncserver.schemas.path.js
+++ b/src/components/core/schemas/cncserver.schemas.path.js
@@ -6,7 +6,7 @@
*
*/
/* eslint-disable max-len */
-module.exports = () => ({
+const schema = {
type: 'object',
title: 'Path',
properties: {
@@ -25,4 +25,6 @@ module.exports = () => ({
default: 'black',
},
},
-});
+};
+
+export default schema;
diff --git a/src/components/core/schemas/cncserver.schemas.print.js b/src/components/core/schemas/cncserver.schemas.print.js
new file mode 100644
index 00000000..4c2057ef
--- /dev/null
+++ b/src/components/core/schemas/cncserver.schemas.print.js
@@ -0,0 +1,166 @@
+/**
+* @file Print settings schema.
+*/
+/* eslint-disable max-len */
+
+const properties = {
+ hash: {
+ type: 'string',
+ title: 'Hash of settings to validate differences',
+ description: 'Machine name for the colorset, set from title.',
+ },
+ title: {
+ type: 'string',
+ title: 'Title',
+ description: 'Human readable name for the print and its settings.',
+ maxLength: 200,
+ },
+ description: {
+ type: 'string',
+ title: 'Description',
+ description: 'Description of this print.',
+ },
+ botType: {
+ type: 'string',
+ title: 'Bot Type',
+ description: 'Type of bot expected',
+ },
+ settings: {
+ type: 'object',
+ title: 'Print Settings',
+ properties: {
+ parkAfter: {
+ type: 'boolean',
+ title: 'Park After Print?',
+ description: 'When true, after print completes the bot will park.',
+ format: 'checkbox',
+ default: true,
+ },
+ planning: {
+ type: 'object',
+ title: 'Path Planning',
+ description: 'Methods of path planning.',
+ properties: {
+ method: {
+ type: 'string',
+ title: 'Method',
+ enum: ['linear', 'dvl'],
+ options: {
+ enum_titles: [
+ 'Linear (single speed)',
+ 'Dynamic Vector Lookahead (DVL)',
+ ],
+ },
+ default: 'dvl',
+ },
+ linear: {
+ type: 'object',
+ title: 'Linear movement options',
+ properties: {
+ moveSpeed: {
+ type: 'number',
+ title: 'Moving Speed',
+ description: 'Percentage of maximum speed of the carriage while moving above the work (not drawing).',
+ format: 'range',
+ step: 0.1,
+ minimum: 1,
+ maximum: 100,
+ default: 40,
+ },
+ drawSpeed: {
+ type: 'number',
+ title: 'Draw Speed',
+ description: 'Percentage of maximum speed of the carriage while drawing the work.',
+ format: 'range',
+ step: 0.1,
+ minimum: 1,
+ maximum: 100,
+ default: 25,
+ },
+ },
+ },
+ dvl: {
+ type: 'object',
+ title: 'DVL movement options',
+ properties: {
+ accelRate: {
+ type: 'number',
+ title: 'Accelleration Rate',
+ description: 'Percentage of maximum speed to increase over given time/distance.',
+ format: 'range',
+ step: 0.1,
+ minimum: 1,
+ maximum: 80,
+ default: 25,
+ },
+ speedMultiplyer: {
+ type: 'number',
+ title: 'Moment conversion factor',
+ description: 'Factor for converting moment length to velocity',
+ format: 'range',
+ step: 0.01,
+ minimum: 0.1,
+ maximum: 1,
+ default: 0.75,
+ },
+ minSpeed: {
+ type: 'number',
+ title: 'Minimum Speed',
+ description: 'Lowest speed to move at for detailed work.',
+ format: 'range',
+ step: 0.01,
+ minimum: 1,
+ maximum: 50,
+ default: 15,
+ },
+ resolution: {
+ type: 'number',
+ title: 'Step Resolution',
+ description: 'Resolution of step breakdown, in MM. Smaller numbers give more resolution.',
+ format: 'range',
+ step: 0.01,
+ minimum: 0.01,
+ maximum: 2,
+ default: 0.5,
+ },
+ maxDeflection: {
+ type: 'number',
+ title: 'Maximum Deflection',
+ description: 'Maximum angle of deflection to allow before slowing down around tight corners.',
+ format: 'range',
+ step: 0.1,
+ minimum: 0.5,
+ maximum: 20,
+ default: 10,
+ },
+ },
+ },
+ },
+ },
+ wcb: {
+ type: 'object',
+ title: 'Watercolor Settings',
+ properties: {
+ wash: {
+ type: 'string',
+ title: 'Wash',
+ enum: ['full', 'light'],
+ },
+ // TODO:
+ // Wash settings
+ // Reink Settings
+ /// etc?
+ },
+ },
+ },
+ },
+};
+
+const schema = {
+ type: 'object',
+ required: [''],
+ title: 'Print',
+ properties,
+};
+
+export default schema;
diff --git a/src/components/core/schemas/cncserver.schemas.projects.js b/src/components/core/schemas/cncserver.schemas.projects.js
index 9628e839..fadd1453 100644
--- a/src/components/core/schemas/cncserver.schemas.projects.js
+++ b/src/components/core/schemas/cncserver.schemas.projects.js
@@ -5,55 +5,101 @@
* interface restrictions and expectations for all data IO.
*/
/* eslint-disable max-len */
-module.exports = () => {
- const properties = {
- /* hash: {
- type: 'string',
- title: 'Hash ID',
- description: 'Initial hash for the project, used as a UUID',
- },
- created: {
- type: 'string',
- format: 'date',
- title: 'Created date',
- description: 'When the project was last edited.',
- },
- updated: {
- type: 'string',
- format: 'date',
- title: 'Updated date',
- description: 'When the project was last edited.',
- }, */ // These are computed 🤔
- name: {
- type: 'string',
- title: 'Machine name',
- description: 'Machine name of the project, if not given, comes from title.',
- },
- title: {
- type: 'string',
- title: 'Project Title',
- description: 'Full title of the project.',
- default: 'New Project',
- },
- description: {
- type: 'string',
- title: 'Description',
- description: 'Full text describing the project.',
- },
- open: {
- type: 'boolean',
- format: 'checkbox',
- title: 'Open After Creation',
- description: 'Will open new project by default. Set to false to create in the background.',
- default: true,
+const properties = {
+ /* hash: {
+ type: 'string',
+ title: 'Hash ID',
+ description: 'Initial hash for the project, used as a UUID',
+ },
+ created: {
+ type: 'string',
+ format: 'date',
+ title: 'Created date',
+ description: 'When the project was last edited.',
+ },
+ updated: {
+ type: 'string',
+ format: 'date',
+ title: 'Updated date',
+ description: 'When the project was last edited.',
+ }, */ // These are computed 🤔
+ name: {
+ type: 'string',
+ title: 'Machine name',
+ description: 'Machine name of the project, if not given, comes from title.',
+ },
+ title: {
+ type: 'string',
+ title: 'Project Title',
+ description: 'Full title of the project.',
+ default: 'New Project',
+ },
+ description: {
+ type: 'string',
+ title: 'Description',
+ description: 'Full text describing the project.',
+ format: 'textarea',
+ },
+ open: {
+ type: 'boolean',
+ format: 'checkbox',
+ title: 'Open After Creation',
+ description: 'Will open new project by default. Set to false to create in the background.',
+ default: true,
+ },
+ colorset: {
+ type: 'string',
+ title: 'Suggested Colorset',
+ description: 'Colorset preset attached to the project',
+ default: 'default-single-pen',
+ },
+ options: {
+ type: 'object',
+ title: 'Project Options',
+ description: 'Customizable settings specific to this project',
+ properties: {
+ paper: {
+ type: 'object',
+ title: 'Paper Options',
+ description: 'Customizable settings specific to this project',
+ properties: {
+ color: {
+ type: 'string',
+ format: 'color',
+ title: 'Paper Color',
+ description: 'The assumed color of the paper, for preview and ignoring.',
+ default: '#FFFFFF',
+ },
+ ignore: {
+ type: 'boolean',
+ format: 'checkbox',
+ title: 'Ignore Paper Color',
+ description: 'Attempt to match items to the paper color to prevent rendering them.',
+ default: true,
+ },
+ ignoreColorWeight: {
+ type: 'number',
+ title: 'Ignore Color Weighting',
+ description: 'Amount to adjust for color selection preference. Smaller than 0 selects less often, larger than 0 selects more often.',
+ format: 'range',
+ minimum: -1,
+ maximum: 1,
+ default: -0.5,
+ step: 0.01,
+ options: { dependencies: { ignore: true } },
+ },
+ },
+ },
},
- // settings: @see cncserver.schemas.content.settings.js
- };
+ },
+ // settings: @see cncserver.schemas.content.settings.js
+};
- return {
- type: 'object',
- properties,
- title: 'Project',
- description: 'Full definition of a project that holds content',
- };
+const schema = {
+ type: 'object',
+ properties,
+ title: 'Project',
+ description: 'Full definition of a project that holds content',
};
+
+export default schema;
diff --git a/src/components/core/schemas/cncserver.schemas.stroke.js b/src/components/core/schemas/cncserver.schemas.stroke.js
index 37c8fbb1..802406e4 100644
--- a/src/components/core/schemas/cncserver.schemas.stroke.js
+++ b/src/components/core/schemas/cncserver.schemas.stroke.js
@@ -6,7 +6,7 @@
*
*/
/* eslint-disable max-len */
-const strokeSchema = {
+const properties = {
render: {
type: 'boolean',
format: 'checkbox',
@@ -23,4 +23,10 @@ const strokeSchema = {
},
};
-module.exports = () => ({ type: 'object', title: 'Stroke', properties: strokeSchema });
+const schema = {
+ type: 'object',
+ title: 'Stroke',
+ properties,
+};
+
+export default schema;
diff --git a/src/components/core/schemas/cncserver.schemas.text.js b/src/components/core/schemas/cncserver.schemas.text.js
index 2643deeb..2b8005d5 100644
--- a/src/components/core/schemas/cncserver.schemas.text.js
+++ b/src/components/core/schemas/cncserver.schemas.text.js
@@ -5,99 +5,137 @@
* system fonts.
*/
/* eslint-disable max-len */
-const fontList = require('font-list');
+import fontList from 'font-list';
+import { bindTo } from 'cs/binder';
-module.exports = (cncserver) => {
- const properties = {
- render: {
- type: 'boolean',
- format: 'checkbox',
- title: 'Render',
- description: 'Render text',
- default: true,
+const properties = {
+ render: {
+ type: 'boolean',
+ format: 'checkbox',
+ title: 'Render',
+ description: 'Render text',
+ default: true,
+ },
+ spaceWidth: {
+ type: 'number',
+ format: 'range',
+ default: 0,
+ minimum: -1000,
+ maximum: 1000,
+ title: 'Space width adjustment',
+ description: 'Units to add/remove from the internal font width.',
+ },
+ wrapLines: {
+ type: 'boolean',
+ format: 'checkbox',
+ default: true,
+ title: 'Automatically wrap long lines',
+ description: 'If enabled, lines beyond textwrap width will automatically move to the next line.',
+ },
+ wrapChars: {
+ type: 'integer',
+ format: 'range',
+ default: 60,
+ minimum: 1,
+ maximum: 500,
+ title: 'Textwrap width (in spaces)',
+ description: 'Number of space character widths to wait before text length triggers a newline.',
+ },
+ font: {
+ type: 'string',
+ default: 'hershey_sans_1',
+ enum: ['temp'], // Set below after fonts have loaded.
+ title: 'Stroke Font',
+ description: 'Which EMS/SVG stroke font to use to render the text.',
+ options: {
+ enum_titles: ['temp'],
},
- spaceWidth: {
- type: 'number',
- default: 20,
- title: 'Space width',
- description: 'Width of the space character.',
- },
- font: {
- type: 'string',
- default: 'hershey_sans_1',
- enum: Object.keys(cncserver.drawing.text.fonts),
- title: 'Stroke Font',
- description: 'Which EMS/SVG stroke font to use to render the text.',
- },
- systemFont: {
- type: 'string',
- default: '',
- title: 'System Font',
- description: 'Which system font to render the text with. Overrides stroke font settings, creates filled text.',
- },
- lineHeight: {
- type: 'number',
- default: 90,
- title: 'Line height',
- description: 'Height of each line when more than one line of text is given.',
- },
- character: {
- type: 'object',
- title: 'Character settings',
- description: 'Options that effect individual characters',
- options: { collapsed: true },
- properties: {
- rotation: {
- type: 'number',
- format: 'range',
- default: 0,
- minimum: -360,
- maximum: 360,
- title: 'Rotation',
- description: 'Amount to rotate each character.',
- },
- spacing: {
- type: 'number',
- format: 'range',
- minimum: -100,
- maximum: 100,
- default: 18,
- title: 'Spacing',
- description: 'Amount of space given to each character after the width of the previous.',
- },
+ },
+ systemFont: {
+ type: 'string',
+ default: '',
+ title: 'System Font',
+ description: 'Which system font to render the text with. Overrides stroke font settings, creates filled text.',
+ },
+ lineHeight: {
+ type: 'number',
+ default: 0,
+ format: 'range',
+ minimum: -1000,
+ maximum: 1000,
+ title: 'Line height adjustment',
+ description: 'Adjustment to the height of each line when more than one line of text is given or computed.',
+ },
+ character: {
+ type: 'object',
+ title: 'Character settings',
+ description: 'Options that effect individual characters',
+ options: { collapsed: true },
+ properties: {
+ rotation: {
+ type: 'number',
+ format: 'range',
+ default: 0,
+ minimum: -360,
+ maximum: 360,
+ title: 'Rotation',
+ description: 'Amount to rotate each character.',
},
- },
- align: {
- type: 'object',
- title: 'Text alignment',
- description: 'Options for aligning the text.',
- options: { collapsed: true },
- properties: {
- paragraph: {
- type: 'string',
- enum: ['left', 'right', 'center'],
- default: 'left',
- title: 'Paragraph',
- description: 'When more than one line of text exists, how to align the text within the bounds.',
- },
- // TODO: What other alignment options do we need?
+ spacing: {
+ type: 'number',
+ format: 'range',
+ default: 0,
+ minimum: -1000,
+ maximum: 1000,
+ title: 'Spacing',
+ description: 'Amount of space given to each character after the width of the previous.',
},
},
- rotation: {
- type: 'number',
- format: 'range',
- default: 0,
- minimum: -360,
- maximum: 360,
- title: 'Text rotation',
- description: 'Angle to rotate the entire text object within the bounds.',
+ },
+ align: {
+ type: 'object',
+ title: 'Text alignment',
+ description: 'Options for aligning the text.',
+ options: { collapsed: true },
+ properties: {
+ paragraph: {
+ type: 'string',
+ enum: ['left', 'right', 'center'],
+ options: { enum_titles: ['Left', 'Center', 'Right'] },
+ default: 'left',
+ title: 'Paragraph',
+ description: 'When more than one line of text exists, how to align the text within the bounds.',
+ },
+ // TODO: What other alignment options do we need?
},
- };
+ },
+ rotation: {
+ type: 'number',
+ format: 'range',
+ default: 0,
+ minimum: -360,
+ maximum: 360,
+ step: 0.1,
+ title: 'Text rotation',
+ description: 'Angle to rotate the entire text object within the bounds.',
+ },
+};
- // Add system font enum.
- fontList.getFonts().then((fonts) => {
- properties.systemFont.enum = ['', ...fonts.map(s => s.replace(/"/g, ''))];
- });
+// Add system font enum.
+fontList.getFonts().then(fonts => {
+ properties.systemFont.enum = ['', ...fonts.map(s => s.replace(/"/g, ''))];
+});
- return { type: 'object', title: 'Text', properties };
+// Add SVG font enum.
+bindTo('drawing.text.setup', 'schemas.text', fonts => {
+ properties.font.enum = Object.keys(fonts);
+ properties.font.options.enum_titles = Object.values(fonts).map(font => font.name);
+});
+
+const schema = {
+ type: 'object',
+ title: 'Text',
+ properties,
};
+
+export default schema;
diff --git a/src/components/core/schemas/cncserver.schemas.tools.js b/src/components/core/schemas/cncserver.schemas.tools.js
new file mode 100644
index 00000000..e8ded62a
--- /dev/null
+++ b/src/components/core/schemas/cncserver.schemas.tools.js
@@ -0,0 +1,83 @@
+/**
+ * @file Individual Tool schema.
+ *
+ */
+/* eslint-disable max-len */
+const schema = {
+ type: 'object',
+ title: 'Tool',
+ description: 'A tool defines a specific location (and optionally area) outside of the workspace to change a property of the current drawing implement.',
+ required: ['id', 'x', 'y'],
+ properties: {
+ id: {
+ type: 'string',
+ title: 'ID',
+ description: 'Machine name for colorset tool.',
+ },
+ title: {
+ type: 'string',
+ title: 'Title',
+ description: 'Human readable name for the tool.',
+ },
+ x: {
+ type: 'number',
+ format: 'number',
+ title: 'X Point',
+ step: 0.1,
+ description: 'X coordinate of the position of the tool in mm.',
+ },
+ y: {
+ type: 'number',
+ format: 'number',
+ title: 'Y Point',
+ step: 0.1,
+ description: 'Y coordinate of top left position of the tool in mm away from parent top left.',
+ },
+ position: {
+ type: 'string',
+ title: 'Position Type',
+ description: 'Height of the usable tool area in mm.',
+ enum: ['center', 'topleft'],
+ options: { enum_titles: ['Center (default)', 'Top Left'] },
+ default: 'center',
+ },
+ width: {
+ type: 'number',
+ format: 'number',
+ title: 'Width',
+ description: 'Width of the usable tool area in mm.',
+ minimum: 1,
+ step: 0.1,
+ },
+ height: {
+ type: 'number',
+ format: 'number',
+ title: 'Height',
+ description: 'Height of the usable tool area in mm.',
+ minimum: 1,
+ step: 0.1,
+ },
+ radius: {
+ type: 'number',
+ format: 'number',
+ title: 'Radius',
+ description: 'Amount in mm to curve corners of tool area. Set to 0 for square corners.',
+ minimum: 0,
+ step: 0.1,
+ },
+ parent: {
+ type: 'string',
+ title: 'Parent tool',
+ description: 'Parent tool ID to base X and Y off of.',
+ default: '',
+ },
+ group: {
+ type: 'string',
+ title: 'Tool Group',
+ description: 'Arbitrary grouping tag for tools',
+ default: '',
+ },
+ },
+};
+
+export default schema;
diff --git a/src/components/core/schemas/cncserver.schemas.toolsets.js b/src/components/core/schemas/cncserver.schemas.toolsets.js
new file mode 100644
index 00000000..9f478fc5
--- /dev/null
+++ b/src/components/core/schemas/cncserver.schemas.toolsets.js
@@ -0,0 +1,41 @@
+/**
+ * @file Toolset tool holder.
+ *
+ */
+const schema = {
+ type: 'object',
+ title: 'Toolset',
+ description: 'A set of tools saved as a preset.',
+ required: ['name', 'title'],
+ properties: {
+ name: {
+ type: 'string',
+ title: 'Machine Name',
+ description: 'Machine name identifier for this toolset.',
+ },
+ manufacturer: {
+ type: 'string',
+ title: 'Manufacturer',
+ description: 'Original creator of the toolset.',
+ },
+ title: {
+ type: 'string',
+ title: 'Title',
+ description: 'Human readable name for the toolset.',
+ },
+ description: {
+ type: 'string',
+ title: 'Description',
+ description: 'Human readable description of the toolset.',
+ format: 'textarea',
+ },
+ items: {
+ type: 'array',
+ title: 'Tool Items',
+ description: 'Custom additional tools in the set.',
+ items: {}, // Set in indexer as tools schema.
+ },
+ },
+};
+
+export default schema;
diff --git a/src/components/core/schemas/cncserver.schemas.vectorize.js b/src/components/core/schemas/cncserver.schemas.vectorize.js
index a3180399..039fc850 100644
--- a/src/components/core/schemas/cncserver.schemas.vectorize.js
+++ b/src/components/core/schemas/cncserver.schemas.vectorize.js
@@ -6,124 +6,134 @@
*
*/
/* eslint-disable max-len */
-const fs = require('fs');
-const path = require('path');
+import fs from 'fs';
+import path from 'path';
+import { __basedir } from 'cs/utils';
const vectorizerSchema = 'cncserver.drawing.vectorizer.schema.js';
-module.exports = () => {
- const globalSchema = {
- render: {
- type: 'boolean',
- format: 'checkbox',
- title: 'Render',
- description: 'Render vectorization',
- default: true,
- },
- method: {
- type: 'string',
- title: 'Vectorize Method',
- description: 'The method used to turn a raster into paths.',
- default: 'basic',
- enum: [], // Filled below from available vectorizer method schemas.
- },
- raster: {
- type: 'object',
- title: 'Image Processing',
- description: 'All settings relating to raster image pre-processing.',
- options: { collapsed: true },
- properties: {
- brightness: {
- type: 'number',
- title: 'Brightness',
- description: 'How much to brighten or darken the image.',
- default: 0.05,
- minimum: -1,
- maximum: 1,
- },
- contrast: {
- type: 'number',
- title: 'Contrast',
- description: 'How much to adjust overall pixel contrast.',
- default: 0,
- minimum: -1,
- maximum: 1,
- },
- invert: {
- type: 'boolean',
- format: 'checkbox',
- title: 'Invert',
- description: 'Invert the pixel color before processing.',
- default: false,
- },
- grayscale: {
- type: 'boolean',
- title: 'Grayscale',
- format: 'checkbox',
- description: 'Convert the image to black and white grayscale.',
- default: false,
- },
- normalize: {
- type: 'boolean',
- title: 'Normalize',
- format: 'checkbox',
- description: 'Normalize color channels to correct for exposure/color temperature.',
- default: false,
- },
- flatten: {
- type: 'boolean',
- format: 'checkbox',
- title: 'Flatten transparency',
- description: 'Flatten alpha transparency in the image will to the flatten color.',
- default: false,
- },
- flattenColor: {
- type: 'string',
- format: 'color',
- title: 'Flatten Color',
- description: 'The color to treat as transparent, or to replace transparency.',
- default: '#FFFFFF',
- },
- resolution: {
- type: 'integer',
- title: 'Resolution',
- description: 'Raster re-render resolution in DPI, higher gives more detail.',
- default: 150,
- minimum: 25,
- maximum: 600,
- },
- blur: {
- type: 'integer',
- title: 'Blur',
- description: 'Blurs the image by the number of pixels specified',
- default: 0,
- minimum: 0,
- maximum: 50,
- },
+const globalSchema = {
+ render: {
+ type: 'boolean',
+ format: 'checkbox',
+ title: 'Render',
+ description: 'Render vectorization',
+ default: true,
+ },
+ method: {
+ type: 'string',
+ title: 'Vectorize Method',
+ description: 'The method used to turn a raster into paths.',
+ default: 'basic',
+ enum: [], // Filled below from available vectorizer method schemas.
+ options: { enum_titles: [] },
+ },
+ raster: {
+ type: 'object',
+ title: 'Image Processing',
+ description: 'All settings relating to raster image pre-processing.',
+ options: { collapsed: true },
+ properties: {
+ brightness: {
+ type: 'number',
+ title: 'Brightness',
+ description: 'How much to brighten or darken the image.',
+ default: 0.05,
+ minimum: -1,
+ maximum: 1,
+ step: 0.01,
+ },
+ contrast: {
+ type: 'number',
+ title: 'Contrast',
+ description: 'How much to adjust overall pixel contrast.',
+ default: 0,
+ minimum: -1,
+ maximum: 1,
+ step: 0.01,
+ },
+ invert: {
+ type: 'boolean',
+ format: 'checkbox',
+ title: 'Invert',
+ description: 'Invert the pixel color before processing.',
+ default: false,
+ },
+ grayscale: {
+ type: 'boolean',
+ title: 'Grayscale',
+ format: 'checkbox',
+ description: 'Convert the image to black and white grayscale.',
+ default: false,
+ },
+ normalize: {
+ type: 'boolean',
+ title: 'Normalize',
+ format: 'checkbox',
+ description: 'Normalize color channels to correct for exposure/color temperature.',
+ default: false,
+ },
+ flatten: {
+ type: 'boolean',
+ format: 'checkbox',
+ title: 'Flatten transparency',
+ description: 'Flatten alpha transparency in the image will to the flatten color.',
+ default: false,
+ },
+ flattenColor: {
+ type: 'string',
+ format: 'color',
+ title: 'Flatten Color',
+ description: 'The color to treat as transparent, or to replace transparency.',
+ default: '#FFFFFF',
+ },
+ resolution: {
+ type: 'integer',
+ title: 'Resolution',
+ description: 'Raster re-render resolution in DPI, higher gives more detail.',
+ default: 150,
+ minimum: 25,
+ maximum: 600,
+ },
+ blur: {
+ type: 'integer',
+ title: 'Blur',
+ description: 'Blurs the image by the number of pixels specified',
+ default: 0,
+ minimum: 0,
+ maximum: 50,
},
},
- };
+ },
+};
- // Compile list of all other filler modules and their schemas and merge them.
- const vectorizerPath = path.resolve(__dirname, '..', 'drawing', 'vectorizers');
- fs.readdirSync(vectorizerPath).map((dir) => {
- const fullPath = path.resolve(vectorizerPath, dir);
- if (fs.lstatSync(fullPath).isDirectory()) {
- const schemaPath = path.resolve(fullPath, vectorizerSchema);
- if (fs.existsSync(schemaPath)) {
- globalSchema.method.enum.push(dir);
+// Compile list of all other filler modules and their schemas and merge them.
+const vectorizerPath = path.resolve(__basedir, 'components', 'core', 'drawing', 'vectorizers');
+fs.readdirSync(vectorizerPath).map(dir => {
+ const fullPath = path.resolve(vectorizerPath, dir);
+ if (fs.lstatSync(fullPath).isDirectory()) {
+ const schemaPath = path.resolve(fullPath, vectorizerSchema);
+ if (fs.existsSync(schemaPath)) {
+ globalSchema.method.enum.push(dir);
- // eslint-disable-next-line
- const vectorizerSchemaObject = require(schemaPath);
+ // eslint-disable-next-line
+ import(schemaPath).then(({ default: vectorizerSchemaObject }) => {
+ globalSchema.method.options.enum_titles.push(vectorizerSchemaObject.title);
// Only add if vectorizer defines custom props.
if (Object.entries(vectorizerSchemaObject.properties).length) {
globalSchema[dir] = vectorizerSchemaObject;
globalSchema[dir].options = { dependencies: { method: dir } };
}
- }
+ });
}
- });
+ }
+});
- return { type: 'object', title: 'Vectorization', properties: globalSchema };
+const schema = {
+ type: 'object',
+ title: 'Vectorization',
+ properties: globalSchema,
};
+
+export default schema;
diff --git a/src/components/core/schemas/index.js b/src/components/core/schemas/index.js
index bfb872ab..f691740b 100644
--- a/src/components/core/schemas/index.js
+++ b/src/components/core/schemas/index.js
@@ -1,29 +1,55 @@
/**
* @file Settings schema indexer.
- *
*/
-/* eslint-disable global-require, import/no-dynamic-require */
-module.exports = (cncserver) => {
- // TODO: Schemas to finalize: stroke, vectorize, text
- const items = ['projects', 'content', 'fill', 'stroke', 'text', 'vectorize', 'path'];
- const settingsKeys = ['fill', 'stroke', 'text', 'vectorize', 'path'];
+import projects from 'cs/schemas/projects';
+import content from 'cs/schemas/content';
+import contentSettings from 'cs/schemas/content/settings';
+import fill from 'cs/schemas/fill';
+import stroke from 'cs/schemas/stroke';
+import text from 'cs/schemas/text';
+import vectorize from 'cs/schemas/vectorize';
+import path from 'cs/schemas/path';
+import color from 'cs/schemas/color';
+import colors from 'cs/schemas/colors';
+import tools from 'cs/schemas/tools';
+import implementSchema from 'cs/schemas/implements';
+import toolsets from 'cs/schemas/toolsets';
- const schemas = {};
- const settingsKeySchemas = {};
+// TODO: Schemas to finalize: stroke, vectorize, text
- items.forEach((name) => {
- schemas[name] = require(`./cncserver.schemas.${name}.js`)(cncserver);
- if (settingsKeys.includes(name)) settingsKeySchemas[name] = schemas[name];
- });
+// All schemas that define content specific settings.
+const settingsSchemas = {
+ fill,
+ stroke,
+ text,
+ vectorize,
+ path,
+};
- // Build the content "settings" schema.
- schemas.settings = require(
- './cncserver.schemas.content.settings.js'
- )(settingsKeySchemas);
+const schemas = {
+ projects,
+ content,
+ settings: contentSettings(settingsSchemas),
+ fill,
+ stroke,
+ text,
+ vectorize,
+ path,
+ color,
+ colors,
+ tools,
+ implements: implementSchema,
+ toolsets,
+};
- // Attach to content and projects schemas.
- schemas.content.properties.settings = schemas.settings;
- schemas.projects.properties.settings = schemas.settings;
+// Add the color schema to the colors.items schema.
+schemas.colors.properties.items.items = schemas.color;
- return schemas;
-};
+// Add the tool schema to the toolset.items schema.
+schemas.toolsets.properties.items.items = schemas.tools;
+
+// Attach to content and projects schemas.
+schemas.content.properties.settings = schemas.settings;
+schemas.projects.properties.settings = schemas.settings;
+
+export default schemas;
diff --git a/src/components/core/utils/cncserver.binder.js b/src/components/core/utils/cncserver.binder.js
index 8fe06bf0..e5485775 100644
--- a/src/components/core/utils/cncserver.binder.js
+++ b/src/components/core/utils/cncserver.binder.js
@@ -1,63 +1,74 @@
/**
* @file Util helper for binding to arbitrary events.
*/
-const binder = {}; // Exposed export.
-
-
-module.exports = (cncserver) => {
- const hooks = {};
-
- /**
- * Binder binder! Registers a callback for an arbitrary event.
- *
- * @param {string} event
- * Named event to trigger on.
- * @param {string} caller
- * Who's binding this? ensures there's only one binding for each caller.
- * @param {function} callback
- Function called when the event is triggered.
- */
- binder.bindTo = (event, caller, callback) => {
- if (typeof hooks[event] !== 'object') {
- hooks[event] = {};
+const hooks = { };
+const lateTrigger = {};
+const debug = false;
+
+/**
+ * Binder binder! Registers a callback for an arbitrary event.
+ *
+ * @param {string} event
+ * Named event to trigger on.
+ * @param {string} caller
+ * Who's binding this? ensures there's only one binding for each caller.
+ * @param {function} callback
+ Function called when the event is triggered.
+ */
+export function bindTo(event, caller, callback) {
+ if (debug) console.log('BINDER: BindTo', event, 'from', caller);
+ if (typeof hooks[event] !== 'object') {
+ hooks[event] = {};
+ }
+
+ hooks[event][caller] = callback;
+
+ // Immediately trigger binding if there's a late binding trigger.
+ if (lateTrigger[event]) {
+ if (debug) console.log('BINDER: LATE TRIGGER', event, caller);
+ callback(lateTrigger[event]);
+ }
+}
+
+/**
+ * Trigger an event callback for any bound event callers.
+ *
+ * @param {string} event
+ * Event name being triggered.
+ * @param {object} payload
+ * Optional data payload to hand to bound callbacks.
+ * @param {bool} allowLateTrigger
+ * If true, late bindings will trigger with last payload.
+ */
+export function trigger(event, payload = {}, allowLateTrigger = false) {
+ let runningPayload = payload;
+
+ // Allow late triggering for lazy binders?
+ if (allowLateTrigger) {
+ lateTrigger[event] = payload;
+ }
+
+ if (debug) console.log('BINDER: TRIGGER', event);
+
+ if (typeof hooks[event] === 'object') {
+ // Debug for unbound triggers.
+ if (debug && !Object.keys(hooks[event]).length) {
+ console.log(`BINDER: Event "${event}" triggered with NO BOUND IMPLEMENTORS`);
}
+ for (const [caller, callback] of Object.entries(hooks[event])) {
+ if (typeof callback === 'function') {
+ if (debug) {
+ console.log(`BINDER: Event "${event}" triggered for "${caller}" with`, payload);
+ }
+ runningPayload = callback(runningPayload);
- hooks[event][caller] = callback;
- };
-
- /**
- * Trigger an event callback for any bound event callers.
- *
- * @param {string} event
- * Event name being triggered.
- * @param {object} payload
- * Optional data payload to hand to bound callbacks.
- */
- binder.trigger = (event, payload = {}) => {
- let runningPayload = payload;
-
- if (typeof hooks[event] === 'object') {
- // Debug for unbound triggers.
- if (cncserver.settings.gConf.get('debug') && !Object.keys(hooks[event]).length) {
- console.log(`Event "${event}" triggered with NO BOUND IMPLEMENTORS`);
- }
- for (const [caller, callback] of Object.entries(hooks[event])) {
- if (typeof callback === 'function') {
- if (cncserver.settings.gConf.get('debug')) {
- console.log(`Event "${event}" triggered for "${caller}" with`, payload);
- }
- runningPayload = callback(runningPayload);
-
- // If a binder implementation doesn't return, reset the payload.
- if (runningPayload === undefined) {
- runningPayload = payload;
- }
+ // If a binder implementation doesn't return, reset the payload.
+ if (runningPayload === undefined) {
+ runningPayload = payload;
}
}
}
+ }
- return runningPayload;
- };
-
- return binder;
-};
+ return runningPayload;
+}
diff --git a/src/components/core/utils/cncserver.buffer.js b/src/components/core/utils/cncserver.buffer.js
index d45167e2..9ef96568 100644
--- a/src/components/core/utils/cncserver.buffer.js
+++ b/src/components/core/utils/cncserver.buffer.js
@@ -1,313 +1,308 @@
/**
* @file Abstraction module for the run/queue utilities for CNC Server!
*/
-let buffer = {};
-
-module.exports = (cncserver) => {
- // Buffer State variables
- buffer = {
- dataSet: {}, // Holds the actual buffer data keyed by hash.
- data: [], // Holds the order of the in a flat array of hashes.
- running: false, // Are we running? True if items in buffer/not paused
- paused: false, // Are we paused?
- newlyPaused: false, // Trigger for pause callback on executeNext()
- pauseCallback: null, // Temporary callback storage when pause is complete.
- pausePen: null, // Hold the state when paused initiated for resuming
- };
- /**
- * Helper function for clearing the buffer. Used mainly by plugins.
- */
- buffer.clear = () => {
- buffer.data = [];
-
- // Reset the state of the buffer tip pen to the state of the actual robot.
- // If this isn't done, it will be assumed to be a state that was deleted
- // and never sent out.
- cncserver.pen.resetState();
- cncserver.sockets.sendBufferVars();
- };
+import { getHash, extend, getHeightChangeData } from 'cs/utils';
+import { trigger as binderTrigger } from 'cs/binder';
+import { sendMessage } from 'cs/ipc';
+import { bot, gConf } from 'cs/settings';
+import { resetState, getPosChangeData } from 'cs/pen';
+import { state as actualPenState, forceState as forceActualPen } from 'cs/actualPen';
+import { setBatchRunningState } from 'cs/api';
+import {
+ sendBufferVars,
+ sendBufferAdd,
+ sendBufferRemove,
+ sendBufferComplete,
+ sendMessageUpdate,
+ sendCallbackUpdate
+} from 'cs/sockets';
+
+// Buffer State variables
+export const state = {
+ dataSet: {}, // Holds the actual buffer data keyed by hash.
+ data: [], // Holds the order of the in a flat array of hashes.
+ running: false, // Are we running? True if items in buffer/not paused
+ paused: false, // Are we paused?
+ newlyPaused: false, // Trigger for pause callback on executeNext()
+ pauseCallback: null, // Temporary callback storage when pause is complete.
+ pausePen: null, // Hold the state when paused initiated for resuming
+};
- /**
- * Setter for the internal buffer running flag.
- *
- * @param {boolean} runState
- * True to keep buffer running, false to stop buffer on next loop.
- */
- buffer.setRunning = (runState) => {
- buffer.running = !!runState;
- };
+/**
+ * Setter for the internal buffer running flag.
+ *
+ * @param {boolean} runState
+ * True to keep buffer running, false to stop buffer on next loop.
+ */
+export function setRunning(runState) {
+ state.running = !!runState;
+}
- /**
- * Setter for the internal buffer newly paused trigger flag.
- *
- * @param {boolean} pauseState
- * True to allow triggering of pauseCallback, false to end trigger state.
- */
- buffer.setNewlyPaused = (pauseState) => {
- buffer.newlyPaused = !!pauseState;
- };
+/**
+ * Setter for the internal buffer newly paused trigger flag.
+ *
+ * @param {boolean} pauseState
+ * True to allow triggering of pauseCallback, false to end trigger state.
+ */
+export function setNewlyPaused(pauseState) {
+ state.newlyPaused = !!pauseState;
+}
- /**
- * Setter for the internal buffer paused trigger callback.
- *
- * @param {function} pauseCB
- */
- buffer.setPauseCallback = (pauseCB) => {
- buffer.pauseCallback = () => {
- buffer.newlyPaused = false;
- buffer.pauseCallback = null;
- pauseCB();
- };
+/**
+ * Setter for the internal buffer paused trigger callback.
+ *
+ * @param {function} pauseCB
+ */
+export function setPauseCallback(pauseCB) {
+ state.pauseCallback = () => {
+ state.newlyPaused = false;
+ state.pauseCallback = null;
+ pauseCB();
};
+}
- // Pause the buffer running.
- buffer.pause = () => {
- buffer.paused = true;
-
- // Hold on to the current actualPen to return to before resuming.
- buffer.pausePen = cncserver.utils.extend(
- {}, cncserver.actualPen.state
+/**
+ * Create a bot specific serial command string from a key:value object
+ *
+ * @param {string} name
+ * Key in bot.commands object to find the command string
+ * @param {object} values
+ * Object containing the keys of placeholders to find in command string,
+ * with value to replace placeholder.
+ *
+ * @returns {string}
+ * Serial command string intended to be outputted directly, empty string
+ * if error.
+ */
+export function cmdstr(name, values = {}) {
+ if (!name || !bot.commands[name]) return ''; // Sanity check
+ let out = bot.commands[name];
+
+ for (const [key, value] of Object.entries(values)) {
+ out = out.replace(`%${key}`, value);
+ }
+
+ return out;
+}
+
+// Pause the buffer running.
+export function pause() {
+ state.paused = true;
+
+ // Hold on to the current actualPen to return to before resuming.
+ state.pausePen = { ...actualPenState };
+ sendMessage('buffer.pause');
+ sendBufferVars();
+}
+
+// Resume the buffer running.
+export function resume() {
+ state.paused = false;
+ state.pausePen = null;
+ sendMessage('buffer.resume');
+ sendBufferVars();
+}
+
+// Toggle the state
+export function toggle(setPause) {
+ if (setPause && !state.paused) {
+ pause();
+ } else if (!setPause && state.paused) {
+ resume();
+ }
+}
+
+// Event for when a buffer has been started.
+export function startItem(hash) {
+ if (gConf.get('debug')) {
+ console.log(`Buffer RUN [${hash}]`);
+ }
+ const index = state.data.indexOf(hash);
+ if (state.dataSet[hash] && index > -1) {
+ const item = state.dataSet[hash];
+
+ // Update the state of the actualPen to match the one in the buffer.
+ item.pen.bufferHash = hash;
+ forceActualPen(item.pen);
+ } else {
+ // TODO: when this happens, account for why or PREVENT IT.
+ console.error(
+ 'IPC/Buffer Item or Hash Mismatch. This should never happen!',
+ hash,
+ `Index: ${index}`
);
- cncserver.ipc.sendMessage('buffer.pause');
- cncserver.sockets.sendBufferVars();
- };
+ }
+}
- // Resume the buffer running.
- buffer.resume = () => {
- buffer.paused = false;
- buffer.pausePen = null;
- cncserver.ipc.sendMessage('buffer.resume');
- cncserver.sockets.sendBufferVars();
- };
-
- // Toggle the state
- buffer.toggle = (setPause) => {
- if (setPause && !buffer.paused) {
- buffer.pause();
- } else if (!setPause && buffer.paused) {
- buffer.resume();
- }
- };
-
- // Add an object to the buffer.
- buffer.addItem = (item) => {
- const hash = cncserver.utils.getHash(item);
- if (cncserver.settings.gConf.get('debug')) {
- console.log(`Buffer ADD [${hash}]:`, item);
+/**
+ * Trigger non-serial commands in local buffer items on execution by the
+ * runner. The runner can't do anything with these except say that their
+ * place in line has come.
+ *
+ * @param {object} item
+ * Buffer item to check/trigger.
+ *
+ * @return {boolean}
+ * True if triggered, false if not applicable.
+ */
+export function trigger(item) {
+ if (typeof item.command === 'function') { // Custom Callback buffer item
+ // Just call the callback function.
+ item.command(1);
+ return true;
+ }
+
+ if (typeof item.command === 'object') { // Detailed buffer object
+ switch (item.command.type) {
+ case 'message':
+ sendMessageUpdate(item.command.message);
+ return true;
+ case 'callbackname':
+ sendCallbackUpdate(item.command.name);
+ return true;
+ default:
}
-
- buffer.data.unshift(hash);
- buffer.dataSet[hash] = item;
-
- // Add the item to the runner's buffer.
- cncserver.ipc.sendMessage('buffer.add', {
+ }
+
+ return false;
+}
+
+// Remove an object with the specific hash from the buffer.
+//
+// This should only be called by the process running the buffer, and denotes
+// when an item is run into the machine.
+export function removeItem(hash) {
+ const index = state.data.indexOf(hash);
+ if (state.dataSet[hash] && index > -1) {
+ state.data.splice(index, 1);
+ const item = state.dataSet[hash];
+
+ // For buffer items with non-serial commands, it's time to do something!
+ trigger(item);
+
+ delete state.dataSet[hash];
+ sendBufferRemove();
+ } else if (state.data.length) {
+ // This is really only an issue if we didn't just clear the buffer.
+ console.error(
+ 'End IPC/Buffer Item & Hash Mismatch. This should never happen!',
hash,
- ...buffer.render(item),
- });
-
- cncserver.sockets.sendBufferAdd(item, hash); // Alert clients.
- return hash;
- };
-
- // Event for when a buffer has been started.
- buffer.startItem = (hash) => {
- if (cncserver.settings.gConf.get('debug')) {
- console.log(`Buffer RUN [${hash}]`);
- }
- const index = buffer.data.indexOf(hash);
- if (buffer.dataSet[hash] && index > -1) {
- const item = buffer.dataSet[hash];
-
- // Update the state of the actualPen to match the one in the buffer.
- item.pen.bufferHash = hash;
- cncserver.actualPen.updateState(item.pen);
- } else {
- // TODO: when this happens, account for why or PREVENT IT.
- console.error(
- 'IPC/Buffer Item or Hash Mismatch. This should never happen!',
- hash,
- `Index: ${index}`
- );
- }
- };
-
- // Remove an object with the specific hash from the buffer.
- //
- // This should only be called by the process running the buffer, and denotes
- // when an item is run into the machine.
- buffer.removeItem = (hash) => {
- const index = buffer.data.indexOf(hash);
- if (buffer.dataSet[hash] && index > -1) {
- buffer.data.splice(index, 1);
- const item = buffer.dataSet[hash];
-
- // For buffer items with non-serial commands, it's time to do something!
- buffer.trigger(item);
-
- delete buffer.dataSet[hash];
- cncserver.sockets.sendBufferRemove();
- } else if (buffer.data.length) {
- // This is really only an issue if we didn't just clear the buffer.
- console.error(
- 'End IPC/Buffer Item & Hash Mismatch. This should never happen!',
- hash,
- `Index: ${index}`
- );
- }
+ `Index: ${index}`
+ );
+ }
- // Trigger the pause callback if it exists when this item is done.
- if (typeof buffer.pauseCallback === 'function') {
- buffer.pauseCallback();
- }
- };
+ // Trigger the pause callback if it exists when this item is done.
+ if (typeof state.pauseCallback === 'function') {
+ state.pauseCallback();
+ }
+}
- /**
- * Helper function for clearing the buffer.
- */
- buffer.clear = (isEmpty) => {
- buffer.data = [];
- buffer.dataSet = {};
+/**
+ * Helper function for clearing the buffer.
+ */
+export function clear(isEmpty) {
+ state.data.length = 0;
+ state.dataSet = {};
- buffer.pausePen = null; // Resuming with an empty buffer is silly
- buffer.paused = false;
+ state.pausePen = null; // Resuming with an empty buffer is silly
+ state.paused = false;
- // If we're clearing, we need to kill any batch processes running.
- cncserver.api.setBatchRunningState(false);
+ // If we're clearing, we need to kill any batch processes running.
+ setBatchRunningState(false);
- // Reset the state of the buffer tip pen to the state of the actual robot.
- // If this isn't done, it will be assumed to be a state that was deleted
- // and never sent out.
- cncserver.pen.resetState();
+ // Reset the state of the buffer tip pen to the state of the actual robot.
+ // If this isn't done, it will be assumed to be a state that was deleted
+ // and never sent out.
+ resetState();
- // Detect if this came from IPC runner being empty or not.
- if (!isEmpty) {
- cncserver.ipc.sendMessage('buffer.clear');
- console.log('Run buffer cleared!');
- }
+ // Detect if this came from IPC runner being empty or not.
+ if (!isEmpty) {
+ sendMessage('buffer.clear');
+ console.log('Run buffer cleared!');
+ }
- // Send full update as it's been cleared.
- cncserver.sockets.sendBufferComplete();
+ // Send full update as it's been cleared.
+ sendBufferComplete();
- // Trigger the event.
- cncserver.binder.trigger('buffer.clear');
- };
+ // Trigger the event.
+ binderTrigger('buffer.clear');
+}
- /**
- * Render an action item into an array of serial command strings.
- *
- * @param {object} item
- * The raw buffer "action" item.
- *
- * @return {object}
- * Object containing keys:
- * commands: array of all serial command strings rendered from item.
- * duration: numeric duration (in milliseconds) that item should take.
- */
- buffer.render = (item) => {
- let commands = [];
- let duration = 0;
-
- if (typeof item.command === 'object') { // Detailed buffer object
- switch (item.command.type) {
- case 'absmove':
- // eslint-disable-next-line
- const posChangeData = cncserver.utils.getPosChangeData(
+/**
+ * Render an action item into an array of serial command strings.
+ *
+ * @param {object} item
+ * The raw buffer "action" item.
+ *
+ * @return {object}
+ * Object containing keys:
+ * commands: array of all serial command strings rendered from item.
+ * duration: numeric duration (in milliseconds) that item should take.
+ */
+export function render(item) {
+ let commands = [];
+ let duration = 0;
+
+ if (typeof item.command === 'object') { // Detailed buffer object
+ switch (item.command.type) {
+ case 'absmove':
+ // eslint-disable-next-line
+ const posChangeData = getPosChangeData(
+ item.command.source,
+ item.command
+ );
+
+ posChangeData.d = item.duration;
+ duration = posChangeData.d;
+ commands = [cmdstr('movexy', posChangeData)];
+ break;
+
+ case 'absheight':
+ // To set Height, we can set a rate for how slow it moves,
+ // - servo.minduration is minimum time.
+ // - Don't set duration if moving at same time
+ //
+ commands = [cmdstr('movez', {
+ r: 2200,
+ z: item.command.z,
+ d: getHeightChangeData(
item.command.source,
- item.command
- );
-
- posChangeData.d = item.duration;
- duration = posChangeData.d;
- commands = [buffer.cmdstr('movexy', posChangeData)];
- break;
-
- case 'absheight':
- // To set Height, we can set a rate for how slow it moves,
- // - servo.minduration is minimum time.
- // - Don't set duration if moving at same time
- //
- commands = [buffer.cmdstr('movez', {
- r: 2200,
- z: item.command.z,
- d: cncserver.utils.getHeightChangeData(
- item.command.source,
- item.command.z
- ).d,
- })];
-
- break;
-
- case 'special':
- return { commands, duration, special: item.command.data };
-
- default:
- }
- } else if (typeof item.command === 'string') {
- // Serial command is direct string in item.command, no render needed.
- commands = [item.command];
- }
+ item.command.z
+ ).d,
+ })];
- return { commands, duration };
- };
+ break;
- /**
- * Trigger non-serial commands in local buffer items on execution by the
- * runner. The runner can't do anything with these except say that their
- * place in line has come.
- *
- * @param {object} item
- * Buffer item to check/trigger.
- *
- * @return {boolean}
- * True if triggered, false if not applicable.
- */
- buffer.trigger = (item) => {
- if (typeof item.command === 'function') { // Custom Callback buffer item
- // Just call the callback function.
- item.command(1);
- return true;
- }
+ case 'special':
+ return { commands, duration, special: item.command.data };
- if (typeof item.command === 'object') { // Detailed buffer object
- switch (item.command.type) {
- case 'message':
- cncserver.sockets.sendMessageUpdate(item.command.message);
- return true;
- case 'callbackname':
- cncserver.sockets.sendCallbackUpdate(item.command.name);
- return true;
- default:
- }
+ default:
}
-
- return false;
- };
-
- /**
- * Create a bot specific serial command string from a key:value object
- *
- * @param {string} name
- * Key in cncserver.settings.bot.commands object to find the command string
- * @param {object} values
- * Object containing the keys of placeholders to find in command string,
- * with value to replace placeholder.
- *
- * @returns {string}
- * Serial command string intended to be outputted directly, empty string
- * if error.
- */
- buffer.cmdstr = (name, values = {}) => {
- if (!name || !cncserver.settings.bot.commands[name]) return ''; // Sanity check
-
- let out = cncserver.settings.bot.commands[name];
-
- for (const [key, value] of Object.entries(values)) {
- out = out.replace(`%${key}`, value);
- }
-
- return out;
- };
-
- return buffer;
-};
+ } else if (typeof item.command === 'string') {
+ // Serial command is direct string in item.command, no render needed.
+ commands = [item.command];
+ }
+
+ return { commands, duration };
+}
+
+// Add an object to the buffer.
+export function addItem(item) {
+ const hash = getHash(item);
+ if (gConf.get('debug')) {
+ console.log(`Buffer ADD [${hash}]:`, item);
+ }
+
+ state.data.unshift(hash);
+ state.dataSet[hash] = item;
+
+ // Add the item to the runner's buffer.
+ sendMessage('buffer.add', {
+ hash,
+ ...render(item),
+ });
+
+ sendBufferAdd(item, hash); // Alert clients.
+ return hash;
+}
diff --git a/src/components/core/utils/cncserver.i18n.js b/src/components/core/utils/cncserver.i18n.js
index b8065c89..dc58cc29 100644
--- a/src/components/core/utils/cncserver.i18n.js
+++ b/src/components/core/utils/cncserver.i18n.js
@@ -2,45 +2,46 @@
* @file Glue module to manage i18n localization initialization, etc.
* @see https://www.i18next.com/translation-function/essentials
*/
-const i18next = require('i18next');
-const i18nextFSBackend = require('i18next-node-fs-backend');
-const i18nextMiddleware = require('i18next-express-middleware');
-const path = require('path');
+import i18next from 'i18next';
+import i18nextFSBackend from 'i18next-node-fs-backend';
+import i18nextMiddleware from 'i18next-express-middleware';
+import path from 'path';
+import { bindTo } from 'cs/binder';
+import { __basedir } from 'cs/utils';
-const i18n = { id: 'i18n', t: {} }; // Final Object to be exported.
-
-module.exports = (cncserver) => {
- // Initialize full i18next stack with middleware.
- i18next
- .use(i18nextMiddleware.LanguageDetector)
- .use(i18nextFSBackend)
- .init({
- preload: ['ar', 'bn', 'de', 'en-US', 'es', 'fr', 'hi', 'pt', 'ru', 'ur', 'zh-CN'],
- fallbackLng: ['en-US'],
- lng: 'en',
- // debug: true,
- ns: ['common', 'colorsets'],
- defaultNS: 'common',
- detection: {
- lookupHeader: 'accept-language',
- },
- backend: {
- loadPath: path.join(global.__basedir, 'locales', '{{ns}}', '{{lng}}.json'),
- addPath: path.join(global.__basedir, 'locales', '{{ns}}', '{{lng}}.missing.json'),
- },
- })
- .then((t) => {
- i18n.n = i18next;
- i18n.t = t;
- })
- .catch((err) => {
- console.log(err);
- });
+export const i18n = {
+ t: s => s,
+ n: {},
+};
- // Bind to server config to initialize locale detection.
- cncserver.binder.bindTo('server.configure', i18n.id, (app) => {
- app.use(i18nextMiddleware.handle(i18next));
+// Initialize full i18next stack with middleware.
+i18next
+ .use(i18nextMiddleware.LanguageDetector)
+ .use(i18nextFSBackend)
+ .init({
+ preload: ['ar', 'bn', 'de', 'en-US', 'es', 'fr', 'hi', 'pt', 'ru', 'ur', 'zh-CN'],
+ fallbackLng: ['en-US'],
+ lng: 'en',
+ // debug: true,
+ ns: ['common', 'colorsets'],
+ defaultNS: 'common',
+ detection: {
+ lookupHeader: 'accept-language',
+ },
+ backend: {
+ loadPath: path.join(__basedir, 'locales', '{{ns}}', '{{lng}}.json'),
+ addPath: path.join(__basedir, 'locales', '{{ns}}', '{{lng}}.missing.json'),
+ },
+ })
+ .then(t => {
+ i18n.n = i18next;
+ i18n.t = t;
+ })
+ .catch(err => {
+ console.log(err);
});
- return i18n;
-};
+// Bind to server config to initialize locale detection.
+bindTo('server.configure', 'i18n', app => {
+ app.use(i18nextMiddleware.handle(i18next));
+});
diff --git a/src/components/core/utils/cncserver.run.js b/src/components/core/utils/cncserver.run.js
index 08a46368..80ec2107 100644
--- a/src/components/core/utils/cncserver.run.js
+++ b/src/components/core/utils/cncserver.run.js
@@ -2,100 +2,98 @@
* @file Abstraction module for the cncserver run utility
*/
-let run;
+import * as pen from 'cs/pen';
+import { cmdstr, addItem } from 'cs/buffer';
+import { bot } from 'cs/settings';
-module.exports = (cncserver) => {
- /**
- * Add a command to the command runner buffer.
- *
- * @param {string} command
- * The command type to be run, must be one of the supported:
- * - move
- * - height
- * - message
- * - callbackname
- * - wait
- * - custom
- * - callback
- * @param {object} data
- * The data to be applied in the command.
- * @param {int} rawDuration
- * The time in milliseconds this command should take to run.
- *
- * @returns {boolean}
- * Return false if failure, true if success
- */
- run = (command, data, rawDuration) => {
- let c = '';
-
- // Sanity check duration to minimum of 1, int only
- let duration = !rawDuration ? 1 : Math.abs(parseInt(rawDuration, 10));
- duration = duration <= 0 ? 1 : duration;
+/**
+ * Add a command to the command runner buffer.
+ *
+ * @param {string} command
+ * The command type to be run, must be one of the supported:
+ * - move
+ * - height
+ * - message
+ * - callbackname
+ * - wait
+ * - custom
+ * - callback
+ * @param {object} data
+ * The data to be applied in the command.
+ * @param {int} rawDuration
+ * The time in milliseconds this command should take to run.
+ *
+ * @returns {boolean}
+ * Return false if failure, true if success
+ */
+export default function run(command, data, rawDuration) {
+ let c = '';
- switch (command) {
- case 'move':
- // Detailed buffer object X and Y.
- c = {
- type: 'absmove',
- x: data.x,
- y: data.y,
- source: data.source,
- };
- break;
+ // Sanity check duration to minimum of 1, int only
+ let duration = !rawDuration ? 1 : Math.abs(parseInt(rawDuration, 10));
+ duration = duration <= 0 ? 1 : duration;
- case 'height':
- // Detailed buffer object with z height and state string
- c = {
- type: 'absheight',
- z: data.z,
- source: data.source,
- state: cncserver.pen.state.state,
- };
- break;
+ switch (command) {
+ case 'move':
+ // Detailed buffer object X and Y.
+ c = {
+ type: 'absmove',
+ x: data.x,
+ y: data.y,
+ source: data.source,
+ };
+ break;
- case 'message':
- // Detailed buffer object with a string message
- c = { type: 'message', message: data };
- break;
+ case 'height':
+ // Detailed buffer object with z height and state string
+ c = {
+ type: 'absheight',
+ z: data.z,
+ source: data.source,
+ state: pen.state.state,
+ };
+ break;
- case 'callbackname':
- // Detailed buffer object with a callback machine name
- c = { type: 'callbackname', name: data };
- break;
+ case 'message':
+ // Detailed buffer object with a string message
+ c = { type: 'message', message: data };
+ break;
- case 'wait':
- // Send wait, blocking buffer
- if (!cncserver.settings.bot.commands.wait) return false;
- c = cncserver.buffer.cmdstr('wait', { d: duration });
- break;
+ case 'callbackname':
+ // Detailed buffer object with a callback machine name
+ c = { type: 'callbackname', name: data };
+ break;
- case 'custom':
- c = data;
- break;
+ case 'wait':
+ // Send wait, blocking buffer
+ if (!bot.commands.wait) return false;
+ c = cmdstr('wait', { d: duration });
+ break;
- case 'callback': // Custom callback runner for API return triggering
- c = data;
- break;
+ case 'custom':
+ c = data;
+ break;
- case 'special': // Low level special command, executed only in the runner.
- c = { type: 'special', data };
- break;
+ case 'callback': // Custom callback runner for API return triggering
+ c = data;
+ break;
- default:
- return false;
- }
+ case 'special': // Low level special command, executed only in the runner.
+ c = { type: 'special', data };
+ break;
- // Add final command and duration to end of queue, along with a copy of the
- // pen state at this point in time to be copied to actualPen after execution
- cncserver.pen.forceState({ lastDuration: duration });
- cncserver.buffer.addItem({
- command: c,
- duration,
- pen: cncserver.utils.extend({}, cncserver.pen.state),
- });
+ default:
+ return false;
+ }
- return true;
- };
+ // Add final command and duration to end of queue, along with a copy of the
+ // pen state at this point in time to be copied to actualPen after execution
+ pen.forceState({ lastDuration: duration });
+ addItem({
+ command: c,
+ duration,
+ pen: { ...pen.state },
+ });
- return run;
-};
+ return true;
+}
diff --git a/src/components/core/utils/cncserver.settings.js b/src/components/core/utils/cncserver.settings.js
index fc99cc46..8249c71d 100644
--- a/src/components/core/utils/cncserver.settings.js
+++ b/src/components/core/utils/cncserver.settings.js
@@ -2,226 +2,224 @@
* @file Abstraction module for all settings related code for CNC Server!
*
*/
-const nconf = require('nconf'); // Configuration and INI file.
-const fs = require('fs'); // File System management.
-const path = require('path'); // Path management and normalization.
-const ini = require('ini'); // Reading INI files.
-
-const settings = {}; // Global core component export object to be attached.
-
-module.exports = (cncserver) => {
- settings.gConf = new nconf.Provider();
- settings.botConf = new nconf.Provider();
- settings.bot = {};
-
- // Pull conf from env, or arguments
- settings.gConf.env().argv();
-
- /**
- * Initialize/load the global cncserver configuration file & options.
- *
- * @param {function} cb
- * Optional callback triggered when complete.
- */
- settings.loadGlobalConfig = (cb) => {
- // Pull conf from file
- const configPath = path.resolve(global.__basedir, '..', 'config.ini');
- settings.gConf.reset();
- settings.gConf.use('file', {
- file: configPath,
- format: nconf.formats.ini,
- }).load(() => {
- // Set Global Config Defaults
- settings.gConf.defaults(cncserver.globalConfigDefaults);
-
- // Save Global Conf file defaults if not saved
- if (!fs.existsSync(configPath)) {
- const def = settings.gConf.stores.defaults.store;
- for (const [key, value] in Object.entries(def)) {
- if (key !== 'type') {
- settings.gConf.set(key, value);
- }
- }
+import nconf from 'nconf'; // Configuration and INI file.
+import fs from 'fs'; // File System management.
+import path from 'path'; // Path management and normalization.
+import ini from 'ini'; // Reading INI files.
+
+import { trigger } from 'cs/binder';
+import { __basedir } from 'cs/utils';
+
+// Export base constants.
+export const gConf = new nconf.Provider();
+export const botConf = new nconf.Provider();
+export const bot = {};
+
+// Global Defaults (also used to write the initial config.ini)
+export const globalConfigDefaults = {
+ httpPort: 4242,
+ httpLocalOnly: true,
+ swapMotors: false, // Global setting for bots that don't have it configured
+ invertAxis: {
+ x: false,
+ y: false,
+ },
+ maximumBlockingCallStack: 100, // Limit for the # blocking sequential calls
+ showSerial: false, // Specific debug to show serial data.
+ serialPath: '{auto}', // Empty for auto-config
+ bufferLatencyOffset: 30, // Number of ms to move each command closer together
+ corsDomain: '*', // Start as open to CORs enabled browser clients
+ debug: false,
+ botType: 'watercolorbot',
+ scratchSupport: true,
+ flipZToggleBit: false,
+ botOverride: {
+ info: 'Override bot settings E.G. > [botOverride.eggbot] servo:max = 1234',
+ },
+};
+
+// Pull conf from env, or arguments
+gConf.env().argv();
- // Should be sync/blocking save with no callback
- settings.gConf.save();
+/**
+ * Initialize/load the global cncserver configuration file & options.
+ *
+ * @param {function} cb
+ * Optional callback triggered when complete.
+ */
+export function loadGlobalConfig(cb) {
+ // Pull conf from file
+ const configPath = path.resolve(__basedir, '..', 'config.ini');
+ gConf.reset();
+ gConf.use('file', {
+ file: configPath,
+ format: nconf.formats.ini,
+ }).load(() => {
+ // Set Global Config Defaults
+ gConf.defaults(globalConfigDefaults);
+
+ // Save Global Conf file defaults if not saved
+ if (!fs.existsSync(configPath)) {
+ const def = gConf.stores.defaults.store;
+ for (const [key, value] in Object.entries(def)) {
+ if (key !== 'type') {
+ gConf.set(key, value);
+ }
}
- if (cb) cb(); // Trigger the callback
+ // Should be sync/blocking save with no callback
+ gConf.save();
+ }
- // Output if debug mode is on
- if (settings.gConf.get('debug')) {
- console.info('== CNCServer Debug mode is ON ==');
- }
- });
- };
-
- /**
- * Load bot specific config file
- *
- * @param {function} cb
- * Callback triggered when loading is complete
- * @param {string} botType
- * Optional, the machine name for the bot type to load. Defaults to the
- * globally configured bot type.
- */
- settings.loadBotConfig = (cb, botType = settings.gConf.get('botType')) => {
- const botFile = path.resolve(
- global.__basedir,
- '..',
- 'machine_types',
- `${botType}.ini`
- );
+ if (cb) cb(); // Trigger the callback
- if (!fs.existsSync(botFile)) {
- console.error(
- `Bot configuration file "${botFile}" doesn't exist. Error #16`
- );
-
- process.exit(16);
- } else {
- settings.botConf.reset();
- settings.botConf
- .use('file', {
- file: botFile,
- format: nconf.formats.ini,
- })
- .load(() => {
- // Mesh in bot overrides from main config
- const overrides = settings.gConf.get('botOverride');
- if (overrides) {
- if (overrides[botType]) {
- for (const [key, value] of Object.entries(overrides[botType])) {
- settings.botConf.set(key, value);
- }
- }
- }
+ // Output if debug mode is on
+ if (gConf.get('debug')) {
+ console.info('== CNCServer Debug mode is ON ==');
+ }
+ });
+}
- // Handy bot constant for easy number from string conversion
- settings.bot = {
- workArea: {
- left: Number(settings.botConf.get('workArea:left')),
- top: Number(settings.botConf.get('workArea:top')),
- right: Number(settings.botConf.get('maxArea:width')),
- bottom: Number(settings.botConf.get('maxArea:height')),
- },
- maxArea: {
- width: Number(settings.botConf.get('maxArea:width')),
- height: Number(settings.botConf.get('maxArea:height')),
- },
- maxAreaMM: {
- width: Number(settings.botConf.get('maxAreaMM:width')),
- height: Number(settings.botConf.get('maxAreaMM:height')),
- },
- park: {
- x: Number(settings.botConf.get('park:x')),
- y: Number(settings.botConf.get('park:y')),
- },
- controller: settings.botConf.get('controller'),
- commands: settings.botConf.get('controller').commands,
- };
+/**
+ * Load bot specific config file
+ *
+ * @param {function} cb
+ * Callback triggered when loading is complete
+ * @param {string} botType
+ * Optional, the machine name for the bot type to load. Defaults to the
+ * globally configured bot type.
+ */
+export function loadBotConfig(cb, botType = gConf.get('botType')) {
+ const botFile = path.resolve(__basedir, '..', 'machine_types', `${botType}.ini`);
+
+ if (!fs.existsSync(botFile)) {
+ console.error(
+ `Bot configuration file "${botFile}" doesn't exist. Error #16`
+ );
- // Check if a point is within the work area.
- settings.bot.inWorkArea = ({ x, y }) => {
- const area = settings.bot.workArea;
- if (x > area.right || x < area.left) {
- return false;
- }
- if (y > area.bottom || y < area.top) {
- return false;
+ process.exit(16);
+ } else {
+ botConf.reset();
+ botConf
+ .use('file', {
+ file: botFile,
+ format: nconf.formats.ini,
+ })
+ .load(() => {
+ // Mesh in bot overrides from main config
+ const overrides = gConf.get('botOverride');
+ if (overrides) {
+ if (overrides[botType]) {
+ for (const [key, value] of Object.entries(overrides[botType])) {
+ botConf.set(key, value);
}
- return true;
- };
-
- // Store assumed constants.
- const { bot } = settings;
- bot.workArea.width = bot.maxArea.width - bot.workArea.left;
- bot.workArea.height = bot.maxArea.height - bot.workArea.top;
+ }
+ }
- bot.workArea.relCenter = {
- x: bot.workArea.width / 2,
- y: bot.workArea.height / 2,
+ // Handy bot constant for easy number from string conversion
+ bot.workArea = {
+ left: Number(botConf.get('workArea:left')),
+ top: Number(botConf.get('workArea:top')),
+ right: Number(botConf.get('maxArea:width')),
+ bottom: Number(botConf.get('maxArea:height')),
+ };
+ bot.maxArea = {
+ width: Number(botConf.get('maxArea:width')),
+ height: Number(botConf.get('maxArea:height')),
+ };
+ bot.maxAreaMM = {
+ width: Number(botConf.get('maxAreaMM:width')),
+ height: Number(botConf.get('maxAreaMM:height')),
+ };
+ bot.park = {
+ x: Number(botConf.get('park:x')),
+ y: Number(botConf.get('park:y')),
+ };
+ bot.controller = botConf.get('controller');
+ bot.commands = botConf.get('controller').commands;
+
+ // Check if a point is within the work area.
+ bot.inWorkArea = ({ x, y }) => {
+ const area = bot.workArea;
+ if (x > area.right || x < area.left) {
+ return false;
+ }
+ if (y > area.bottom || y < area.top) {
+ return false;
+ }
+ return true;
+ };
+
+ // Store assumed constants.
+ bot.workArea.width = bot.maxArea.width - bot.workArea.left;
+ bot.workArea.height = bot.maxArea.height - bot.workArea.top;
+
+ bot.workArea.relCenter = {
+ x: bot.workArea.width / 2,
+ y: bot.workArea.height / 2,
+ };
+
+ bot.workArea.absCenter = {
+ x: bot.workArea.relCenter.x + bot.workArea.left,
+ y: bot.workArea.relCenter.y + bot.workArea.top,
+ };
+
+ // If supplied, add conversions for abs distance.
+ if (bot.maxAreaMM.width) {
+ bot.stepsPerMM = {
+ x: bot.maxArea.width / bot.maxAreaMM.width,
+ y: bot.maxArea.height / bot.maxAreaMM.height,
};
- bot.workArea.absCenter = {
- x: bot.workArea.relCenter.x + bot.workArea.left,
- y: bot.workArea.relCenter.y + bot.workArea.top,
+ bot.workAreaMM = {
+ left: bot.workArea.left / bot.stepsPerMM.x,
+ top: bot.workArea.top / bot.stepsPerMM.y,
+ right: bot.workArea.right / bot.stepsPerMM.x,
+ bottom: bot.workArea.bottom / bot.stepsPerMM.y,
};
+ // bot.stepsPerMM
+ } else {
+ bot.maxAreaMM = false;
+ }
- // If supplied, add conversions for abs distance.
- if (bot.maxAreaMM.width) {
- bot.stepsPerMM = {
- x: bot.maxArea.width / bot.maxAreaMM.width,
- y: bot.maxArea.height / bot.maxAreaMM.height,
- };
-
- bot.workAreaMM = {
- left: bot.workArea.left / bot.stepsPerMM.x,
- top: bot.workArea.top / bot.stepsPerMM.y,
- right: bot.workArea.right / bot.stepsPerMM.x,
- bottom: bot.workArea.bottom / bot.stepsPerMM.y,
- };
- // bot.stepsPerMM
- } else {
- settings.bot.maxAreaMM = false;
- }
-
- // Set initial pen position at park position
- // TODO: Add set park position helper in control.
- const park = cncserver.utils.centToSteps(bot.park, true);
- cncserver.pen.forceState({
- x: park.x,
- y: park.y,
- });
-
- // Set global override for swapMotors if set by bot config
- const swapMotors = settings.botConf.get('controller:swapMotors');
- if (typeof swapMotors !== 'undefined') {
- settings.gConf.set('swapMotors', swapMotors);
- }
-
- // Trigger any bot/board specific setup.
- cncserver.binder.trigger('controller.setup', settings.botConf.get('controller'));
+ // Set global override for swapMotors if set by bot config
+ const swapMotors = botConf.get('controller:swapMotors');
+ if (typeof swapMotors !== 'undefined') {
+ gConf.set('swapMotors', swapMotors);
+ }
- console.log(
- `Successfully loaded config for ${settings.botConf.get(
- 'name'
- )}! Initializing...`
- );
+ // Trigger any bot/board specific setup.
+ trigger('controller.setup', botConf.get('controller'), true);
- // Trigger the callback once we're done
- if (cb) cb();
- });
- }
- };
-
- /**
- * Get the list of supported bots and their full ini config arrays.
- *
- * @return {object}
- * A keyed array/object of all supported bot configurations and data.
- */
- settings.getSupportedBots = () => {
- const list = fs.readdirSync(path.resolve(__dirname, '..', 'machine_types'));
- const out = {};
- for (const i of list) {
- const file = path.resolve(__dirname, '..', 'machine_types', list[i]);
- const data = ini.parse(fs.readFileSync(file, 'utf-8'), 'utf-8');
- const type = list[i].split('.')[0];
- out[type] = {
- name: data.name,
- data,
- };
- }
- return out;
- };
+ console.log(
+ `Successfully loaded config for ${botConf.get(
+ 'name'
+ )}! Initializing...`
+ );
- // Exports.
- settings.exports = {
- getSupportedBots: settings.getSupportedBots,
- loadGlobalConfig: settings.loadGlobalConfig,
- loadBotConfig: settings.loadBotConfig,
- };
+ // Trigger the callback once we're done
+ if (cb) cb();
+ });
+ }
+}
- return settings;
-};
+/**
+ * Get the list of supported bots and their full ini config arrays.
+ *
+ * @return {object}
+ * A keyed array/object of all supported bot configurations and data.
+ */
+export function getSupportedBots() {
+ const list = fs.readdirSync(path.resolve(__basedir, '..', 'machine_types'));
+ const out = {};
+ for (const i of list) {
+ const file = path.resolve(__basedir, '..', 'machine_types', list[i]);
+ const data = ini.parse(fs.readFileSync(file, 'utf-8'), 'utf-8');
+ const type = list[i].split('.')[0];
+ out[type] = {
+ name: data.name,
+ data,
+ };
+ }
+ return out;
+}
diff --git a/src/components/core/utils/cncserver.utils.js b/src/components/core/utils/cncserver.utils.js
index d7007edf..e54c33b7 100644
--- a/src/components/core/utils/cncserver.utils.js
+++ b/src/components/core/utils/cncserver.utils.js
@@ -1,490 +1,681 @@
/**
* @file Abstraction module for generic util helper functions for CNC Server!
*/
-const crypto = require('crypto'); // Crypto library for hashing.
-const extend = require('util')._extend; // Util for cloning objects
-const merge = require('merge-deep');
+import crypto from 'crypto'; // Crypto library for hashing.
+import glob from 'glob';
// File Utils.
-const { homedir } = require('os');
-const fs = require('fs');
-const path = require('path');
-
-const utils = { extend }; // Final Object to be exported.
-
-module.exports = (cncserver) => {
- /**
- * Sanity check a given coordinate within the absolute area.
- * @param {object} point
- * The point to be checked and operated on by reference.
- * // TODO: It's an antipattern to operate by ref, refactor.
- * @return {null}
- */
- utils.sanityCheckAbsoluteCoord = ({ x, y }) => {
- const { maxArea } = cncserver.settings.bot;
- return {
- x: Math.max(0, x > maxArea.width ? maxArea.width : x),
- y: Math.max(0, y > maxArea.height ? maxArea.height : y),
- };
- };
+import { homedir } from 'os';
+import fs from 'fs';
+import path from 'path';
- /**
- * Create a 16char hash using passed data (and a salt).
- *
- * @param {mixed} data
- * Data to be hashed, either an object or array.
- * @param {string} salt
- * Type of salt to adjust the hash with. Pass null to disable, otherwise:
- * "increment" (default): will increment each has salt by 1, providing
- * consecutive data similarty hash safety.
- * "date": Inserts the ISO date as a salt.
- *
- * @return {string}
- * 16 char hash of data and current time in ms.
- */
- let hashSequence = 0;
- utils.getHash = (data, saltMethod = 'increment') => {
- const md5sum = crypto.createHash('md5');
- let salt = '';
-
- switch (saltMethod) {
- case 'increment':
- salt = hashSequence++;
- break;
-
- case 'date':
- salt = new Date().toISOString();
- break;
-
- default:
- break;
- }
+// Settings.
+import { gConf, bot, botConf } from 'cs/settings';
- md5sum.update(`${JSON.stringify(data)}${salt}`);
- return md5sum.digest('hex').substr(0, 16);
- };
+// Directly exported imports.
+export { _extend as extend } from 'util'; // Util for cloning objects
+export { default as merge } from 'merge-deep';
- /**
- * Get a named user content directory.
- *
- * @param {string} name
- * The arbitrary name of the dir.
- *
- * @return {string}
- * The full path of the writable path.
- */
- utils.getUserDir = (name) => {
- // Ensure we have the base user folder.
- const home = utils.getDir(path.resolve(homedir(), 'cncserver'));
- const botHome = utils.getDir(path.resolve(home, cncserver.settings.gConf.get('botType')));
+// MM to Inches const.
+export const MM_TO_INCHES = 25.4;
- // Home base dir? or bot specific?
- if (['projects'].includes(name)) {
- return utils.getDir(path.resolve(botHome, name));
+/**
+ * Apply byref all keys and values from left to right.
+ *
+ * @export
+ * @param {object} source
+ * Source single level object with key/values to set from.
+ * @param {object} dest
+ * Destination single level object to be modified.
+ * @param {bool} strict
+ * If true, only keys existing on the destination will be set.
+ *
+ * @returns {boolean}
+ * True if there was a difference, false if no change.
+ */
+export function applyObjectTo(source, dest, strict = false) {
+ let hasChanges = false;
+ Object.entries(source).forEach(([key, value]) => {
+ // eslint-disable-next-line no-param-reassign
+ if (strict) {
+ if (key in dest) {
+ if (dest[key] !== value) {
+ hasChanges = true;
+ // eslint-disable-next-line no-param-reassign
+ dest[key] = value;
+ }
+ }
+ } else if (dest[key] !== value) {
+ hasChanges = true;
+ // eslint-disable-next-line no-param-reassign
+ dest[key] = value;
}
- return utils.getDir(path.resolve(home, name));
- };
+ });
- utils.singleLineString = (strings, ...values) => {
- // Interweave the strings with the
- // substitution vars first.
- let output = '';
- for (let i = 0; i < values.length; i++) {
- output += strings[i] + values[i];
- }
- output += strings[values.length];
+ return hasChanges;
+}
- // Split on newlines.
- const lines = output.split(/(?:\r\n|\n|\r)/);
+// ES Module basedir replacement.
+export const __basedir = path.resolve('./src/');
- // Rip out the leading whitespace.
- return lines.map(line => line.replace(/^\s+/gm, '')).join(' ').trim();
- };
+/**
+ * Sanity check a given coordinate within the absolute area.
+ * @param {object} point
+ * The point in absolute steps to be checked and operated on by reference.
+ *
+ * @return {object}
+ * The final sanitized coordinate.
+ */
+export function sanityCheckAbsoluteCoord({ x, y }) {
+ const { maxArea } = bot;
- /**
- * Validate that a directory at the given path exists.
- *
- * @param {string} dir
- * The full path of the dir.
- *
- * @return {string}
- * The full path dir, or an error is thrown if there's permission issues.
- */
- utils.getDir = (dir) => {
- if (!fs.existsSync(dir)) fs.mkdirSync(dir);
- return dir;
+ return {
+ x: Math.round(Math.max(0, x > maxArea.width ? maxArea.width : x)),
+ y: Math.round(Math.max(0, y > maxArea.height ? maxArea.height : y)),
};
+}
- // Externalize Merge Deep:
- // @see https://github.com/jonschlinkert/merge-deep
- utils.merge = merge;
-
- /**
- * Calculate the duration for a pen movement from the distance.
- * Takes into account whether pen is up or down
- *
- * @param {float} distance
- * Distance in steps that we'll be moving
- * @param {int} min
- * Optional minimum value for output duration, defaults to 1.
- * @param {object} inPen
- * Incoming pen object to check (buffer tip or bot current).
- * @param {number} speedOverride
- * Optional speed override, overrides calculated speed percent.
- *
- * @returns {number}
- * Millisecond duration of how long the move should take
- */
- utils.getDurationFromDistance = (distance, min = 1, inPen, speedOverride = null) => {
- const minSpeed = parseFloat(cncserver.settings.botConf.get('speed:min'));
- const maxSpeed = parseFloat(cncserver.settings.botConf.get('speed:max'));
- const drawingSpeed = cncserver.settings.botConf.get('speed:drawing');
- const movingSpeed = cncserver.settings.botConf.get('speed:moving');
-
- // Use given speed over distance to calculate duration
- let speed = (utils.penDown(inPen)) ? drawingSpeed : movingSpeed;
- if (speedOverride != null) {
- speed = speedOverride;
- }
+/**
+ * Create a 16char hash using passed data (and a salt).
+ *
+ * @param {mixed} data
+ * Data to be hashed, either an object or array.
+ * @param {string} salt
+ * Type of salt to adjust the hash with. Pass null to disable, otherwise:
+ * "increment" (default): will increment each has salt by 1, providing
+ * consecutive data similarty hash safety.
+ * "date": Inserts the ISO date as a salt.
+ *
+ * @return {string}
+ * 16 char hash of data and current time in ms.
+ */
+let hashSequence = 0;
+export function getHash(data, saltMethod = 'increment') {
+ const md5sum = crypto.createHash('md5');
+ let salt = '';
- speed = parseFloat(speed) / 100;
+ switch (saltMethod) {
+ case 'increment':
+ salt = hashSequence++;
+ break;
- // Convert to steps from percentage
- speed = (speed * (maxSpeed - minSpeed)) + minSpeed;
+ case 'date':
+ salt = new Date().toISOString();
+ break;
- // Sanity check speed value
- speed = speed > maxSpeed ? maxSpeed : speed;
- speed = speed < minSpeed ? minSpeed : speed;
+ default:
+ break;
+ }
- // How many steps a second?
- return Math.max(Math.abs(Math.round(distance / speed * 1000)), min);
- };
+ md5sum.update(`${JSON.stringify(data)}${salt}`);
+ return md5sum.digest('hex').substr(0, 16);
+}
- /**
- * Given two points, find the difference and duration at current speed
- *
- * @param {{x: number, y: number}} src
- * Source position coordinate (in steps).
- * @param {{x: number, y: number}} dest
- * Destination position coordinate (in steps).
- * @param {number} speed
- * Speed override for this movement in percent.
- *
- * @returns {{d: number, x: number, y: number}}
- * Object containing the change amount in steps for x & y, along with the
- * duration in milliseconds.
- */
- utils.getPosChangeData = (src, dest, speed = null) => {
- let change = {
- x: Math.round(dest.x - src.x),
- y: Math.round(dest.y - src.y),
- };
+/**
+ * Validate that a directory at the given path exists.
+ *
+ * @param {string} dir
+ * The full path of the dir.
+ *
+ * @return {string}
+ * The full path dir, or an error is thrown if there's permission issues.
+ */
+export function getDir(dir) {
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir);
+ return dir;
+}
- // Calculate distance
- const duration = utils.getDurationFromDistance(
- utils.getVectorLength(change),
- 1,
- src,
- speed
- );
-
- // Adjust change direction/inversion
- if (cncserver.settings.botConf.get('controller').position === 'relative') {
- // Invert X or Y to match stepper direction
- change.x = cncserver.settings.gConf.get('invertAxis:x') ? change.x * -1 : change.x;
- change.y = cncserver.settings.gConf.get('invertAxis:y') ? change.y * -1 : change.y;
- } else { // Absolute! Just use the "new" absolute X & Y locations
- change.x = cncserver.pen.state.x;
- change.y = cncserver.pen.state.y;
+/**
+ * Get a named user content directory.
+ *
+ * @param {string} name
+ * The arbitrary name of the dir.
+ *
+ * @return {string}
+ * The full path of the writable path.
+ */
+export function getUserDir(name) {
+ // Ensure we have the base user folder.
+ const home = getDir(path.resolve(homedir(), 'cncserver'));
+ const botType = gConf.get('botType');
+
+ // Only if we have a bot type, use its specific home dir.
+ // This happens when this function is used before settings are available.
+ if (botType) {
+ const botHome = getDir(path.resolve(home, botType));
+
+ // Home base dir? or bot specific?
+ if (['projects', 'colorsets', 'implements', 'toolsets'].includes(name)) {
+ return getDir(path.resolve(botHome, name));
}
+ }
+ return getDir(path.resolve(home, name));
+}
+
+/**
+ * Get list of files from a named user content directory.
+ *
+ * @param {string} name
+ * The arbitrary name of the dir.
+ * @param {string} pattern
+ * The file pattern to filter to, defaults to all files.
+ * @param {function} mapFunc
+ * The function to map over the array of files.
+ *
+ * @return {array}
+ * List of file paths matching the pattern, after running through the map.
+ */
+export function getUserDirFiles(name, pattern = '*.*', mapFunc = x => x) {
+ let files = [];
+ try {
+ const dir = getUserDir(name);
+ files = glob.sync(path.join(dir, pattern));
+ } catch (error) {
+ console.error(error);
+ }
+
+ return files.map(mapFunc);
+}
- // Swap motor positions
- if (cncserver.settings.gConf.get('swapMotors')) {
- change = {
- x: change.y,
- y: change.x,
- };
+/**
+ * Get JSON data from a file in a given folder. Null if problem.
+ *
+ * @param {string} file
+ * The full file path of the JSON file.
+ *
+ * @return {object}
+ * Data in file, null if not found or bad JSON.
+ */
+export function getJSONFile(file) {
+ let data = null;
+
+ if (fs.existsSync(file)) {
+ try {
+ data = JSON.parse(fs.readFileSync(file));
+
+ // Clean out metadata header here.
+ // TODO: Validate header somehow?
+ delete data._meta;
+ } catch (error) {
+ console.error(`Problem loading JSON file: '${file}'`, error);
}
+ }
- return { d: duration, x: change.x, y: change.y };
- };
+ return data;
+}
- /**
- * Given two height positions, find the difference and pro-rate duration.
- *
- * @param {integer} src
- * Source position.
- * @param {integer} dest
- * Destination position.
- *
- * @returns {{d: number, a: number}}
- * Object containing the change amount in steps for x & y, along with the
- * duration in milliseconds.
- */
- utils.getHeightChangeData = (src, dest) => {
- const sd = cncserver.settings.botConf.get('servo:minduration');
-
- // Get the amount of change from difference between actualPen and absolute
- // height position, pro-rating the duration depending on amount of change
- const range = parseInt(cncserver.settings.botConf.get('servo:max'), 10)
- - parseInt(cncserver.settings.botConf.get('servo:min'), 10);
-
- const duration = Math.max(1, Math.round((Math.abs(dest - src) / range) * sd));
-
- return { d: duration, a: dest - src };
- };
+/**
+ * Get an object of all JSON data in a given folder.
+ *
+ * @param {string} dir
+ * The full path of the directory to search in.
+ *
+ * @return {object}
+ * Object keyed on "name" of all items in dir. Empty object if invalid or no files.
+ */
+export function getJSONList(dir) {
+ const out = {};
+
+ if (fs.existsSync(dir)) {
+ const files = glob.sync(path.join(dir, '*.json'));
+ files.forEach(jsonPath => {
+ const data = getJSONFile(jsonPath);
+ if (data && data.name) {
+ out[data.name] = data;
+ }
+ });
+ }
- /**
- * Helper abstraction for checking if the tip of buffer pen is "down" or not.
- *
- * @param {object} inPen
- * The pen object to check for down status, defaults to buffer tip.
- * @returns {Boolean}
- * False if pen is considered up, true if pen is considered down.
- */
- utils.penDown = (inPen) => {
- // TODO: Refactor to ensure no modification by reference/intent.
- if (!inPen || !inPen.state) inPen = cncserver.pen.state;
-
- if (inPen.state === 'up' || inPen.state < 0.5) {
- return false;
- }
+ return out;
+}
- return true;
- };
+/**
+ * Get an object of all JSON data in a given custom folder.
+ *
+ * @param {string} name
+ * The name of the preset type.
+ *
+ * @return {object}
+ * Object keyed on "name" of all items.
+ */
+export function getCustomPresets(name) {
+ return getJSONList(getUserDir(name));
+}
- /**
- * Convert an incoming pen object absolute step coordinate values.
- *
- * @param {{x: number, y: number, abs: [in|mm]}} pen
- * Pen/Coordinate measured in percentage of total draw area, or absolute
- * distance to be converted to steps.
- *
- * @returns {{x: number, y: number}}
- * Converted coordinate in absolute steps.
- */
- utils.inPenToSteps = (inPen) => {
- if (inPen.abs === 'in' || inPen.abs === 'mm') {
- return utils.absToSteps({ x: inPen.x, y: inPen.y }, inPen.abs);
- }
+/**
+ * Get an object of all JSON data in a given internal preset folder.
+ *
+ * @param {string} name
+ * The name of the preset type.
+ *
+ * @return {object}
+ * Object keyed on "name" of all items.
+ */
+export function getInternalPresets(name) {
+ return getJSONList(path.join(__basedir, 'presets', name));
+}
- return utils.centToSteps({ x: inPen.x, y: inPen.y });
+/**
+ * Get an object of all JSON data in a given preset/custom folders.
+ *
+ * @param {string} name
+ * The name of the preset type.
+ *
+ * @return {object}
+ * Object keyed on "name" of all items.
+ */
+export function getPresets(name) {
+ return {
+ ...getInternalPresets(name),
+ ...getCustomPresets(name),
};
+}
- /**
- * Convert an absolute point object to absolute step coordinate values.
- *
- * @param {{x: number, y: number, abs: [in|mm]}} point
- * Coordinate measured in percentage of total draw area, or absolute
- * distance to be converted to steps.
- * @param {string} scale
- * Either 'in' for inches, or 'mm' for millimeters.
- * @param {boolean} inMaxArea
- * Pass "true" if percent vals should be considered within the maximum area
- * otherwise steps will be calculated as part of the global work area.
- *
- * @returns {{x: number, y: number}}
- * Converted coordinate in absolute steps.
- */
- utils.absToSteps = (point, scale, inMaxArea) => {
- const { settings: { bot } } = cncserver;
-
- // TODO: Don't operate by reference (intionally or otherwise), refactor.
- // ALSO move '25.4' value to config.
- // Convert Inches to MM.
- if (scale === 'in') {
- point = { x: point.x * 25.4, y: point.y * 25.4 };
+/**
+ * Curried promise to validate an existing key in set of type presets.
+ *
+ * @param {string} key
+ * Key off of source object to check for valid machine name.
+ * @param {boolean} allowInherit
+ * When true, allows '[inherit]' machine name.
+ * @param {string} type
+ * The type of preset, defaults to pluralized keyname "[key]s".
+ *
+ * @return {Function > Promise}
+ * Curried function, to promise that resolves if the value is valid, rejects
+ * with error and allowed value list if invalid.
+ */
+export function isValidPreset(key, allowInherit = false, type = `${key}s`) {
+ return source => new Promise((resolve, reject) => {
+ const keys = Object.keys(getPresets(type));
+ if (allowInherit) keys.unshift('[inherit]');
+ if (!keys.includes(source[key])) {
+ const err = new Error(`Invalid ${key}, must be one of allowed values`);
+ err.allowedValues = keys;
+ reject(err);
}
+ resolve(source);
+ });
+}
- // Return absolute calculation.
- return {
- x: (!inMaxArea ? bot.workArea.left : 0) + (point.x * bot.stepsPerMM.x),
- y: (!inMaxArea ? bot.workArea.top : 0) + (point.y * bot.stepsPerMM.y),
- };
- };
+/**
+ * Get the direct data of a preset/custom by type and machine name.
+ *
+ * @param {string} type
+ * The type of preset (folder).
+ * @param {string} name
+ * The machine name of the preset (filename).
+ * @param {boolean} customOnly
+ * If true, will not failover to read-only presets.
+ *
+ * @return {object}
+ * Raw data from the JSON file, null if there's a problem.
+ */
+export function getPreset(type, name, customOnly) {
+ const presetPath = path.join(__basedir, 'presets', type, `${name}.json`);
+ const customPath = path.join(getUserDir(type), `${name}.json`);
+
+ if (customOnly) {
+ return getJSONFile(customPath);
+ }
+
+ if (fs.existsSync(customPath)) {
+ return getJSONFile(customPath);
+ }
+ return getJSONFile(presetPath);
+}
- /**
- * Convert an absolute step coordinate to absolute point object.
- *
- * @param {{x: number, y: number}} point
- * Coordinate measured in steps to be converted to absolute in scale.
- * @param {string} scale
- * Either 'in' for inches, or 'mm' for millimeters.
- *
- * @returns {{x: number, y: number}}
- * Converted coordinate in absolute steps.
- */
- utils.stepsToAbs = (point, scale) => {
- const { settings: { bot } } = cncserver;
-
- // Setup output, less workarea boundaries, divided by mm per step.
- let out = {
- x: (point.x - bot.workArea.left) / bot.stepsPerMM.x,
- y: (point.y - bot.workArea.top) / bot.stepsPerMM.y,
- };
+/**
+ * Save the data of a preset/custom by type and data.
+ *
+ * @param {string} type
+ * The type of preset (folder).
+ * @param {string} data
+ * The data to save, with a required key "name" used as machine name.
+ *
+ * @return {object}
+ * Raw data from the JSON file, null if there's a problem.
+ */
+export function savePreset(type, data) {
+ const customPath = path.join(getUserDir(type), `${data.name}.json`);
+ // TODO: Pull version dynamically.
+ const _meta = { cncserverVersion: '3.0.0-beta1', fileType: type };
+ fs.writeFileSync(customPath, JSON.stringify({ _meta, ...data }, null, 2));
+}
- if (scale === 'in') {
- out = { x: point.x / 25.4, y: point.y / 25.4 };
- }
+/**
+ * Delete custom by type and name.
+ *
+ * @param {string} type
+ * The type of preset (folder).
+ * @param {string} name
+ * The machine name of the preset.
+ */
+export function deletePreset(type, name) {
+ const customPath = path.join(getUserDir(type), `${name}.json`);
+ const trashPath = path.resolve(getUserDir('trash'), `${type}-${name}.json`);
+ // Try to rename to trash. If there's one existing, delete.
+ try {
+ if (fs.existsSync(trashPath)) fs.unlinkSync(trashPath);
+ fs.renameSync(customPath, trashPath);
+ } catch (error) {
+ // Basically ignore errors for now.
+ console.error(error);
+ }
+}
+
+// String literal translation function to keep code line lengths down.
+export function singleLineString(strings, ...values) {
+ // Interweave the strings with the
+ // substitution vars first.
+ let output = '';
+ for (let i = 0; i < values.length; i++) {
+ output += strings[i] + values[i];
+ }
+ output += strings[values.length];
+
+ // Split on newlines.
+ const lines = output.split(/(?:\r\n|\n|\r)/);
+
+ // Rip out the leading whitespace.
+ return lines.map(line => line.replace(/^\s+/gm, '')).join(' ').trim();
+}
- // Return absolute calculation.
- return out;
+/**
+ * Given two height positions, find the difference and pro-rate duration.
+ *
+ * @param {integer} src
+ * Source position.
+ * @param {integer} dest
+ * Destination position.
+ *
+ * @returns {{d: number, a: number}}
+ * Object containing the change amount in steps for x & y, along with the
+ * duration in milliseconds.
+ */
+export function getHeightChangeData(src, dest) {
+ const sd = botConf.get('servo:minduration');
+
+ // Get the amount of change from difference between actualPen and absolute
+ // height position, pro-rating the duration depending on amount of change
+ const range = parseInt(botConf.get('servo:max'), 10)
+ - parseInt(botConf.get('servo:min'), 10);
+
+ const duration = Math.max(1, Math.round((Math.abs(dest - src) / range) * sd));
+
+ return { d: duration, a: dest - src };
+}
+
+/**
+ * Convert an absolute point object to absolute step coordinate values.
+ *
+ * @param {{x: number, y: number}} point
+ * Coordinate measured in percentage of total draw area, or absolute
+ * distance to be converted to steps.
+ * @param {string} scale
+ * Either 'in' for inches, or 'mm' for millimeters.
+ * @param {boolean} inMaxArea
+ * Pass "true" if percent vals should be considered within the maximum area
+ * otherwise steps will be calculated as part of the global work area.
+ *
+ * @returns {{x: number, y: number}}
+ * Converted coordinate in absolute steps.
+ */
+export function absToSteps({ x, y }, scale, inMaxArea) {
+ // Convert Inches to MM.
+ let point = { x, y };
+ if (scale === 'in') {
+ point = { x: x * MM_TO_INCHES, y: y * MM_TO_INCHES };
+ }
+
+ // Return absolute calculation.
+ // TODO: validate this works for inches.
+ return {
+ x: (!inMaxArea ? bot.workArea.left : 0) + (point.x * bot.stepsPerMM.x),
+ y: (!inMaxArea ? bot.workArea.top : 0) + (point.y * bot.stepsPerMM.y),
};
+}
- /**
- * Convert percent of total area coordinates into absolute step coordinates.
- *
- * @param {{x: number, y: number}} point
- * Coordinate (measured in steps) to be converted.
- * @param {boolean} inMaxArea
- * Pass "true" if percent vals should be considered within the maximum area
- * otherwise steps will be calculated as part of the global work area.
- *
- * @returns {{x: number, y: number}}
- * Converted coordinate in steps.
- */
- utils.centToSteps = (point, inMaxArea) => {
- const { bot } = cncserver.settings;
- if (!inMaxArea) { // Calculate based on workArea
- return {
- x: bot.workArea.left + ((point.x / 100) * bot.workArea.width),
- y: bot.workArea.top + ((point.y / 100) * bot.workArea.height),
- };
- }
- // Calculate based on ALL area
+/**
+ * Convert an absolute step coordinate to absolute point object.
+ *
+ * @param {{x: number, y: number}} point
+ * Coordinate measured in steps to be converted to absolute in scale.
+ * @param {string} scale
+ * Either 'in' for inches, or 'mm' for millimeters.
+ *
+ * @returns {{x: number, y: number}}
+ * Converted coordinate in absolute steps.
+ */
+export function stepsToAbs(point, scale) {
+ // Setup output, less workarea boundaries, divided by mm per step.
+ let out = {
+ x: (point.x - bot.workArea.left) / bot.stepsPerMM.x,
+ y: (point.y - bot.workArea.top) / bot.stepsPerMM.y,
+ };
+
+ if (scale === 'in') {
+ out = { x: point.x / 25.4, y: point.y / 25.4 };
+ }
+
+ // Return absolute calculation.
+ return out;
+}
+
+/**
+ * Convert percent of total area coordinates into absolute step coordinates.
+ *
+ * @param {{x: number, y: number}} point
+ * Coordinate (measured in steps) to be converted.
+ * @param {boolean} inMaxArea
+ * Pass "true" if percent vals should be considered within the maximum area
+ * otherwise steps will be calculated as part of the global work area.
+ *
+ * @returns {{x: number, y: number}}
+ * Converted coordinate in steps.
+ */
+export function centToSteps(point, inMaxArea) {
+ if (!inMaxArea) { // Calculate based on workArea
return {
- x: (point.x / 100) * bot.maxArea.width,
- y: (point.y / 100) * bot.maxArea.height,
+ x: bot.workArea.left + ((point.x / 100) * bot.workArea.width),
+ y: bot.workArea.top + ((point.y / 100) * bot.workArea.height),
};
+ }
+ // Calculate based on ALL area
+ return {
+ x: (point.x / 100) * bot.maxArea.width,
+ y: (point.y / 100) * bot.maxArea.height,
};
+}
- /**
- * Convert any string into a machine name.
- *
- * @param {string} string
- * Object representing coordinate away from (0,0)
- * @returns {number}
- * Length (in steps) of the given vector point
- */
- utils.getMachineName = (string = '', limit = 32) => string
- .replace(/[^A-Za-z0-9 ]/g, '') // Remove unwanted characters.
+/**
+ * Convert an incoming pen object absolute step coordinate values.
+ *
+ * @param {{x: number, y: number, abs: [in|mm]}} pen
+ * Pen/Coordinate measured in percentage of total draw area, or absolute
+ * distance to be converted to steps.
+ *
+ * @returns {{x: number, y: number}}
+ * Converted coordinate in absolute steps.
+ */
+export function inPenToSteps(inPen) {
+ if (inPen.abs === 'in' || inPen.abs === 'mm') {
+ return absToSteps({ x: inPen.x, y: inPen.y }, inPen.abs);
+ }
+
+ return centToSteps({ x: inPen.x, y: inPen.y });
+}
+
+/**
+ * Convert any string into a machine name.
+ *
+ * @param {string} string
+ * Object representing coordinate away from (0,0)
+ * @returns {number}
+ * Length (in steps) of the given vector point
+ */
+export function getMachineName(string = '', limit = 32) {
+ return string
+ .replace(/[^A-Za-z0-9- ]/g, '') // Remove unwanted characters.
.replace(/\s{2,}/g, ' ') // Replace multi spaces with a single space
.replace(/\s/g, '-') // Replace space with a '-' symbol
.toLowerCase() // Lowercase only.
.substr(0, limit); // Limit
+}
- /**
- * Get the distance/length of the given vector coordinate
- *
- * @param {{x: number, y: number}} vector
- * Object representing coordinate away from (0,0)
- * @returns {number}
- * Length (in steps) of the given vector point
- */
- utils.getVectorLength = vector => Math.sqrt((vector.x ** 2) + (vector.y ** 2));
-
- /**
- * Perform conversion from named/0-1 number state value to given pen height
- * suitable for outputting to a Z axis control statement.
- *
- * @param {string/integer} state
- *
- * @returns {object}
- * Object containing normalized state, and numeric height value. As:
- * {state: [integer|string], height: [float]}
- */
- utils.stateToHeight = (state) => {
- // Whether to use the full min/max range (used for named presets only)
- let fullRange = false;
- let min = parseInt(cncserver.settings.botConf.get('servo:min'), 10);
- let max = parseInt(cncserver.settings.botConf.get('servo:max'), 10);
- let range = max - min;
- let normalizedState = state; // Normalize/sanitize the incoming state
-
- const presets = cncserver.settings.botConf.get('servo:presets');
- let height = 0; // Placeholder for height output
-
- // Validate Height, and conform to a bottom to top based percentage 0 to 100
- if (Number.isNaN(parseInt(state, 10))) { // Textual position!
- if (typeof presets[state] !== 'undefined') {
- height = parseFloat(presets[state]);
- } else { // Textual expression not found, default to UP
- height = presets.up;
- normalizedState = 'up';
- }
-
- fullRange = true;
- } else { // Numerical position (0 to 1), moves between up (0) and draw (1)
- height = Math.abs(parseFloat(state));
- height = height > 1 ? 1 : height; // Limit to 1
- normalizedState = height;
-
- // Reverse value and lock to 0 to 100 percentage with 1 decimal place
- height = parseInt((1 - height) * 1000, 10) / 10;
- }
-
- // Lower the range when using 0 to 1 values to between up and draw
- if (!fullRange) {
- min = ((presets.draw / 100) * range) + min;
- max = ((presets.up / 100) * range);
- max += parseInt(cncserver.settings.botConf.get('servo:min'), 10);
+/**
+ * Get the distance/length of the given vector coordinate
+ *
+ * @param {{x: number, y: number}} vector
+ * Object representing coordinate away from (0,0)
+ * @returns {number}
+ * Length (in steps) of the given vector point
+ */
+export function getVectorLength(vector) {
+ return Math.sqrt((vector.x ** 2) + (vector.y ** 2));
+}
- range = max - min;
+/**
+ * Perform conversion from named/0-1 number state value to given pen height
+ * suitable for outputting to a Z axis control statement.
+ *
+ * @param {string|integer} state
+ *
+ * @returns {object}
+ * Object containing normalized state, and numeric height value. As:
+ * {state: [integer|string], height: [float]}
+ */
+export function stateToHeight(state) {
+ // Whether to use the full min/max range (used for named presets only)
+ let fullRange = false;
+ let min = parseInt(botConf.get('servo:min'), 10);
+ let max = parseInt(botConf.get('servo:max'), 10);
+ let range = max - min;
+ let normalizedState = state; // Normalize/sanitize the incoming state
+
+ const presets = botConf.get('servo:presets');
+ let height = 0; // Placeholder for height output
+
+ // Validate Height, and conform to a bottom to top based percentage 0 to 100
+ if (Number.isNaN(parseInt(state, 10))) { // Textual position!
+ if (typeof presets[state] !== 'undefined') {
+ height = parseFloat(presets[state]);
+ } else { // Textual expression not found, default to UP
+ height = presets.up;
+ normalizedState = 'up';
}
- // Sanity check incoming height value to 0 to 100
- height = height > 100 ? 100 : height;
- height = height < 0 ? 0 : height;
-
- // Calculate the final servo value from percentage
- height = Math.round(((height / 100) * range) + min);
- return { height, state: normalizedState };
+ fullRange = true;
+ } else { // Numerical position (0 to 1), moves between up (0) and draw (1)
+ height = Math.abs(parseFloat(state));
+ height = height > 1 ? 1 : height; // Limit to 1
+ normalizedState = height;
+
+ // Reverse value and lock to 0 to 100 percentage with 1 decimal place
+ height = parseInt((1 - height) * 1000, 10) / 10;
+ }
+
+ // Lower the range when using 0 to 1 values to between up and draw
+ if (!fullRange) {
+ min = ((presets.draw / 100) * range) + min;
+ max = ((presets.up / 100) * range);
+ max += parseInt(botConf.get('servo:min'), 10);
+
+ range = max - min;
+ }
+
+ // Sanity check incoming height value to 0 to 100
+ height = height > 100 ? 100 : height;
+ height = height < 0 ? 0 : height;
+
+ // Calculate the final servo value from percentage
+ height = Math.round(((height / 100) * range) + min);
+ return { height, state: normalizedState };
+}
+
+export function round(number, precision) {
+ const p = 10 ** precision;
+ return Math.round(number * p) / p;
+}
+
+export function roundPoint(point, precision = 2) {
+ if (Array.isArray(point)) {
+ return [
+ round(point[0], precision),
+ round(point[1], precision),
+ ];
+ }
+
+ return {
+ x: round(point.x, precision),
+ y: round(point.y, precision),
};
+}
- utils.round = (number, precision) => {
- const p = 10 ** precision;
- return Math.round(number * p) / p;
- };
-
- utils.roundPoint = (point, precision = 2) => {
- if (Array.isArray(point)) {
- return [
- utils.round(point[0], precision),
- utils.round(point[1], precision),
- ];
- }
+/**
+ * Wrap SVG string content with a header and footer.
+ *
+ * @export
+ * @param {string} content
+ * SVG inner content to be wrapped.
+ * @param {{width: number, height: number}} size
+ * Object containing width and height in SVG size.
+ *
+ * @returns {string}
+ * Final complete SVG text content.
+ */
+export function wrapSVG(content, size) {
+ return singleLineString`\n
+ `;
+}
- return {
- x: utils.round(point.x, precision),
- y: utils.round(point.y, precision),
- };
- };
+/**
+ * Map a value in a given range to a new range.
+ *
+ * @param {number} x
+ * The input number to be mapped.
+ * @param {number} inMin
+ * Expected minimum of the input number.
+ * @param {number} inMax
+ * Expected maximum of the input number.
+ * @param {number} outMin
+ * Expected minimum of the output map.
+ * @param {number} outMax
+ * Expected maximum of the output map.
+ *
+ * @return {number}
+ * The output number after mapping.
+ */
+export function map(x, inMin, inMax, outMin, outMax) {
+ return ((x - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;
+}
- // Wrap SVG string content with a header and footer.
- utils.wrapSVG = (content, size) => {
- const header = `
- `;
- };
+/**
+ * Perform conversion from array structure to a map based on ID.
+ *
+ * @param {array} data
+ *
+ * @returns {Map}
+ * Map of array data keyed by "id" in array data.
+ */
+export function arrayToIDMap(data) {
+ const m = new Map();
+ data.forEach(d => {
+ m.set(d.id, d);
+ });
+
+ return m;
+}
- /**
- * Map a value in a given range to a new range.
- *
- * @param {number} x
- * The input number to be mapped.
- * @param {number} inMin
- * Expected minimum of the input number.
- * @param {number} inMax
- * Expected maximum of the input number.
- * @param {number} outMin
- * Expected minimum of the output map.
- * @param {number} outMax
- * Expected maximum of the output map.
- *
- * @return {number}
- * The output number after mapping.
- */
- utils.map = (x, inMin, inMax, outMin, outMax) => (x - inMin) * (outMax - outMin) / (inMax - inMin) + outMin;
-
- return utils;
-};
+/**
+ * Reverse conversion from map flat array.
+ *
+ * @param {Map} data
+ *
+ * @returns {array}
+ * Flat array of data.
+ */
+export function mapToArray(data) {
+ return data ? Array.from(data.values()) : [];
+}
diff --git a/src/components/index.js b/src/components/index.js
deleted file mode 100644
index 639024d5..00000000
--- a/src/components/index.js
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
- * @file Index of all source components.
- */
-
-module.exports = {
- utils: './components/core/utils/cncserver.utils',
- binder: './components/core/utils/cncserver.binder',
- settings: './components/core/utils/cncserver.settings',
- i18n: './components/core/utils/cncserver.i18n',
- server: './components/core/comms/cncserver.server',
- rest: './components/core/comms/cncserver.rest',
- api: './components/core/comms/cncserver.api',
- ipc: './components/core/comms/cncserver.ipc',
- serial: './components/core/comms/cncserver.serial',
- sockets: './components/core/comms/cncserver.sockets',
- pen: './components/core/control/cncserver.pen',
- actualPen: './components/core/control/cncserver.actualpen',
- control: './components/core/control/cncserver.control',
- buffer: './components/core/utils/cncserver.buffer',
- run: './components/core/utils/cncserver.run',
- scratch: './components/third_party/scratch/cncserver.scratch',
- bots: './components/machine_support/',
- drawing: './components/core/drawing',
- projects: './components/core/control/cncserver.projects',
- content: './components/core/control/cncserver.content',
- schemas: './components/core/schemas/cncserver.schemas',
-};
diff --git a/src/components/machine_support/cncserver.bots.base.js b/src/components/machine_support/cncserver.bots.base.js
index 70b7b830..f4d1ee45 100644
--- a/src/components/machine_support/cncserver.bots.base.js
+++ b/src/components/machine_support/cncserver.bots.base.js
@@ -1,24 +1,31 @@
/**
* @file Abstraction module for base bot specific stuff.
*/
-const base = { id: 'base' }; // Exposed export.
+import { bindTo } from 'cs/binder';
+import run from 'cs/run';
+import { manualSwapTrigger } from 'cs/sockets';
-module.exports = (cncserver) => {
+/**
+ * Initialize bot specific code.
+ *
+ * @export
+ */
+export default function initBot() {
// Bind base wait toolchange support to the toolchange event.
- cncserver.binder.bindTo('tool.change', base.id, (tool) => {
+ bindTo('tool.change', 'base', tool => {
// A "wait" tool requires user feedback before it can continue.
if (typeof tool.wait !== 'undefined') {
// Queue a callback to pause continued execution on tool.wait value
if (tool.wait) {
- cncserver.run('callback', () => {
- cncserver.sockets.manualSwapTrigger(tool.index);
+ run('callback', () => {
+ manualSwapTrigger(tool.index);
});
// Queue a special low-level pause in the runner.
- cncserver.run('special', 'pause');
+ run('special', 'pause');
}
}
});
- return base;
-};
+ return { id: 'bots.base' };
+}
diff --git a/src/components/machine_support/cncserver.bots.ebb.js b/src/components/machine_support/cncserver.bots.ebb.js
index d5fae298..a0584458 100644
--- a/src/components/machine_support/cncserver.bots.ebb.js
+++ b/src/components/machine_support/cncserver.bots.ebb.js
@@ -2,42 +2,51 @@
* @file Abstraction module for EiBotBoard specific support.
* @see http://evil-mad.github.io/EggBot/ebb.html
*/
-const semver = require('semver');
-
-const ebb = { id: 'ebb', version: {} }; // Exposed export.
+import semver from 'semver';
+import { bindTo } from 'cs/binder';
+import run from 'cs/run';
+import { setSetupCommands } from 'cs/serial';
+import { cmdstr } from 'cs/buffer';
+import { botConf } from 'cs/settings';
+import { getSerialValue } from 'cs/ipc';
+import { singleLineString as SLS } from 'cs/utils';
+
+const ebb = { id: 'bots.ebb', version: {} }; // Exposed export.
const minVersion = '>=2.2.7';
let controller = {}; // Placeholder for machine config export.
-module.exports = (cncserver) => {
+/**
+ * Initialize bot specific code.
+ *
+ * @export
+ */
+export default function initBot() {
// Bot support in use parent callback.
- ebb.checkInUse = (botConf) => {
- ebb.inUse = botConf.controller.name === 'EiBotBoard';
+ ebb.checkInUse = botConfig => {
+ ebb.inUse = botConfig.controller.name === 'EiBotBoard';
};
// Bind EBB support on controller setup -before- serial connection.
- cncserver.binder.bindTo('controller.setup', ebb.id, (controller) => {
- if (controller.name === 'EiBotBoard') {
- cncserver.serial.setSetupCommands([
+ bindTo('controller.setup', ebb.id, controllerConfig => {
+ if (controllerConfig.name === 'EiBotBoard') {
+ setSetupCommands([
// Set motor precision.
- cncserver.buffer.cmdstr(
- 'enablemotors',
- { p: cncserver.settings.botConf.get('speed:precision') }
- ),
+ cmdstr('enablemotors', { p: botConf.get('speed:precision') }),
]);
}
});
// Bind to serial connection.
- cncserver.binder.bindTo('serial.connected', ebb.id, () => {
+ bindTo('serial.connected', ebb.id, () => {
// Exit early if we've already done this (happens on reconnects).
if (ebb.version.value) {
return;
}
- controller = cncserver.settings.botConf.get('controller');
+ controller = botConf.get('controller');
// Get EBB version.
- cncserver.ipc.getSerialValue('version').then((message) => {
+ getSerialValue('version').then(message => {
if (message.includes(controller.error)) {
console.error('='.repeat(76));
console.error('Failed to load version information! Message given:');
@@ -52,19 +61,23 @@ module.exports = (cncserver) => {
value: version,
string: message,
};
- console.log(`Connected to ${controller.manufacturer} ${controller.name}, firmware v${version}`);
+ console.log(
+ SLS`Connected to ${controller.manufacturer} ${controller.name},
+ firmware v${version}`
+ );
if (!semver.satisfies(version, minVersion)) {
console.error('='.repeat(76));
- console.error(`ERROR: Firmware version does not meet minimum requirements (${minVersion})`);
- console.error('To upgrade, see: https://wiki.evilmadscientist.com/Updating_EBB_firmware');
+ console.error(SLS`ERROR: Firmware version does not meet minimum
+ requirements (${minVersion})`);
+ console.error(SLS`To upgrade, see:
+ https://wiki.evilmadscientist.com/Updating_EBB_firmware`);
console.error('='.repeat(76));
}
});
});
-
// Bind to general controller message returns.
- cncserver.binder.bindTo('serial.message', ebb.id, (message) => {
+ bindTo('serial.message', ebb.id, message => {
// If this isn't an acknowledgement of a working command...
if (message !== controller.ack) {
if (message.includes(controller.error)) {
@@ -118,8 +131,8 @@ module.exports = (cncserver) => {
*/
// TODO: Find/replace all instances of "serial.sendEBBSetup"
ebb.sendSetup = (id, value) => {
- cncserver.run('custom', `SC,${id},${value}`);
+ run('custom', `SC,${id},${value}`);
};
return ebb;
-};
+}
diff --git a/src/components/machine_support/cncserver.bots.watercolorbot.js b/src/components/machine_support/cncserver.bots.watercolorbot.js
index f0ab0480..cdf838d2 100644
--- a/src/components/machine_support/cncserver.bots.watercolorbot.js
+++ b/src/components/machine_support/cncserver.bots.watercolorbot.js
@@ -1,17 +1,28 @@
/**
* @file Abstraction module for watercolorbot specific stuff.
*/
+import { bindTo } from 'cs/binder';
+import * as pen from 'cs/pen';
+import * as control from 'cs/control';
+import * as tools from 'cs/tools';
+import { getColorID } from 'cs/drawing/base';
+import { getPreset } from 'cs/utils';
// Exposed export.
const watercolorbot = {
- id: 'watercolorbot',
+ id: 'bots.watercolorbot',
paintDistance: 0,
maxPaintDistance: 150, // 48.2 * 10,
};
-module.exports = (cncserver) => {
+/**
+ * Initialize bot specific code.
+ *
+ * @export
+ */
+export default function initBot() {
// Bot support in use parent callback.
- watercolorbot.checkInUse = (botConf) => {
+ watercolorbot.checkInUse = botConf => {
watercolorbot.inUse = botConf.name === 'WaterColorBot';
};
@@ -27,7 +38,7 @@ module.exports = (cncserver) => {
* How many times to move.
*/
watercolorbot.wigglePen = (axis, rawTravel, iterations) => {
- const start = { x: Number(cncserver.pen.state.x), y: Number(cncserver.pen.state.y) };
+ const start = { x: Number(pen.state.x), y: Number(pen.state.y) };
let i = 0;
const travel = Number(rawTravel); // Make sure it's not a string
@@ -55,7 +66,7 @@ module.exports = (cncserver) => {
point[axis] += (toggle ? travel : travel * -1);
}
- cncserver.control.movePenAbs(point);
+ control.movePenAbs(point);
i++;
@@ -63,7 +74,7 @@ module.exports = (cncserver) => {
if (i <= iterations) {
_wiggleSlave(!toggle);
} else { // Done wiggling, go back to start
- cncserver.control.movePenAbs(start);
+ control.movePenAbs(start);
}
}
@@ -74,26 +85,26 @@ module.exports = (cncserver) => {
// Run a full wash in all the waters.
watercolorbot.fullWash = () => {
// TODO: Fix water0 overreach
- cncserver.control.setTool('water0dip');
- cncserver.control.setTool('water1');
- cncserver.control.setTool('water2');
+ tools.changeTo('water0dip');
+ tools.changeTo('water1');
+ tools.changeTo('water2');
};
// Reink with a water dip.
- watercolorbot.reink = (tool = cncserver.pen.state.tool) => {
- cncserver.control.setTool('water0dip');
- cncserver.control.setTool(tool);
+ watercolorbot.reink = (tool = pen.state.tool) => {
+ tools.changeTo('water0dip');
+ tools.changeTo(tool);
};
// Bind the wiggle to the toolchange event.
- cncserver.binder.bindTo('tool.change', watercolorbot.id, (tool) => {
+ bindTo('tool.change', 'watercolorbot', tool => {
// Only trigger this when the current tool isn't a wait.
if (typeof tool.wait === 'undefined') {
// Set the height based on what kind of tool it is.
const downHeight = tool.name.indexOf('water') !== -1 ? 'wash' : 'draw';
// Brush down
- cncserver.pen.setHeight(downHeight);
+ pen.setHeight(downHeight);
// Wiggle the brush a bit
watercolorbot.wigglePen(
@@ -103,7 +114,7 @@ module.exports = (cncserver) => {
);
// Put the pen back up when done!
- cncserver.pen.setHeight('up');
+ pen.setHeight('up');
// Reset paintDistance
watercolorbot.paintDistance = 0;
@@ -111,17 +122,17 @@ module.exports = (cncserver) => {
});
// Bind to begin of rendering path color group.
- cncserver.binder.bindTo('control.render.group.begin', watercolorbot.id, (colorID) => {
+ bindTo('print.render.group.begin', watercolorbot.id, colorID => {
watercolorbot.fullWash();
});
// Bind to end of rendering everything, wash that brush.
- cncserver.binder.bindTo('control.render.finish', watercolorbot.id, () => {
+ bindTo('print.render.finish', watercolorbot.id, () => {
watercolorbot.fullWash();
});
// Bind to path parsing for printing, allows for splitting paths to reink.
- cncserver.binder.bindTo('control.render.path.select', watercolorbot.id, (paths) => {
+ bindTo('print.render.path.select', watercolorbot.id, paths => {
// Only trigger this when WCB is in use.
// TODO: don't trigger this for non reinking implements 😬
if (watercolorbot.inUse) {
@@ -158,16 +169,19 @@ module.exports = (cncserver) => {
});
// Bind to render path complete, to trigger reinking as needed
- cncserver.binder.bindTo('control.render.path.finish', watercolorbot.id, (path) => {
+ bindTo('print.render.path.finish', watercolorbot.id, path => {
watercolorbot.paintDistance += path.length;
if (watercolorbot.paintDistance >= watercolorbot.maxPaintDistance - 1) {
- watercolorbot.reink(cncserver.drawing.base.getColorID(path));
+ watercolorbot.reink(getColorID(path));
}
});
// Bind to color setdefault to set watercolors
- cncserver.binder.bindTo('colors.setDefault', watercolorbot.id, passthroughSet => (watercolorbot.inUse ? cncserver.drawing.colors.setFromPreset('generic-watercolor-generic') : passthroughSet));
-
+ bindTo('colors.setDefault', watercolorbot.id, passthroughSet => (
+ watercolorbot.inUse
+ ? getPreset('colorsets', 'generic-watercolor-generic')
+ : passthroughSet
+ ));
return watercolorbot;
-};
+}
diff --git a/src/components/machine_support/index.js b/src/components/machine_support/index.js
index b72f8681..c23685a1 100644
--- a/src/components/machine_support/index.js
+++ b/src/components/machine_support/index.js
@@ -1,24 +1,25 @@
/**
* @file Index for all machine support components.
*/
-/* eslint-disable global-require */
-const bots = {}; // Conglomerated bot support export.
-
-module.exports = (cncserver) => {
- bots.base = require('./cncserver.bots.base.js')(cncserver);
- bots.ebb = require('./cncserver.bots.ebb.js')(cncserver);
- bots.watercolorbot = require('./cncserver.bots.watercolorbot.js')(cncserver);
+import { bindTo } from 'cs/binder';
+import { botConf } from 'cs/settings';
+import base from 'cs/bots/base';
+import ebb from 'cs/bots/ebb';
+import watercolorbot from 'cs/bots/watercolorbot';
+// Conglomerated bot support export.
+export const bots = {
+ base: base(),
+ ebb: ebb(),
+ watercolorbot: watercolorbot(),
+};
- // Allow each bot support module to enable/disable itself based on setup info.
- cncserver.binder.bindTo('controller.setup', 'bots', () => {
- const conf = cncserver.settings.botConf.get();
- Object.values(bots).forEach((botSupport) => {
- if (typeof botSupport.checkInUse === 'function') {
- botSupport.checkInUse(conf);
- }
- });
+// Allow each bot support module to enable/disable itself based on setup info.
+bindTo('controller.setup', 'bots', () => {
+ const conf = botConf.get();
+ Object.values(bots).forEach(botSupport => {
+ if (typeof botSupport.checkInUse === 'function') {
+ botSupport.checkInUse(conf);
+ }
});
-
- return bots;
-};
+});
diff --git a/src/components/third_party/scratch/SCRATCH.API.md b/src/components/third_party/scratch/SCRATCH.API.md
index b4971905..bb88eb04 100644
--- a/src/components/third_party/scratch/SCRATCH.API.md
+++ b/src/components/third_party/scratch/SCRATCH.API.md
@@ -7,25 +7,26 @@ these are as experimental as Scratch considers them, and the output format may
change without warning.
### Unlike the ReSTful API:
- * `GET` is the only method used for the requests. This means data is only
-passed in the URL path structure, or in get variables.
- * No data is intended to be read from HTTP responses with the exception of the
-`/poll` endpoint, therefore other request in the API return no data.
- * Scratch currently doesn't support any kind of namespacing for URIs, so these
-endpoints live outside the `/v1` ReSTful namsespaced API, directly on the
-root.
- * Scratch _**also**_ doesn't seem to support arbitrary slashes in URI structure
-so I've replaced them with periods. This unfortunately does not apply to
-variables, and must be also added to the end before any variables (if any).
-:imp:
+
+- `GET` is the only method used for the requests. This means data is only
+ passed in the URL path structure, or in get variables.
+- No data is intended to be read from HTTP responses with the exception of the
+ `/poll` endpoint, therefore other request in the API return no data.
+- Scratch currently doesn't support any kind of namespacing for URIs, so these
+ endpoints live outside the `/v1` ReSTful namsespaced API, directly on the
+ root.
+- Scratch _**also**_ doesn't seem to support arbitrary slashes in URI structure
+ so I've replaced them with periods. This unfortunately does not apply to
+ variables, and must be also added to the end before any variables (if any).
+ :imp:
In each request example below, the server is assumed to be added to the
beginning of each resource, E.G.: `GET http://localhost:4242/poll` will `GET`
the poll content output page from a server plugged into the local computer, at
the default port of `4242`.
-
### How do I use this?
+
Though there's not a 1:1 relationship between them, these API endpoints are
intended to be used in Scratch via the custom block definitions found in
[watercolorbot_scratch.s2e](watercolorbot_scratch.s2e). Though this is
@@ -42,8 +43,8 @@ the `.s2e` file above from the root of CNC Server.
Once imported, the blocks depicted above should all be available in the
"More Blocks" section on the UI.
-
### An Important Note on Timing... :clock1: :clock1030: :clock10:
+
At the beginning of
[creating Scratch support](https://github.com/techninja/cncserver/issues/50),
efforts focused on ensuring that Scratch would wait for every single process to
@@ -57,17 +58,20 @@ the buffer. One remaining downside is that X/Y/Z positional values (and others)
read from the bot are realtime and do not take into account past or possible
future values.
-* * *
+---
## 1. Output
-These are Scratch 2 required, *output only* resources required for general use.
+
+These are Scratch 2 required, _output only_ resources required for general use.
### GET /poll
+
This page is polled during every screen draw (about 30 times a second), and
-all input *into* Scratch is handled here. Any value block are populated directly
+all input _into_ Scratch is handled here. Any value block are populated directly
from this page, along with any waiting processes (not yet fully implemented).
#### Response
+
```javascript
HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8
@@ -79,7 +83,6 @@ angle 90
sleeping 0
state 0
height 0
-busy false
tool color0
lastDuration 0
distanceCounter 0
@@ -87,65 +90,77 @@ simulation 1
```
##### Usage Notes
- * Values are set directly from internal storage variables and reflect what the
-bot is currently doing, not what was sent into the buffer and still must be
-done.
- * `x` & `y` are in an arbitrary absolute scale based on the relative offset
-between Scratch pixel values and the number of steps on the WaterColorBot stage
-at default resolution, with 0,0 right in the center to mimic Scratch standards.
- * `z` is either 0 or 1 depending on if the brush is up (0/Not painting), or down
-(1/Totally painting)
- * `angle` the current angle of the internal "turtle", used only for this API to
-allow for linear forward and backward relative movement.
- * `sleeping`, either 0 or 1, states whether the API is actually listening to
-commands.
- * Other fields are pulled directly from the
-[pen object of the ReSTful API](https://github.com/techninja/cncserver/blob/master/API.md#response).
-
-* * *
+
+- Values are set directly from internal storage variables and reflect what the
+ bot is currently doing, not what was sent into the buffer and still must be
+ done.
+- `x` & `y` are in an arbitrary absolute scale based on the relative offset
+ between Scratch pixel values and the number of steps on the WaterColorBot stage
+ at default resolution, with 0,0 right in the center to mimic Scratch standards.
+- `z` is either 0 or 1 depending on if the brush is up (0/Not painting), or down
+ (1/Totally painting)
+- `angle` the current angle of the internal "turtle", used only for this API to
+ allow for linear forward and backward relative movement.
+- `sleeping`, either 0 or 1, states whether the API is actually listening to
+ commands.
+- Other fields are pulled directly from the
+ [pen object of the ReSTful API](https://github.com/techninja/cncserver/blob/master/API.md#response).
+
+---
### GET /crossdomain.xml
+
Created before the CORs standard, this required "file" for flash applications
allows the resources to be accessed outside of the security realm of the flash
application.
#### Response
+
```javascript
GET /crossdomain.xml
Content-Type: text/plain; charset=UTF-8
```
-* * *
+
+---
## Triggers/Input
+
**Note:** The following examples do not include requests or responses as these
all use the `GET` HTTP method with URL parameters and return no text in the body
as described above.
## 3. Absolute movement/settings
+
If you need to change something to an exact know value, these are what you need.
### GET /pen.up & /pen.down
+
Does just what it says on the tin.
### GET /coord/:x/:y
+
Set the absolute X/Y position, given in the same pixel approximate scale in the
return coordinates.
### GET /coord/:named-x/:named-y
+
Set the absolute X/Y position based on the given word for the position, E.G.:
`/coord/right/bottom`, `/coord/center/top`. Accepts standard English `top`,
`left`, `right`, `bottom` & `center`.
### GET /move.absturn./:angle
+
Set the exact angle of the turtle's facing direction or relative movement to the
given angle.
### GET /move.speed./:value
+
Sets the move speed to the given value, accepts 0-10.
### GET /penreink/:distance
+
Sets the number of centimeters the bot will draw (movement over the canvas while
the brush is down) before "re-inking" the brush with the last used media
(water/paint). 48 is the default value used by RoboPaint and is recommended.
@@ -153,74 +168,90 @@ This value is reset to 0 (do nothing) whenever the Stop button/`/reset_all` is
triggered.
### GET /penstopreink
+
A shortcut to set the re-ink distance to 0, or OFF.
-* * *
+---
## 4. Relative movement/settings
+
Relative movement is made possible via the "turtle" pointer interface and is
based on angle, X/Y or other known variables. All relative values are sanity
checked to avoid crashes.
### GET /move.forward./:amount
+
Steps the turtle forward (based on angle) from current position.
### GET /move.nudge.x./:amount & /move.nudge.y./:amount
+
Nudges the turtle X or Y value by the given amount. Allows for positive or
negative values, does not change turtle angle.
### GET /move.right./:amount & /move.left./:amount
+
Rotate direction that the turtle is facing either right or left by the amount of
degrees given.
### GET /move.toward./:x/:y
+
Absolutely sets the turtle facing direction angle towards an absolute X/Y
-coordinate *relative* to the current turtle position. Useful for "follow the
+coordinate _relative_ to the current turtle position. Useful for "follow the
mouse" applications.
-* * *
+---
## 5. Grouped functions
+
These do a series of actions to help make things easier
### GET /tool.color./:index & /tool.water./:index
+
Helpers for getting water or paint on the brush based off the integer index
number given. 0-2 for `water`, 0-7 for `color`.
### GET /pen.wash
+
Washes the brush in all three water dishes from the top to the bottom. Does not
park, but does life the brush when complete.
### GET /park
-Park the bot in the top left parking location. Note: this is *not* the default
+
+Park the bot in the top left parking location. Note: this is _not_ the default
absolute center position the turtle pointer begins at, and this will not effect
the turtle position.
## 6. Misc
### GET /reset_all
+
Triggered by Scratch when the red "stop sign" is clicked, or when another
action is expected to reset the program.
##### Usage Notes
- * Completely resets internal "turtle" to 0 position
- * Clears entire run buffer, so this should stop any currently running action
-or group action (like getting paint).
- * _*Does not*_ park or lift the brush. This may be up for debate.
-* * *
+- Completely resets internal "turtle" to 0 position
+- Clears entire run buffer, so this should stop any currently running action
+ or group action (like getting paint).
+- _*Does not*_ park or lift the brush. This may be up for debate.
+
+---
### GET /pen.off
+
Turn off motors and reset pointer to top left park position.
### GET /pen.resetDistance
+
Reset the `distanceCounter` variable back to 0.
### GET /pen.sleep.0 & /pen.sleep.1
+
Turn on or off sleep mode based on the given value, either 1 for on, 0 for off.
Sleep mode ON will prevent the API from doing anything until sleep mode is turned
OFF.
### GET /move.wait./:seconds
+
Sets a "wait" command in the buffer for the specified number of seconds, decimal
values like 0.5 are OK.
diff --git a/src/components/third_party/scratch/cncserver.scratch.js b/src/components/third_party/scratch/cncserver.scratch.js
index ac50b165..90313e56 100644
--- a/src/components/third_party/scratch/cncserver.scratch.js
+++ b/src/components/third_party/scratch/cncserver.scratch.js
@@ -1,405 +1,408 @@
/**
* @file CNC Server scratch support module.
*/
+import * as pen from 'cs/pen';
+import * as actualPen from 'cs/actualPen';
+import * as utils from 'cs/utils';
+import * as control from 'cs/control';
+import * as tools from 'cs/tools';
+import { bot, botConf, gConf } from 'cs/settings';
+import run from 'cs/run';
+import { createServerEndpoint } from 'cs/rest';
+import * as buffer from 'cs/buffer';
+
const sizeMultiplier = 10; // Amount to increase size of steps
let turtle = {}; // Global turtle state object.
-const scratch = {};
-
-module.exports = (cncserver) => {
- // Move request endpoint handler function
- function moveRequest(req) {
- const url = req.originalUrl.split('.');
-
- const op = url[1];
- const { arg2 } = req.params;
- let { arg } = req.params;
- if (arg2 && !arg) {
- [,, arg] = url;
- }
- // Do nothing if sleeping
- if (turtle.sleeping) {
- // TODO: Do we care about running the math?
- return { code: 200, body: '' };
- }
+// Move request endpoint handler function
+function moveRequest(req) {
+ const url = req.originalUrl.split('.');
- // Park
- if (req.url === '/park') {
- cncserver.pen.setHeight('up');
- cncserver.pen.setPen({
- x: cncserver.settings.bot.park.x,
- y: cncserver.settings.bot.park.y,
- park: true,
- });
- return { code: 200, body: '' };
- }
+ const op = url[1];
+ const { arg2 } = req.params;
+ let { arg } = req.params;
+ if (arg2 && !arg) {
+ [, , arg] = url;
+ }
- // Arbitrary Wait
- if (op === 'wait') {
- arg = parseFloat(arg) * 1000;
- cncserver.run('wait', false, arg);
- return { code: 200, body: '' };
- }
+ // Do nothing if sleeping
+ if (turtle.sleeping) {
+ // TODO: Do we care about running the math?
+ return { code: 200, body: '' };
+ }
- // Speed setting
- if (op === 'speed') {
- arg = parseFloat(arg) * 10;
- cncserver.settings.botConf.set('speed:drawing', arg);
- cncserver.settings.botConf.set('speed:moving', arg);
- }
+ // Park
+ if (req.url === '/park') {
+ pen.setHeight('up');
+ pen.setPen({
+ x: bot.park.x,
+ y: bot.park.y,
+ park: true,
+ });
+ return { code: 200, body: '' };
+ }
- // Rotating Pointer? (just rotate)
- if (op === 'left' || op === 'right') {
- arg = parseInt(arg, 10);
- turtle.degrees = op === 'right'
- ? turtle.degrees + arg
- : turtle.degrees - arg;
-
- if (turtle.degrees > 360) turtle.degrees -= 360;
- if (turtle.degrees < 0) turtle.degrees += 360;
- console.log(`Rotate ${op} ${arg} deg. to ${turtle.degrees} deg.`);
- return { code: 200, body: '' };
- }
+ // Arbitrary Wait
+ if (op === 'wait') {
+ arg = parseFloat(arg) * 1000;
+ run('wait', false, arg);
+ return { code: 200, body: '' };
+ }
- // Rotate pointer towards turtle relative X/Y
- if (op === 'toward') {
- const { workArea } = cncserver.settings.bot;
+ // Speed setting
+ if (op === 'speed') {
+ arg = parseFloat(arg) * 10;
+ botConf.set('speed:drawing', arg);
+ botConf.set('speed:moving', arg);
+ }
- // Convert input X/Y from scratch coordinates
- const point = {
- x: (parseInt(arg, 10) * sizeMultiplier) + workArea.absCenter.x,
- y: (-parseInt(arg2, 10) * sizeMultiplier) + workArea.absCenter.y,
- };
+ // Rotating Pointer? (just rotate)
+ if (op === 'left' || op === 'right') {
+ arg = parseInt(arg, 10);
+ turtle.degrees = op === 'right'
+ ? turtle.degrees + arg
+ : turtle.degrees - arg;
- const theta = Math.atan2(point.y - turtle.y, point.x - turtle.x);
- turtle.degrees = Math.round(theta * 180 / Math.PI);
- if (turtle.degrees > 360) turtle.degrees -= 360;
- if (turtle.degrees < 0) turtle.degrees += 360;
+ if (turtle.degrees > 360) turtle.degrees -= 360;
+ if (turtle.degrees < 0) turtle.degrees += 360;
+ console.log(`Rotate ${op} ${arg} deg. to ${turtle.degrees} deg.`);
+ return { code: 200, body: '' };
+ }
- console.log(`Rotate relative towards ${point.x}, ${point.y}
- from ${turtle.x}, ${turtle.y} to ${turtle.degrees} deg`);
- return { code: 200, body: '' };
- }
+ // Rotate pointer towards turtle relative X/Y
+ if (op === 'toward') {
+ const { workArea } = bot;
- // Rotate pointer directly
- if (op === 'absturn') {
- // Correct for "standard" Turtle orientation in Scratch.
- turtle.degrees = parseInt(arg, 10) - 90;
- console.log(
- `Rotate to ${arg} scratch degrees (actual angle ${turtle.degrees} deg)`
- );
- return { code: 200, body: '' };
- }
+ // Convert input X/Y from scratch coordinates
+ const point = {
+ x: (parseInt(arg, 10) * sizeMultiplier) + workArea.absCenter.x,
+ y: (-parseInt(arg2, 10) * sizeMultiplier) + workArea.absCenter.y,
+ };
- // Simple Nudge X/Y
- if (op === 'nudge') {
- if (arg === 'y') {
- turtle[arg] += -1 * parseInt(arg2, 10) * sizeMultiplier;
- } else {
- turtle[arg] += parseInt(arg2, 10) * sizeMultiplier;
- }
- }
+ const theta = Math.atan2(point.y - turtle.y, point.x - turtle.x);
+ turtle.degrees = Math.round(theta * 180 / Math.PI);
+ if (turtle.degrees > 360) turtle.degrees -= 360;
+ if (turtle.degrees < 0) turtle.degrees += 360;
+
+ console.log(`Rotate relative towards ${point.x}, ${point.y}
+ from ${turtle.x}, ${turtle.y} to ${turtle.degrees} deg`);
+ return { code: 200, body: '' };
+ }
- // Move Pointer? Actually move!
- if (op === 'forward') {
- arg = parseInt(arg, 10);
+ // Rotate pointer directly
+ if (op === 'absturn') {
+ // Correct for "standard" Turtle orientation in Scratch.
+ turtle.degrees = parseInt(arg, 10) - 90;
+ console.log(
+ `Rotate to ${arg} scratch degrees (actual angle ${turtle.degrees} deg)`
+ );
+ return { code: 200, body: '' };
+ }
- console.log(`Move pen by ${arg} steps`);
- const radians = turtle.degrees * (Math.PI / 180);
- turtle.x = Math.round(turtle.x + Math.cos(radians) * arg * sizeMultiplier);
- turtle.y = Math.round(turtle.y + Math.sin(radians) * arg * sizeMultiplier);
+ // Simple Nudge X/Y
+ if (op === 'nudge') {
+ if (arg === 'y') {
+ turtle[arg] += -1 * parseInt(arg2, 10) * sizeMultiplier;
+ } else {
+ turtle[arg] += parseInt(arg2, 10) * sizeMultiplier;
}
+ }
+
+ // Move Pointer? Actually move!
+ if (op === 'forward') {
+ arg = parseInt(arg, 10);
- // Move x, y or both
- if (op === 'x' || op === 'y' || typeof req.params.x !== 'undefined') {
- arg = parseInt(arg, 10);
+ console.log(`Move pen by ${arg} steps`);
+ const radians = turtle.degrees * (Math.PI / 180);
+ turtle.x = Math.round(turtle.x + Math.cos(radians) * arg * sizeMultiplier);
+ turtle.y = Math.round(turtle.y + Math.sin(radians) * arg * sizeMultiplier);
+ }
- if (op === 'x' || op === 'y') {
- turtle[op] = arg * sizeMultiplier;
+ // Move x, y or both
+ if (op === 'x' || op === 'y' || typeof req.params.x !== 'undefined') {
+ arg = parseInt(arg, 10);
+
+ if (op === 'x' || op === 'y') {
+ turtle[op] = arg * sizeMultiplier;
+ } else {
+ // Word positions? convert to actual coordinates
+ // X/Y swapped for "top left" arg positions.
+ const wordX = ['left', 'center', 'right'].indexOf(req.params.y);
+ const wordY = ['top', 'center', 'bottom'].indexOf(req.params.x);
+ if (wordX > -1) {
+ const steps = utils.centToSteps({
+ x: (wordX / 2) * 100,
+ y: (wordY / 2) * 100,
+ });
+
+ turtle.x = steps.x;
+ turtle.y = steps.y;
} else {
- // Word positions? convert to actual coordinates
- // X/Y swapped for "top left" arg positions.
- const wordX = ['left', 'center', 'right'].indexOf(req.params.y);
- const wordY = ['top', 'center', 'bottom'].indexOf(req.params.x);
- if (wordX > -1) {
- const steps = cncserver.utils.centToSteps({
- x: (wordX / 2) * 100,
- y: (wordY / 2) * 100,
- });
-
- turtle.x = steps.x;
- turtle.y = steps.y;
- } else {
- // Convert input X/Y to steps via multiplier
- turtle.x = parseInt(req.params.x, 10) * sizeMultiplier;
-
- // In Scratch, positive Y is up on the page. :(
- turtle.y = -1 * parseInt(req.params.y, 10) * sizeMultiplier;
-
- // When directly setting XY position, offset by half for center 0,0
- turtle.x += cncserver.settings.bot.workArea.absCenter.x;
- turtle.y += cncserver.settings.bot.workArea.absCenter.y;
- }
- }
+ // Convert input X/Y to steps via multiplier
+ turtle.x = parseInt(req.params.x, 10) * sizeMultiplier;
+
+ // In Scratch, positive Y is up on the page. :(
+ turtle.y = -1 * parseInt(req.params.y, 10) * sizeMultiplier;
- console.log(`Move pen to coord ${turtle.x}, ${turtle.y}`);
+ // When directly setting XY position, offset by half for center 0,0
+ turtle.x += bot.workArea.absCenter.x;
+ turtle.y += bot.workArea.absCenter.y;
+ }
}
- // Attempt to move pen to desired point (may be off screen)
- const distance = cncserver.control.movePenAbs(turtle);
- if (distance === 0) console.log('Not moved any distance');
+ console.log(`Move pen to coord ${turtle.x}, ${turtle.y}`);
+ }
- // Add up distance counter
- if ((cncserver.utils.penDown()) && !cncserver.pen.state.offCanvas) {
- turtle.distanceCounter = parseInt(
- Number(distance) + Number(turtle.distanceCounter),
- 10
- );
- }
+ // Attempt to move pen to desired point (may be off screen)
+ const distance = control.movePenAbs(turtle);
+ if (distance === 0) console.log('Not moved any distance');
- // If reink initialized, check distance and initiate reink!
- if (turtle.reinkDistance > 0
- && turtle.distanceCounter > turtle.reinkDistance) {
- turtle.distanceCounter = 0;
+ // Add up distance counter
+ if ((pen.isDown()) && !pen.state.offCanvas) {
+ turtle.distanceCounter = parseInt(
+ Number(distance) + Number(turtle.distanceCounter),
+ 10
+ );
+ }
- // Reink procedure!
- cncserver.control.setTool('water0dip'); // Dip in the water
- cncserver.control.setTool(turtle.media); // Apply the last saved media
- cncserver.control.movePenAbs(turtle); // Move back to "current" position
- cncserver.pen.setHeight('draw'); // Set the position back to draw
- }
+ // If reink initialized, check distance and initiate reink!
+ if (turtle.reinkDistance > 0
+ && turtle.distanceCounter > turtle.reinkDistance) {
+ turtle.distanceCounter = 0;
- return { code: 200, body: '' };
+ // Reink procedure!
+ tools.changeTo('water0dip'); // Dip in the water
+ tools.changeTo(turtle.media); // Apply the last saved media
+ control.movePenAbs(turtle); // Move back to "current" position
+ pen.setHeight('draw'); // Set the position back to draw
}
- // General pen style request handler
- function penRequest(req) {
- // Parse out the arguments as we can't use slashes in the URI(!?!)
- const url = req.originalUrl.split('.');
- let [, op, arg] = url;
+ return { code: 200, body: '' };
+}
- // Reset internal counter
- if (op === 'resetDistance') {
- turtle.distanceCounter = 0;
- return { code: 200, body: '' };
- }
+// General pen style request handler
+function penRequest(req) {
+ // Parse out the arguments as we can't use slashes in the URI(!?!)
+ const url = req.originalUrl.split('.');
+ let [, op, arg] = url;
- // Toggle sleep/simulation mode
- if (op === 'sleep') {
- arg = parseInt(arg, 10);
- turtle.sleeping = !!arg; // Convert integer to true boolean
- return { code: 200, body: '' };
- }
+ // Reset internal counter
+ if (op === 'resetDistance') {
+ turtle.distanceCounter = 0;
+ return { code: 200, body: '' };
+ }
- // Do nothing if sleeping
- if (turtle.sleeping) {
- // TODO: Do we care about running the math?
- return { code: 200, body: '' };
- }
+ // Toggle sleep/simulation mode
+ if (op === 'sleep') {
+ arg = parseInt(arg, 10);
+ turtle.sleeping = !!arg; // Convert integer to true boolean
+ return { code: 200, body: '' };
+ }
- // Set Pen up/down
- if (op === 'up' || op === 'down') {
- if (op === 'down') {
- op = 'draw';
- }
+ // Do nothing if sleeping
+ if (turtle.sleeping) {
+ // TODO: Do we care about running the math?
+ return { code: 200, body: '' };
+ }
- // Don't set the height explicitly when off the canvas
- if (!cncserver.pen.state.offCanvas) {
- cncserver.pen.setHeight(op);
- } else {
- // Save the state for when we come back
- cncserver.pen.forceState({ state: op });
- }
+ // Set Pen up/down
+ if (op === 'up' || op === 'down') {
+ if (op === 'down') {
+ op = 'draw';
}
- // Run simple wash
- if (op === 'wash') {
- cncserver.control.setTool('water0');
- cncserver.control.setTool('water1');
- cncserver.control.setTool('water2');
+ // Don't set the height explicitly when off the canvas
+ if (!pen.state.offCanvas) {
+ pen.setHeight(op);
+ } else {
+ // Save the state for when we come back
+ pen.forceState({ state: op });
}
+ }
- // Turn off motors and zero to park pos
- if (op === 'off') {
- // Zero the assumed position
- const park = cncserver.utils.centToSteps(cncserver.settings.bot.park, true);
- cncserver.pen.forceState({ x: park.x, y: park.y });
- cncserver.actualPen.forceState({ x: park.x, y: park.y });
-
- // You must zero FIRST then disable, otherwise actualPen is overwritten
- cncserver.run('custom', 'EM,0,0');
- cncserver.sockets.sendPenUpdate();
- }
- return { code: 200, body: '' };
+ // Run simple wash
+ if (op === 'wash') {
+ tools.changeTo('water0');
+ tools.changeTo('water1');
+ tools.changeTo('water2');
}
- // Tool Request Handler
- function toolRequest(req) {
- const type = req.originalUrl.split('.')[1];
+ // Turn off motors and zero to park pos
+ if (op === 'off') {
+ // Zero the assumed position
+ const park = utils.centToSteps(bot.park, true);
+ pen.forceState({ x: park.x, y: park.y });
+ actualPen.forceState({ x: park.x, y: park.y });
- // Do nothing if sleeping
- if (turtle.sleeping) {
- // TODO: Do we care about running the math?
- return { code: 200, body: '' };
- }
+ // You must zero FIRST then disable, otherwise actualPen is overwritten
+ run('custom', 'EM,0,0');
+ }
+ return { code: 200, body: '' };
+}
- // Set by ID (water/color)
- if (type) {
- const tool = type + parseInt(req.params.id, 10);
- cncserver.control.setTool(tool);
- turtle.media = tool;
- }
+// Tool Request Handler
+function toolRequest(req) {
+ const type = req.originalUrl.split('.')[1];
+ // Do nothing if sleeping
+ if (turtle.sleeping) {
+ // TODO: Do we care about running the math?
return { code: 200, body: '' };
}
- scratch.initAPI = () => {
- const pollData = {}; // "Array" of "sensor" data to be spat out to poll page
- turtle = { // Helper turtle for relative movement
- x: cncserver.settings.bot.workArea.absCenter.x,
- y: cncserver.settings.bot.workArea.absCenter.y,
- limit: 'workArea',
- sleeping: false,
- reinkDistance: 0,
- media: 'water0',
- degrees: 0,
- distanceCounter: 0,
- };
+ // Set by ID (water/color)
+ if (type) {
+ const tool = type + parseInt(req.params.id, 10);
+ tools.changeTo(tool);
+ turtle.media = tool;
+ }
- pollData.render = function renderData() {
- let out = '';
+ return { code: 200, body: '' };
+}
+
+export function initAPI() {
+ const pollData = {}; // "Array" of "sensor" data to be spat out to poll page
+ turtle = { // Helper turtle for relative movement
+ x: bot.workArea.absCenter.x,
+ y: bot.workArea.absCenter.y,
+ limit: 'workArea',
+ sleeping: false,
+ reinkDistance: 0,
+ media: 'water0',
+ degrees: 0,
+ distanceCounter: 0,
+ };
- const { settings: { bot: { workArea } } } = cncserver;
+ pollData.render = function renderData() {
+ let out = '';
- out += `x ${(turtle.x - workArea.absCenter.x) / sizeMultiplier}\n`;
- out += `y ${(turtle.y - workArea.absCenter.y) / sizeMultiplier}\n`;
- out += `z ${cncserver.utils.penDown() ? '1' : '0'}\n`;
+ const { workArea } = bot;
- // Correct for "standard" Turtle orientation in Scratch
- let angleTemp = turtle.degrees + 90;
- if (angleTemp > 360) {
- angleTemp -= 360;
- }
+ out += `x ${(turtle.x - workArea.absCenter.x) / sizeMultiplier}\n`;
+ out += `y ${(turtle.y - workArea.absCenter.y) / sizeMultiplier}\n`;
+ out += `z ${pen.isDown() ? '1' : '0'}\n`;
- out += `angle ${angleTemp}\n`;
- out += `distanceCounter ${turtle.distanceCounter / sizeMultiplier}\n`;
- out += `sleeping ${turtle.sleeping ? '1' : '0'}\n`;
-
- // Loop through all existing/static pollData
- // TODO: Fix this from original source, this is just wrong :/
- /* out += Object.keys(pollData).reduce(
- (line, key) => `${line}${key} ${pollData[key].join(' ')}\n`
- ); */
-
- // Throw in full pen data as well
- for (const [key, value] of Object.entries(cncserver.pen.state)) {
- if (key !== 'x' && key !== 'y' && key !== 'distanceCounter') {
- out += `${key} ${value}\n`;
- }
- }
- return out;
- };
+ // Correct for "standard" Turtle orientation in Scratch
+ let angleTemp = turtle.degrees + 90;
+ if (angleTemp > 360) {
+ angleTemp -= 360;
+ }
- // Helper function to add/remove busy watchers
- // TODO: Not fully implemented as performance is better without waiting.
- pollData.busy = (id, destroy) => {
- if (!pollData._busy) {
- pollData._busy = []; // Add busy placeholder
- }
+ out += `angle ${angleTemp}\n`;
+ out += `distanceCounter ${turtle.distanceCounter / sizeMultiplier}\n`;
+ out += `sleeping ${turtle.sleeping ? '1' : '0'}\n`;
- const index = pollData._busy.indexOf(id);
+ // Loop through all existing/static pollData
+ // TODO: Fix this from original source, this is just wrong :/
+ /* out += Object.keys(pollData).reduce(
+ (line, key) => `${line}${key} ${pollData[key].join(' ')}\n`
+ ); */
- if (destroy && index > -1) { // Remove
- pollData._busy.splice(index, 1);
- } else if (!destroy && index === -1) { // Add!
- pollData._busy.push(id);
+ // Throw in full pen data as well
+ for (const [key, value] of Object.entries(pen.state)) {
+ if (key !== 'x' && key !== 'y' && key !== 'distanceCounter') {
+ out += `${key} ${value}\n`;
}
- };
+ }
+ return out;
+ };
+ // Helper function to add/remove busy watchers
+ // TODO: Not fully implemented as performance is better without waiting.
+ pollData.busy = (id, destroy) => {
+ if (!pollData._busy) {
+ pollData._busy = []; // Add busy placeholder
+ }
- // SCRATCH v2 Specific endpoints =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
- // Central poll returner (Queried ~30hz)
- cncserver.rest.createServerEndpoint('/poll', () => (
- { code: 200, body: pollData.render() }
- ));
-
- // Flash crossdomain helper
- cncserver.rest.createServerEndpoint('/crossdomain.xml', () => ({
- code: 200,
- body:
- `\
- `,
- }));
-
- // Initialize/reset status
- cncserver.rest.createServerEndpoint('/reset_all', () => {
- turtle = { // Reset to default
- x: cncserver.settings.bot.workArea.absCenter.x,
- y: cncserver.settings.bot.workArea.absCenter.y,
- limit: 'workArea', // Limits movements to bot work area
- sleeping: false,
- media: 'water0',
- reinkDistance: 0,
- degrees: 0,
- distanceCounter: 0,
- };
-
- // Clear Run Buffer
- // @see /v1/buffer/ DELETE
- cncserver.buffer.clear();
-
- pollData._busy = []; // Clear busy indicators
- return { code: 200, body: '' };
- });
+ const index = pollData._busy.indexOf(id);
- // SCRATCH v2 Specific endpoints =^=-=^=-=^=-=^=-=^=-=^=-=^=-=^=-=^=-=^=-=^=
-
- // Move Endpoint(s)
- cncserver.rest.createServerEndpoint('/park', moveRequest);
- cncserver.rest.createServerEndpoint('/coord/:x/:y', moveRequest);
- cncserver.rest.createServerEndpoint('/move.forward./:arg', moveRequest);
- cncserver.rest.createServerEndpoint('/move.wait./:arg', moveRequest);
- cncserver.rest.createServerEndpoint('/move.right./:arg', moveRequest);
- cncserver.rest.createServerEndpoint('/move.left./:arg', moveRequest);
- cncserver.rest.createServerEndpoint('/move.absturn./:arg', moveRequest);
- cncserver.rest.createServerEndpoint('/move.toward./:arg/:arg2', moveRequest);
- cncserver.rest.createServerEndpoint('/move.speed./:arg', moveRequest);
-
- cncserver.rest.createServerEndpoint('/move.nudge.x./:arg2', moveRequest);
- cncserver.rest.createServerEndpoint('/move.nudge.y./:arg2', moveRequest);
-
- // Reink initialization endpoint
- cncserver.rest.createServerEndpoint('/penreink/:distance', (req) => {
- // 167.7 = 1.6mm per step * 100 mm per cm (as input)
- const cm = parseFloat(req.params.distance);
- turtle.reinkDistance = Math.round(cm * 167.7);
- console.log('Reink distance: ', turtle.reinkDistance);
- return { code: 200, body: '' };
- });
+ if (destroy && index > -1) { // Remove
+ pollData._busy.splice(index, 1);
+ } else if (!destroy && index === -1) { // Add!
+ pollData._busy.push(id);
+ }
+ };
- // Stop Reinking endpoint
- cncserver.rest.createServerEndpoint('/penstopreink', () => {
- turtle.reinkDistance = 0;
- console.log('Reink distance: ', turtle.reinkDistance);
- return { code: 200, body: '' };
- });
+ // SCRATCH v2 Specific endpoints =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+ // Central poll returner (Queried ~30hz)
+ createServerEndpoint('/poll', () => (
+ { code: 200, body: pollData.render() }
+ ));
+
+ // Flash crossdomain helper
+ createServerEndpoint('/crossdomain.xml', () => ({
+ code: 200,
+ body:
+ `\
+ `,
+ }));
+
+ // Initialize/reset status
+ createServerEndpoint('/reset_all', () => {
+ turtle = { // Reset to default
+ x: bot.workArea.absCenter.x,
+ y: bot.workArea.absCenter.y,
+ limit: 'workArea', // Limits movements to bot work area
+ sleeping: false,
+ media: 'water0',
+ reinkDistance: 0,
+ degrees: 0,
+ distanceCounter: 0,
+ };
- // Pen endpoints
- cncserver.rest.createServerEndpoint('/pen', penRequest);
- cncserver.rest.createServerEndpoint('/pen.wash', penRequest);
- cncserver.rest.createServerEndpoint('/pen.up', penRequest);
- cncserver.rest.createServerEndpoint('/pen.down', penRequest);
- cncserver.rest.createServerEndpoint('/pen.off', penRequest);
- cncserver.rest.createServerEndpoint('/pen.resetDistance', penRequest);
- cncserver.rest.createServerEndpoint('/pen.sleep.1', penRequest);
- cncserver.rest.createServerEndpoint('/pen.sleep.0', penRequest);
-
- // Tool set endpoints
- cncserver.rest.createServerEndpoint('/tool.color./:id', toolRequest);
- cncserver.rest.createServerEndpoint('/tool.water./:id', toolRequest);
- };
+ // Clear Run Buffer
+ // @see /v1/buffer/ DELETE
+ buffer.clear();
- return scratch;
-};
+ pollData._busy = []; // Clear busy indicators
+ return { code: 200, body: '' };
+ });
+
+ // SCRATCH v2 Specific endpoints =^=-=^=-=^=-=^=-=^=-=^=-=^=-=^=-=^=-=^=-=^=
+
+ // Move Endpoint(s)
+ createServerEndpoint('/park', moveRequest);
+ createServerEndpoint('/coord/:x/:y', moveRequest);
+ createServerEndpoint('/move.forward./:arg', moveRequest);
+ createServerEndpoint('/move.wait./:arg', moveRequest);
+ createServerEndpoint('/move.right./:arg', moveRequest);
+ createServerEndpoint('/move.left./:arg', moveRequest);
+ createServerEndpoint('/move.absturn./:arg', moveRequest);
+ createServerEndpoint('/move.toward./:arg/:arg2', moveRequest);
+ createServerEndpoint('/move.speed./:arg', moveRequest);
+
+ createServerEndpoint('/move.nudge.x./:arg2', moveRequest);
+ createServerEndpoint('/move.nudge.y./:arg2', moveRequest);
+
+ // Reink initialization endpoint
+ createServerEndpoint('/penreink/:distance', req => {
+ // 167.7 = 1.6mm per step * 100 mm per cm (as input)
+ const cm = parseFloat(req.params.distance);
+ turtle.reinkDistance = Math.round(cm * 167.7);
+ console.log('Reink distance: ', turtle.reinkDistance);
+ return { code: 200, body: '' };
+ });
+
+ // Stop Reinking endpoint
+ createServerEndpoint('/penstopreink', () => {
+ turtle.reinkDistance = 0;
+ console.log('Reink distance: ', turtle.reinkDistance);
+ return { code: 200, body: '' };
+ });
+
+ // Pen endpoints
+ createServerEndpoint('/pen', penRequest);
+ createServerEndpoint('/pen.wash', penRequest);
+ createServerEndpoint('/pen.up', penRequest);
+ createServerEndpoint('/pen.down', penRequest);
+ createServerEndpoint('/pen.off', penRequest);
+ createServerEndpoint('/pen.resetDistance', penRequest);
+ createServerEndpoint('/pen.sleep.1', penRequest);
+ createServerEndpoint('/pen.sleep.0', penRequest);
+
+ // Tool set endpoints
+ createServerEndpoint('/tool.color./:id', toolRequest);
+ createServerEndpoint('/tool.water./:id', toolRequest);
+}
diff --git a/src/interface/images/loading.svg b/src/interface/images/loading.svg
new file mode 100644
index 00000000..997f091c
--- /dev/null
+++ b/src/interface/images/loading.svg
@@ -0,0 +1,53 @@
+
+
diff --git a/src/interface/index.html b/src/interface/index.html
index d2a43db5..2738b6c6 100644
--- a/src/interface/index.html
+++ b/src/interface/index.html
@@ -1,109 +1,113 @@
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ CNC Server client
-
-
-
-
-
-
-
-
-
-CNC Server client
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ TODO
+
+
+
+
+
+
+
-
-
-
-
-
+
-
-
+
-
+
+
+
-
+
-
+ // Globalize various imports.
+ ['cncserver', 'preview', 'shapes'].forEach(name => {
+ window[name] = eval(name);
+ });
+ // Initialize CNC Server connection details
+ cncserver.init({
+ domain: document.domain,
+ port: location.port ? location.port : 80,
+ protocol: 'http',
+ version: '2',
+ ax: axios,
+ socketio: io,
+ });
+
+
diff --git a/src/interface/lib/cncserver.client.api.js b/src/interface/lib/cncserver.client.api.js
index 9c7f9a02..414a1079 100644
--- a/src/interface/lib/cncserver.client.api.js
+++ b/src/interface/lib/cncserver.client.api.js
@@ -18,6 +18,7 @@ let axios = {}; // Placeholder.
// Initialize wrapper object is this library is being used elsewhere
const cncserver = {
+ contentTimeout: 20000,
init: ({
domain = 'localhost',
port = 4242,
@@ -25,7 +26,7 @@ const cncserver = {
version = 2,
ax,
socketio,
- }) => new Promise((resolve) => {
+ }) => new Promise(resolve => {
axios = ax;
if (socketio) {
cncserver.socket = socketio(`${protocol}://${domain}:${port}`);
@@ -90,6 +91,7 @@ function _request(method, path, options = {}) {
return axios.request({
url,
method,
+ params: options.params,
data: options.data,
timeout: options.timeout || 5000,
});
@@ -140,18 +142,20 @@ cncserver.api = {
},
renderStage: () => _patch('projects', { data: { rendering: true } }),
- drawPreview: () => _patch('projects', { data: { drawing: true } }),
+ startPrinting: () => _patch('projects', { data: { printing: true } }),
schema: () => _options('projects'),
},
content: {
stat: () => _get('content'),
add: {
- direct: data => _post('content', { data }),
+ direct: data => _post('content', { data, timeout: cncserver.contentTimeout }),
local: (type, content, options = {}) => _post('content', {
data: { ...options, source: { type, content } },
+ timeout: cncserver.contentTimeout,
}),
remote: (type, url, options = {}) => _post('content', {
data: { ...options, source: { type, url } },
+ timeout: cncserver.contentTimeout,
}),
},
@@ -166,15 +170,33 @@ cncserver.api = {
colors: {
stat: () => _get('colors'),
preset: preset => _post('colors', { data: { preset } }),
+ deletePreset: preset => _delete('colors', { data: { preset } }),
+ get: id => _get(`colors/${id}`),
add: data => _post('colors', { data }),
- save: data => _put(`colors/${data.id}`, { data }),
- delete: id => _delete(`colors/${id}`),
+ editSet: data => _patch('colors', { data }),
+ save: data => _patch(`colors/${data.id}`, { data }),
+ delete: id => _delete(`colors/${id}/`),
+ schema: () => _options('colors'),
+ },
+ implements: {
+ stat: () => _get('implements'),
+ get: preset => _get(`implements/${preset}`),
+ add: data => _post('implements', { data }),
+ edit: data => _patch('implements', { data }),
+ save: data => _put(`implements/${data.name}/`, { data }),
+ delete: name => _delete(`implements/${name}/`),
+ schema: () => _options('implements'),
},
pen: {
/**
* Get pen stat without doing anything else. Directly sets state.
*/
- stat: () => _get('pen'),
+ stat: (actual = false) => {
+ if (actual) {
+ return _get('pen', { params: { actual: 1 } });
+ }
+ return _get('pen');
+ },
/**
* Set pen position height
@@ -219,7 +241,7 @@ cncserver.api = {
* {x, y} point object of coordinate within 0-100% of canvas to move to,
* or with `abs` key set to 'mm' or 'in' for absolute position.
*/
- move: (point) => {
+ move: point => {
if (typeof point === 'undefined') {
return Promise.reject(new Error('Invalid coordinates for move'));
}
@@ -309,15 +331,15 @@ cncserver.api = {
// Scratch turtle/abstracted API, non-ReSTful.
scratch: {
- move: (direction, amount) => new Promise((success) => {
+ move: (direction, amount) => new Promise(success => {
_get(`/move.${direction}./${amount}`).then(() => {
cncserver.api.scratch.stat().then(success);
});
}),
- stat: () => new Promise((success) => {
+ stat: () => new Promise(success => {
_get('/poll').then(({ data }) => {
const out = {};
- data.split('\n').forEach((item) => {
+ data.split('\n').forEach(item => {
const [key, val] = item.split(' ');
out[key] = val;
});
@@ -450,7 +472,7 @@ cncserver.api = {
_post('batch', {
data: dump,
timeout: 1000 * 60 * 10, // Timeout of 10 mins!
- success: (d) => {
+ success: d => {
console.timeEnd('process-batch');
console.info(d);
callback();
diff --git a/src/interface/modules/components/elements/button-single.mjs b/src/interface/modules/components/elements/button-single.mjs
index 3d0cc3c4..3c126ef1 100644
--- a/src/interface/modules/components/elements/button-single.mjs
+++ b/src/interface/modules/components/elements/button-single.mjs
@@ -4,9 +4,9 @@
import { html } from '/modules/hybrids.js';
export default styles => ({
- title: '',
+ text: '',
icon: '',
- style: 'plain',
+ type: 'plain',
loading: false,
desc: '',
fullwidth: false,
@@ -14,16 +14,27 @@ export default styles => ({
disabled: false,
render: ({
- style, icon, title, desc, fullwidth, active, disabled, loading
+ icon, text, desc, fullwidth, active, disabled, loading, type,
}) => {
const linkClasses = { button: true, 'is-active': active, 'is-loading': loading };
- if (style) linkClasses[`is-${style}`] = true;
+ if (type) linkClasses[`is-${type}`] = true;
+
+ const buttonStyle = { display: fullwidth ? 'flex' : 'inline-block' };
return html`
${styles}
-
- ${icon && html``}
- ${title && html`${title}`}
+
+
+ ${icon && html`
+
+
+
+ `}
+ ${text && html`${text}`}
`;
},
diff --git a/src/interface/modules/components/elements/button-toggle.mjs b/src/interface/modules/components/elements/button-toggle.mjs
index af5a1a6b..6647f282 100644
--- a/src/interface/modules/components/elements/button-toggle.mjs
+++ b/src/interface/modules/components/elements/button-toggle.mjs
@@ -17,19 +17,19 @@ export default styles => ({
fullwidth: false,
onTitle: 'On',
onIcon: 'smile',
- onStyle: 'success',
+ onType: 'success',
offTitle: 'Off',
offIcon: 'frown',
- offStyle: '',
+ offType: '',
render: (props) => {
const word = props.state ? 'on' : 'off';
- const style = props[`${word}Style`];
+ const type = props[`${word}Type`];
const title = props[`${word}Title`];
const icon = props[`${word}Icon`];
const aClasses = { button: true, 'is-fullwidth': props.fullwidth };
- if (style) aClasses[`is-${style}`] = true;
+ if (type) aClasses[`is-${type}`] = true;
const iconClasses = { fas: true };
if (icon) iconClasses[`fa-${icon}`] = true;
diff --git a/src/interface/modules/components/elements/color-set.mjs b/src/interface/modules/components/elements/color-set.mjs
new file mode 100644
index 00000000..8a3672dc
--- /dev/null
+++ b/src/interface/modules/components/elements/color-set.mjs
@@ -0,0 +1,104 @@
+/**
+ * @file Color set/preset display element definition.
+ */
+import { html, svg } from '/modules/hybrids.js';
+
+export default () => ({
+ colors: 'black',
+ labels: 'Black',
+ display: 'round', // 'round', or 'line'.
+ width: 55,
+
+ render: ({ colors, labels, width, display }) => {
+ colors = colors.split(',');
+ labels = labels.split(',');
+
+ const spots = [];
+ let spotWidth = 0;
+ if (display === 'round') {
+ // Build out positions on a circle.
+ const radius = width / 2;
+ spotWidth = colors.length === 1 ? radius : radius * ((Math.PI * 2) / colors.length);
+ colors.forEach((color, index) => {
+ const angle = (Math.PI * 2) * (index / colors.length) - Math.PI;
+
+ const x = Math.round(Math.cos(angle) * radius);
+ const y = Math.round(Math.sin(angle) * radius);
+ const style = {
+ backgroundColor: color,
+ left: `${x + radius - spotWidth / 2}px`,
+ top: `${y + radius - spotWidth / 2}px`,
+ };
+
+ spots.push(html`
+
+ `);
+ });
+ } else if (display === 'line') {
+ spotWidth = width / colors.length;
+ colors.forEach((color, index) => {
+ const style = { backgroundColor: color };
+ spots.push(html`
+
+ `);
+ });
+ }
+
+ return html`
+
+
+
+
+ ${spots.map(spot => spot)}
+
+
+ `;
+ },
+});
diff --git a/src/interface/modules/components/elements/index.mjs b/src/interface/modules/components/elements/index.mjs
index 200791c7..5a8e3640 100644
--- a/src/interface/modules/components/elements/index.mjs
+++ b/src/interface/modules/components/elements/index.mjs
@@ -3,18 +3,32 @@
*/
import TabGroup from './tab-group.mjs';
import TabItem from './tab-item.mjs';
+import SlideGroup from './slide-group.mjs';
+import SlideItem from './slide-item.mjs';
import ButtonToggle from './button-toggle.mjs';
import ButtonSingle from './button-single.mjs';
import LabelTitle from './label-title.mjs';
import MainTitle from './main-title.mjs';
import PaperCanvas from './paper-canvas.mjs';
+import SchemaForm from './schema-form.mjs';
+import ToolImplement from './tool-implement.mjs';
+import ColorSet from './color-set.mjs';
+import NotifyLoading from './notify-loading.mjs';
+import NotifyModal from './notify-modal.mjs';
export default styles => ({
'tab-item': TabItem(styles),
'tab-group': TabGroup(styles),
+ 'slide-item': SlideItem(styles),
+ 'slide-group': SlideGroup(styles),
'button-toggle': ButtonToggle(styles),
'button-single': ButtonSingle(styles),
'label-title': LabelTitle(styles),
'main-title': MainTitle(styles),
'paper-canvas': PaperCanvas(),
+ 'schema-form': SchemaForm(styles),
+ 'tool-implement': ToolImplement(styles),
+ 'color-set': ColorSet(styles),
+ 'notify-loading': NotifyLoading(styles),
+ 'notify-modal': NotifyModal(styles),
});
diff --git a/src/interface/modules/components/elements/label-title.mjs b/src/interface/modules/components/elements/label-title.mjs
index 2e2d206c..ea311d0d 100644
--- a/src/interface/modules/components/elements/label-title.mjs
+++ b/src/interface/modules/components/elements/label-title.mjs
@@ -17,6 +17,15 @@ export default styles => ({
return html`
${styles}
+