add quickjs eval engine for macros
This commit is contained in:
parent
35b76802d1
commit
209f5cae9c
9 changed files with 404 additions and 10 deletions
28
bin/webpack-quickjs-bundle.js
Normal file
28
bin/webpack-quickjs-bundle.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import * as QuickJSModule from "../node_modules/quickjs-emscripten/dist/index.js";
|
||||
|
||||
// Re-export all the named exports
|
||||
export const getQuickJS = QuickJSModule.getQuickJS;
|
||||
export const getQuickJSSync = QuickJSModule.getQuickJSSync;
|
||||
export const newQuickJSWASMModule = QuickJSModule.newQuickJSWASMModule;
|
||||
export const newQuickJSAsyncWASMModule = QuickJSModule.newQuickJSAsyncWASMModule;
|
||||
export const newAsyncRuntime = QuickJSModule.newAsyncRuntime;
|
||||
export const newAsyncContext = QuickJSModule.newAsyncContext;
|
||||
export const DEBUG_SYNC = QuickJSModule.DEBUG_SYNC;
|
||||
export const DEBUG_ASYNC = QuickJSModule.DEBUG_ASYNC;
|
||||
export const RELEASE_SYNC = QuickJSModule.RELEASE_SYNC;
|
||||
export const RELEASE_ASYNC = QuickJSModule.RELEASE_ASYNC;
|
||||
export const errors = QuickJSModule.errors;
|
||||
export const memoizePromiseFactory = QuickJSModule.memoizePromiseFactory;
|
||||
export const Lifetime = QuickJSModule.Lifetime;
|
||||
export const Scope = QuickJSModule.Scope;
|
||||
export const WeakLifetime = QuickJSModule.WeakLifetime;
|
||||
export const StaticLifetime = QuickJSModule.StaticLifetime;
|
||||
export const TestQuickJSWASMModule = QuickJSModule.TestQuickJSWASMModule;
|
||||
export const isFail = QuickJSModule.isFail;
|
||||
export const isSuccess = QuickJSModule.isSuccess;
|
||||
export const assertSync = QuickJSModule.assertSync;
|
||||
export const DeferredPromise = QuickJSModule.DeferredPromise;
|
||||
export const shouldInterruptAfterDeadline = QuickJSModule.shouldInterruptAfterDeadline;
|
||||
|
||||
// Default export
|
||||
export default QuickJSModule;
|
||||
50
bin/webpack-quickjs-esm.js
Normal file
50
bin/webpack-quickjs-esm.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
|
||||
module.exports = {
|
||||
mode: 'production',
|
||||
entry: path.resolve(__dirname, './webpack-quickjs-bundle.js'),
|
||||
output: {
|
||||
path: path.resolve(__dirname, '../src/ext'),
|
||||
filename: 'quickjs.js',
|
||||
library: {
|
||||
type: 'module'
|
||||
},
|
||||
module: true
|
||||
},
|
||||
experiments: {
|
||||
outputModule: true
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.mjs', '.js'],
|
||||
fallback: {
|
||||
'path': false,
|
||||
'fs': false,
|
||||
'crypto': false
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.QTS_DEBUG': JSON.stringify(false),
|
||||
'process.env.NODE_ENV': JSON.stringify('production')
|
||||
})
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.m?js$/,
|
||||
resolve: {
|
||||
fullySpecified: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
optimization: {
|
||||
minimize: false
|
||||
},
|
||||
performance: {
|
||||
hints: false,
|
||||
maxAssetSize: 2 * 1024 * 1024,
|
||||
maxEntrypointSize: 2 * 1024 * 1024,
|
||||
},
|
||||
};
|
||||
|
|
@ -60,6 +60,7 @@
|
|||
"mqtt": "^5.10.3",
|
||||
"npm": "^11.4.2",
|
||||
"prettier": "^3.5.3",
|
||||
"quickjs-emscripten": "^0.20.0",
|
||||
"react-responsive-carousel": "^3.2.23",
|
||||
"serve-static": "^2.2.0",
|
||||
"three": "^0.182.0",
|
||||
|
|
@ -120,7 +121,8 @@
|
|||
"start-ddb": "npm run prebuild && electron . --devel --debugg",
|
||||
"start-dev": "npm run prebuild prod && electron . --devel",
|
||||
"start": "npm run prebuild && electron .",
|
||||
"webpack-ext": "npm run webpack-three && npm run webpack-zip",
|
||||
"webpack-ext": "npm run webpack-three && npm run webpack-zip && npm run webpack-qjs",
|
||||
"webpack-qjs": "npx webpack --config bin/webpack-quickjs-esm.js",
|
||||
"webpack-src": "node bin/esbuild.config.mjs",
|
||||
"webpack-three": "npx webpack --config bin/webpack-three-esm.js",
|
||||
"webpack-zip": "npx webpack --config bin/webpack-jszip-esm.js"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { arcToPath } from '../../geo/paths.js';
|
||||
import { consts } from './consts.js';
|
||||
import { createVM } from '../../moto/quickjs.js';
|
||||
import { newPoint } from '../../geo/point.js';
|
||||
import { util } from '../../geo/base.js';
|
||||
|
||||
|
|
@ -24,6 +25,7 @@ class Print {
|
|||
this.tools = {};
|
||||
// set to 1 to enable flow rate analysis (console)
|
||||
this.debugE = settings ? (settings.controller.devel ? 1 : 0) : 0;
|
||||
this._ready = this.createSafeEval();
|
||||
}
|
||||
|
||||
setType(type) {
|
||||
|
|
@ -237,7 +239,26 @@ class Print {
|
|||
return output.last().point;
|
||||
}
|
||||
|
||||
async ready() {
|
||||
await this._ready;
|
||||
}
|
||||
|
||||
async createSafeEval() {
|
||||
this.safeEval = await createVM();
|
||||
this.safeEval.eval("function range(a,b) { return (a + (layer / layers) * (b-a)) }");
|
||||
}
|
||||
|
||||
disposeSafeEval() {
|
||||
if (this.safeEval) {
|
||||
this.safeEval.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
constReplace(str, consts, start, pad, short) {
|
||||
let safeEval = this.safeEval;
|
||||
if (safeEval) {
|
||||
safeEval.setContext(consts);
|
||||
}
|
||||
function tryeval(str) {
|
||||
try {
|
||||
return eval(`{ ${str} }`)
|
||||
|
|
@ -269,7 +290,7 @@ class Print {
|
|||
}
|
||||
eva.push(`function range(a,b) { return (a + (layer / layers) * (b-a)).round(4) }`);
|
||||
eva.push(`try {( ${tok} )} catch (e) {console.log(e);0}`);
|
||||
let evl = tryeval(eva.join(''));
|
||||
let evl = safeEval ? safeEval.eval(tok) : tryeval(eva.join(''));
|
||||
nutok = evl;
|
||||
if (pad === 666) {
|
||||
return evl;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ export async function cam_prepare(widgets, settings, update) {
|
|||
const print = self.kiri_worker.current.print = newPrint(settings, active);
|
||||
const { origin } = settings;
|
||||
|
||||
// wait for safe eval setup
|
||||
await print.ready();
|
||||
|
||||
// cam-specific storage
|
||||
print.output = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@ export async function fdm_prepare(widgets, settings, update) {
|
|||
output = [],
|
||||
layerout = [];
|
||||
|
||||
// wait for safe eval setup
|
||||
await print.ready();
|
||||
|
||||
// compute bounds if missing
|
||||
if (!bounds) {
|
||||
bounds = new THREE.Box3();
|
||||
|
|
|
|||
|
|
@ -58,6 +58,13 @@ function debug() {
|
|||
console.log(...arguments);
|
||||
}
|
||||
|
||||
function setPrint(print) {
|
||||
if (current.print && current.print !== print) {
|
||||
current.print.disposeSafeEval();
|
||||
}
|
||||
return current.print = print;
|
||||
}
|
||||
|
||||
// catch clipper alerts and convert to console messages
|
||||
self.alert = function(o) {
|
||||
console.log(o);
|
||||
|
|
@ -66,7 +73,7 @@ self.alert = function(o) {
|
|||
self.uuid = ((Math.random() * Date.now()) | 0).toString(36);
|
||||
|
||||
/**
|
||||
* @returns {RasterPath}
|
||||
* @returns {RasterPath} instantiated class
|
||||
*/
|
||||
self.get_raster_gpu = async function({ mode, resolution, rotationStep }) {
|
||||
let gpu = new RasterPath({
|
||||
|
|
@ -314,7 +321,7 @@ const dispatch = {
|
|||
// purge all sync data
|
||||
clear(data, send) {
|
||||
// current.snap = null;
|
||||
current.print = null;
|
||||
setPrint(null);
|
||||
dispatch.group = wgroup = {};
|
||||
dispatch.cache = worker.cache = wcache = {};
|
||||
Widget.Groups.clear();
|
||||
|
|
@ -427,7 +434,7 @@ const dispatch = {
|
|||
|
||||
let last = time(), now;
|
||||
|
||||
current.print = null;
|
||||
setPrint(null);
|
||||
current.mode = settings.mode.toUpperCase();
|
||||
|
||||
widget.anno = data.anno || widget.anno;
|
||||
|
|
@ -496,7 +503,7 @@ const dispatch = {
|
|||
send.data(emit, state.zeros);
|
||||
}).then(() => {
|
||||
const unitScale = settings.controller.units === 'in' ? (1 / 25.4) : 1;
|
||||
const print = current.print || {};
|
||||
const print = setPrint(current.print || {});
|
||||
const minSpeed = (print.minSpeed || 0) * unitScale;
|
||||
const maxSpeed = (print.maxSpeed || 0) * unitScale;
|
||||
|
||||
|
|
@ -571,7 +578,7 @@ const dispatch = {
|
|||
z: origin.z - (process.camOriginOffZ ?? 0)
|
||||
};
|
||||
const device = settings.device;
|
||||
const print = current.print = newPrint(settings, Object.values(wcache));
|
||||
const print = setPrint(newPrint(settings, Object.values(wcache)));
|
||||
const tools = device.extruders;
|
||||
const mode = settings.mode;
|
||||
const thin = settings.controller.lineType === 'line' || mode !== 'FDM';
|
||||
|
|
@ -601,7 +608,7 @@ const dispatch = {
|
|||
out.point = newPoint(x,y,z || 0);
|
||||
});
|
||||
});
|
||||
const print = current.print = newPrint(null, Object.values(wcache));
|
||||
const print = setPrint(newPrint(null, Object.values(wcache)));
|
||||
render.path(parsed, progress => {
|
||||
send.data({ progress });
|
||||
}, { thin: true })
|
||||
|
|
|
|||
280
src/moto/quickjs.js
Normal file
280
src/moto/quickjs.js
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
/**
|
||||
* QuickJS VM Wrapper
|
||||
* Provides a simplified interface for working with QuickJS contexts
|
||||
*/
|
||||
|
||||
import { getQuickJS } from '../ext/quickjs.js';
|
||||
|
||||
let quickJSInstance = null;
|
||||
|
||||
/**
|
||||
* Get or initialize the QuickJS singleton instance
|
||||
*/
|
||||
async function getQuickJSInstance() {
|
||||
if (!quickJSInstance) {
|
||||
quickJSInstance = await getQuickJS();
|
||||
}
|
||||
return quickJSInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* QuickJS VM wrapper with helper methods
|
||||
* Automatically manages a QuickJS context lifecycle
|
||||
*/
|
||||
class QuickJSVM {
|
||||
/**
|
||||
* Create a new QuickJS VM context
|
||||
* @param {Object} options - Optional VM configuration
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
this.vm = null;
|
||||
this.disposed = false;
|
||||
this.initPromise = this._init(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal initialization (async)
|
||||
*/
|
||||
async _init(options) {
|
||||
const QuickJS = await getQuickJSInstance();
|
||||
this.vm = QuickJS.newContext(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure VM is initialized before use
|
||||
*/
|
||||
async ready() {
|
||||
await this.initPromise;
|
||||
if (this.disposed) {
|
||||
throw new Error('VM has been disposed');
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set multiple variables in the global context
|
||||
* @param {Object} obj - Object with key/value pairs to set as globals
|
||||
* @example
|
||||
* vm.setContext({
|
||||
* x: 123,
|
||||
* config: { width: 800, height: 600 },
|
||||
* items: [1, 2, 3]
|
||||
* });
|
||||
*/
|
||||
setContext(obj) {
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const handle = this.jsToVm(value);
|
||||
this.vm.setProp(this.vm.global, key, handle);
|
||||
handle.dispose();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a single global variable
|
||||
* @param {string} name - Variable name
|
||||
* @param {*} value - JavaScript value to set
|
||||
* @example
|
||||
* vm.set('x', 123);
|
||||
* vm.set('config', { width: 800 });
|
||||
*/
|
||||
set(name, value) {
|
||||
const handle = this.jsToVm(value);
|
||||
this.vm.setProp(this.vm.global, name, handle);
|
||||
handle.dispose();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a global variable value
|
||||
* @param {string} name - Variable name
|
||||
* @returns {*} JavaScript value
|
||||
* @example
|
||||
* const x = vm.get('x'); // 123
|
||||
*/
|
||||
get(name) {
|
||||
const handle = this.vm.getProp(this.vm.global, name);
|
||||
const value = this.vm.dump(handle);
|
||||
handle.dispose();
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert JavaScript value to QuickJS handle
|
||||
* @param {*} value - JavaScript value
|
||||
* @returns {QuickJSHandle} QuickJS handle (caller must dispose)
|
||||
*/
|
||||
jsToVm(value) {
|
||||
// Handle primitives
|
||||
if (value === null) {
|
||||
return this.vm.null;
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
return this.vm.undefined;
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return this.vm.newNumber(value);
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return this.vm.newString(value);
|
||||
}
|
||||
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? this.vm.true : this.vm.false;
|
||||
}
|
||||
|
||||
// Handle arrays
|
||||
if (Array.isArray(value)) {
|
||||
const arr = this.vm.newArray();
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const itemHandle = this.jsToVm(value[i]);
|
||||
this.vm.setProp(arr, i, itemHandle);
|
||||
itemHandle.dispose();
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
// Handle objects
|
||||
if (typeof value === 'object') {
|
||||
const obj = this.vm.newObject();
|
||||
for (const [k, v] of Object.entries(value)) {
|
||||
const propHandle = this.jsToVm(v);
|
||||
this.vm.setProp(obj, k, propHandle);
|
||||
propHandle.dispose();
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Fallback for unsupported types
|
||||
console.warn(`Unsupported type for VM: ${typeof value}, setting to undefined`);
|
||||
return this.vm.undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate JavaScript code in the VM
|
||||
* @param {string} code - JavaScript code to evaluate
|
||||
* @returns {*} Result value or throws error
|
||||
* @example
|
||||
* const result = vm.eval('1 + 2'); // 3
|
||||
*/
|
||||
eval(code) {
|
||||
const result = this.vm.evalCode(code);
|
||||
|
||||
if (result.error) {
|
||||
const error = this.vm.dump(result.error);
|
||||
result.error.dispose();
|
||||
throw new Error(`VM Error: ${error}`);
|
||||
}
|
||||
|
||||
const value = this.vm.dump(result.value);
|
||||
result.value.dispose();
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate code and return raw result handle (caller must dispose)
|
||||
* @param {string} code - JavaScript code to evaluate
|
||||
* @returns {Object} { error?: handle, value?: handle }
|
||||
*/
|
||||
evalRaw(code) {
|
||||
return this.vm.evalCode(code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JavaScript function that can be called from VM code
|
||||
* @param {string} name - Function name in VM
|
||||
* @param {Function} fn - JavaScript function to wrap
|
||||
* @example
|
||||
* vm.setFunction('add', (a, b) => a + b);
|
||||
* vm.eval('add(2, 3)'); // 5
|
||||
*/
|
||||
setFunction(name, fn) {
|
||||
const fnHandle = this.vm.newFunction(name, (...args) => {
|
||||
// Convert VM args to JS
|
||||
const jsArgs = args.map(arg => this.vm.dump(arg));
|
||||
|
||||
// Call JS function
|
||||
const result = fn(...jsArgs);
|
||||
|
||||
// Convert result back to VM
|
||||
return this.jsToVm(result);
|
||||
});
|
||||
|
||||
this.vm.setProp(this.vm.global, name, fnHandle);
|
||||
fnHandle.dispose();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an object with JSON-like structure
|
||||
* @param {Object} data - JavaScript object to convert
|
||||
* @returns {QuickJSHandle} Handle to VM object (caller must dispose)
|
||||
*/
|
||||
newJSON(data) {
|
||||
return this.vm.unwrapResult(
|
||||
this.vm.evalCode(`(${JSON.stringify(data)})`)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a function in the VM with arguments
|
||||
* @param {string} funcName - Function name in VM
|
||||
* @param {...*} args - Arguments to pass
|
||||
* @returns {*} Result value
|
||||
* @example
|
||||
* vm.eval('function add(a, b) { return a + b; }');
|
||||
* vm.call('add', 2, 3); // 5
|
||||
*/
|
||||
call(funcName, ...args) {
|
||||
const argsStr = args.map(a => JSON.stringify(a)).join(', ');
|
||||
return this.eval(`${funcName}(${argsStr})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if VM has been disposed
|
||||
*/
|
||||
isDisposed() {
|
||||
return this.disposed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get direct access to underlying QuickJS context
|
||||
* Use with caution - you're responsible for handle management
|
||||
*/
|
||||
getContext() {
|
||||
return this.vm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose the VM and free all resources
|
||||
* Must be called when done to prevent memory leaks
|
||||
*/
|
||||
dispose() {
|
||||
if (!this.disposed && this.vm) {
|
||||
this.vm.dispose();
|
||||
this.disposed = true;
|
||||
this.vm = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and initialize a new QuickJS VM
|
||||
* @param {Object} options - Optional VM configuration
|
||||
* @returns {Promise<QuickJSVM>} Initialized VM instance
|
||||
* @example
|
||||
* const vm = await createVM();
|
||||
* vm.set('x', 123);
|
||||
* const result = vm.eval('x * 2'); // 246
|
||||
* vm.dispose();
|
||||
*/
|
||||
async function createVM(options = {}) {
|
||||
const vm = new QuickJSVM(options);
|
||||
await vm.ready();
|
||||
return vm;
|
||||
}
|
||||
|
||||
export { QuickJSVM, createVM };
|
||||
|
|
@ -22,8 +22,8 @@
|
|||
<link rel="stylesheet" type="text/css" href="index.css">
|
||||
<link href="manifest.json" rel="manifest">
|
||||
<link href="../font/css/all.min.css" rel="stylesheet">
|
||||
<!-- <script type="module" src="/lib/kiri/run/worker.js"></script>
|
||||
<script type="module" src="/lib/kiri/run/minion.js"></script> -->
|
||||
<!-- <script type="module" src="/lib/kiri/run/worker.js"></script> -->
|
||||
<!-- <script type="module" src="/lib/kiri/run/minion.js"></script> -->
|
||||
<script src="../lib/ext/tween.js"></script>
|
||||
<script src="../font/js/all.min.js" crossorigin="anonymous" defer></script>
|
||||
<script src="../lib/main/kiri.js" type="module"></script>
|
||||
|
|
|
|||
Loading…
Reference in a new issue