Merge branch 'rel-4.6'
This commit is contained in:
commit
c6c9846b96
184 changed files with 15553 additions and 9433 deletions
10
.gitignore
vendored
10
.gitignore
vendored
|
|
@ -1,5 +1,7 @@
|
||||||
|
*.pem
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.bache
|
.bcache
|
||||||
|
.claude
|
||||||
.docusaurus
|
.docusaurus
|
||||||
.env
|
.env
|
||||||
alt
|
alt
|
||||||
|
|
@ -7,12 +9,14 @@ app.json
|
||||||
build
|
build
|
||||||
data
|
data
|
||||||
dist
|
dist
|
||||||
jszip-esm.js
|
|
||||||
logs
|
logs
|
||||||
mod
|
mod
|
||||||
node_modules
|
node_modules
|
||||||
pack
|
pack
|
||||||
package-lock.json
|
package-lock.json
|
||||||
three.js
|
src/ext/jszip-esm.js
|
||||||
|
src/ext/quickjs.js
|
||||||
|
src/ext/three.js
|
||||||
tmp
|
tmp
|
||||||
|
tmp_*/
|
||||||
web/boot/bundle*
|
web/boot/bundle*
|
||||||
|
|
|
||||||
11
app.js
11
app.js
|
|
@ -579,16 +579,11 @@ function cookieValue(cookie,key) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function addAppHeaders(req, res, next) {
|
function addAppHeaders(req, res, next) {
|
||||||
if ([
|
if (req.app.path.indexOf("/lib") === 0) {
|
||||||
|
addCorsHeaders(req, res);
|
||||||
|
} else if ([
|
||||||
"/kiri/",
|
"/kiri/",
|
||||||
"/mesh/",
|
"/mesh/",
|
||||||
"/lib/gpu/raster.js",
|
|
||||||
"/lib/gpu/raster-worker.js",
|
|
||||||
"/lib/mesh/work.js",
|
|
||||||
"/lib/kiri/run/worker.js",
|
|
||||||
"/lib/kiri/run/minion.js",
|
|
||||||
"/lib/kiri/run/engine.js",
|
|
||||||
"/lib/kiri/run/frame.js"
|
|
||||||
].indexOf(req.app.path) >= 0) {
|
].indexOf(req.app.path) >= 0) {
|
||||||
addCorsHeaders(req, res);
|
addCorsHeaders(req, res);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -4,58 +4,119 @@ description: Keyboard Shortcuts and Mouse Controls
|
||||||
|
|
||||||
# Controls
|
# Controls
|
||||||
|
|
||||||
| Key | Action | Notes |
|
## Layer Navigation
|
||||||
| ---------------- | ----------------------------------------- | --------------------------------------- |
|
|
||||||
| e | Device Dialog | Select and customize devices |
|
| Key | Action | Notes |
|
||||||
| o | CNC Tool Dialog | CNC mode only |
|
| --- | ------ | ----- |
|
||||||
| c | Grid.Local Dialog | Find GridBot devices on your network |
|
| ` (backtick) | Show layer 0 | First layer |
|
||||||
| q | Preference Dialog | Change application behaviors |
|
| 1-9 | Show layers at 10-90% | In 10% increments |
|
||||||
| l | Work Profile Dialog | Load, Save, Rename work profiles |
|
| 0 | Show all layers (100%) | Show maximum layer |
|
||||||
| | | |
|
| Meta+Up | Next layer | Navigate up one layer |
|
||||||
| a | Arrange | Switch to layout view from other views |
|
| Meta+Down | Previous layer | Navigate down one layer |
|
||||||
| | | or perform an automatic layout when |
|
| [shift + wheel] | Cycle through layers | Preview mode only |
|
||||||
| | | already in layout view |
|
| v | Toggle single slice view | Preview mode: toggle single vs range |
|
||||||
| s | Slice Object | Slice workspace objects |
|
|
||||||
| p | Preview Paths | Do route planning for sliced objects |
|
## View Navigation
|
||||||
| g | CNC Animation | Enter milling animation mode |
|
|
||||||
| x | Export Paths as GCode, SVG, DXF | Produce downloadable files |
|
| Key | Action | Notes |
|
||||||
| | | |
|
| --- | ------ | ----- |
|
||||||
| i | File Import Dialog | |
|
| h | Home View | Default 45° view |
|
||||||
| r | Recent Files Dialog | |
|
| t | Top View | Top-down view |
|
||||||
| | | |
|
| f | Front View | Direct front side-view |
|
||||||
| h | Home View | Default 45 degree view of objects |
|
| b | Back View | Direct back side-view |
|
||||||
| t | Top View | Top-down view of objects |
|
| z | Reset View | Default zoom and home |
|
||||||
| f | Front View | Direct side-view (front) |
|
| F | Fit View to Contents | Zoom to fit all objects |
|
||||||
| z | Reset View | Default zoom and Home |
|
| `<` | Previous Side View | Cycle: left → back → right → front |
|
||||||
| < > | Cycle Side Views | Front, Right, Back, Left |
|
| `>` | Next Side View | Cycle: front → right → back → left |
|
||||||
| v | Focus on Selection (arrange mode) | Camera points to object center |
|
| v | Focus on Selection | Arrange mode: center camera on object |
|
||||||
| v | Toggle Single Slice View (preview mode) | Toggle single vs range of slices |
|
|
||||||
| | | |
|
## File Operations
|
||||||
| \~,1-9,0 | Select layer in 10% increments | Slice and Preview modes |
|
|
||||||
| | | |
|
| Key | Action | Notes |
|
||||||
| d | Duplicate Selected Objects | Create a copy of selected objects |
|
| --- | ------ | ----- |
|
||||||
| m | Mirror Selected Objects | Mirror vertices of selected objects |
|
| i | File Import Dialog | Import 3D models |
|
||||||
| \[delete] | Delete Selected Objects | Remove selected object from workspace |
|
| r | Recent Files Dialog | Open recently used files |
|
||||||
| \[arrow] | Rotate 90 degrees | Rotate selected object |
|
|
||||||
| \[shift + arrow] | Rotate 5 degrees | Rotate selected object |
|
## Workflow Actions
|
||||||
| \[ctrl + a] | Select All Objects | Select all objects |
|
|
||||||
| | | |
|
| Key | Action | Notes |
|
||||||
| \[shit + click] | Toggle Selection | Select or Deselect a single object |
|
| --- | ------ | ----- |
|
||||||
| \[shift + wheel] | Cycle through layers (preview mode) | Use mouse wheel as range slider |
|
| s / S | Slice Object | Process workspace objects |
|
||||||
| \[meta + drag] | Pan View | Move focus of camera |
|
| p / P | Preview Paths | Route planning (hidden in SLA mode) |
|
||||||
| \[right + drag] | Pan View | |
|
| g | CNC Animation | Enter milling animation mode (CNC only) |
|
||||||
| \[left + drag] | Rotate | Move camera around a focal point |
|
| x / X | Export | Export as GCode, SVG, or DXF |
|
||||||
| \[mouse wheel] | Zoom | Move camera closer / farther from focus |
|
| a | Arrange | Switch to ARRANGE view, or auto-layout if already in ARRANGE |
|
||||||
| \[middle + drag] | Zoom | |
|
|
||||||
| | | |
|
## Object Manipulation
|
||||||
| \[ctrl + click] | Align face of selected object to platform | Rotate object face toward work surface |
|
|
||||||
| \[meta + click] | Align face of selected object to platform | |
|
| Key | Action | Notes |
|
||||||
| | | |
|
| --- | ------ | ----- |
|
||||||
| \[shift + z] | Reset all preferences to stock | Reload page afterward |
|
| d | Duplicate Selected | Create copy of selected objects |
|
||||||
| | | |
|
| m | Mirror Selected | Mirror vertices of selected objects |
|
||||||
| **Mouse** | **FDM Support Controls** | **Version Notes** |
|
| O | Manual Rotation Input | Enter rotation values manually |
|
||||||
| \[left click] | Toggle add / remove column | |
|
| [delete] | Delete Selected | Remove selected objects |
|
||||||
| \[ctrl + drag] | Erase support columns | 2.7+ |
|
| [ctrl + a] / [cmd + a] | Select All | Select all objects in workspace |
|
||||||
| \[cmd + drag] | Erase support columns | 2.7+ |
|
| [shift + click] | Toggle Selection | Select or deselect individual object |
|
||||||
| \[opt + drag] | Draw on support columns | 2.7+ |
|
|
||||||
| \[alt + drag] | Draw on support columns | 2.7+ |
|
## Rotation & Movement
|
||||||
|
|
||||||
|
| Key | Action | Notes |
|
||||||
|
| --- | ------ | ----- |
|
||||||
|
| [arrow] | Rotate 90° | Arrow keys rotate on respective axes |
|
||||||
|
| [shift + arrow] | Rotate 5° | Fine rotation control |
|
||||||
|
| [alt + arrow] | Move 5mm | Move object in X/Y plane |
|
||||||
|
|
||||||
|
## Rendering Modes
|
||||||
|
|
||||||
|
| Key | Action | Notes |
|
||||||
|
| --- | ------ | ----- |
|
||||||
|
| w | Toggle Ghost Rendering | Cycle between solid and semi-transparent |
|
||||||
|
| W | Toggle Wireframe | Cycle between solid and wireframe |
|
||||||
|
| Ctrl+W / Cmd+W | Toggle Edge Rendering | Show/hide model edges |
|
||||||
|
|
||||||
|
## Settings & Dialogs
|
||||||
|
|
||||||
|
| Key | Action | Notes |
|
||||||
|
| --- | ------ | ----- |
|
||||||
|
| e | Device Dialog | Select and customize devices |
|
||||||
|
| o | Tool Dialog | CNC mode only |
|
||||||
|
| q | Preferences Dialog | Change application behaviors |
|
||||||
|
| l | Work Profile Dialog | Load, save, rename work profiles |
|
||||||
|
| ? | Help Dialog | Show help and documentation |
|
||||||
|
| C | Refresh Catalog | Reload device catalog |
|
||||||
|
|
||||||
|
## Workspace Management
|
||||||
|
|
||||||
|
| Key | Action | Notes |
|
||||||
|
| --- | ------ | ----- |
|
||||||
|
| Ctrl+S | Save Settings | Save current settings |
|
||||||
|
| Cmd+S | Save Workspace | Save workspace state (macOS) |
|
||||||
|
| Cmd+L | Restore Workspace | Restore saved workspace (macOS) |
|
||||||
|
| Z | Reset All Preferences | Clear all settings (requires confirmation) |
|
||||||
|
|
||||||
|
## Mouse Controls
|
||||||
|
|
||||||
|
### General Navigation
|
||||||
|
|
||||||
|
| Input | Action | Notes |
|
||||||
|
| ----- | ------ | ----- |
|
||||||
|
| [left + drag] | Rotate View | Orbit camera around focus point |
|
||||||
|
| [right + drag] | Pan View | Move camera focus |
|
||||||
|
| [meta + drag] | Pan View | Alternative pan (macOS) |
|
||||||
|
| [middle + drag] | Zoom | Mouse wheel click + drag |
|
||||||
|
| [mouse wheel] | Zoom | Scroll to zoom in/out |
|
||||||
|
|
||||||
|
### Object Interaction
|
||||||
|
|
||||||
|
| Input | Action | Notes |
|
||||||
|
| ----- | ------ | ----- |
|
||||||
|
| [ctrl + click] | Lay Flat | Rotate clicked face toward platform |
|
||||||
|
| [meta + click] | Lay Flat | Alternative (macOS) |
|
||||||
|
|
||||||
|
### FDM Support Mode
|
||||||
|
|
||||||
|
| Input | Action | Notes |
|
||||||
|
| ----- | ------ | ----- |
|
||||||
|
| [left click] | Toggle Support Column | Add or remove support column |
|
||||||
|
| [ctrl + drag] / [cmd + drag] | Erase Columns | Remove support columns |
|
||||||
|
| [alt + drag] / [opt + drag] | Draw Columns | Add support columns |
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
self.kiri.load(api => {
|
self.kiri.load(api => {
|
||||||
|
|
||||||
console.log('BAMBU MODULE RUNNING');
|
|
||||||
|
|
||||||
const { uc: ui } = api;
|
const { uc: ui } = api;
|
||||||
const { $, h } = api.web;
|
const { $, h } = api.web;
|
||||||
const defams = ";; DEFINE BAMBU-AMS ";
|
const defams = ";; DEFINE BAMBU-AMS ";
|
||||||
|
|
@ -1232,7 +1230,7 @@ self.kiri.load(api => {
|
||||||
if (!bound) {
|
if (!bound) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (devsel.extras?.bbl && !api.ui.deviceSave.disabled) {
|
if (devsel.extras?.bbl && !api.ui.devices.save.disabled) {
|
||||||
device = devsel;
|
device = devsel;
|
||||||
printers = devsel.extras.bbl;
|
printers = devsel.extras.bbl;
|
||||||
bound.bblman.classList.remove('hide');
|
bound.bblman.classList.remove('hide');
|
||||||
|
|
@ -1255,7 +1253,7 @@ self.kiri.load(api => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function prep_export(gen3mf, gcode, info, settings) {
|
function prep_export(gen3mf, gcode, info, settings) {
|
||||||
if (!settings.device.extras?.bbl || api.ui.deviceSave.disabled) {
|
if (!settings.device.extras?.bbl || api.ui.devices.save.disabled) {
|
||||||
$('bambu-output').style.display = 'none';
|
$('bambu-output').style.display = 'none';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1327,4 +1325,4 @@ self.kiri.load(api => {
|
||||||
setInterval(monitor_keepalive, 5000);
|
setInterval(monitor_keepalive, 5000);
|
||||||
|
|
||||||
let bblapi = api.bambu = { send, prep_export };
|
let bblapi = api.bambu = { send, prep_export };
|
||||||
});
|
}, 'Bambu');
|
||||||
|
|
|
||||||
|
|
@ -20,4 +20,4 @@ self.kiri.load(api => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
}, 'Developer');
|
||||||
|
|
|
||||||
16
package.json
16
package.json
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "grid-apps",
|
"name": "grid-apps",
|
||||||
"version": "4.5.0",
|
"version": "4.6.0",
|
||||||
"description": "grid.space 3d slicing & modeling tools",
|
"description": "grid.space 3d slicing & modeling tools",
|
||||||
"author": "Stewart Allen <sa@grid.space>",
|
"author": "Stewart Allen <sa@grid.space>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
@ -35,12 +35,12 @@
|
||||||
"node": ">=22.0.0"
|
"node": ">=22.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@docusaurus/plugin-client-redirects": "^3.9.0",
|
"@docusaurus/plugin-client-redirects": "3.9.1",
|
||||||
"@fortawesome/fontawesome-free": "^6.1.1",
|
"@fortawesome/fontawesome-free": "^6.1.1",
|
||||||
"@gridspace/app-server": "^0.0.20",
|
"@gridspace/app-server": "^0.0.20",
|
||||||
"@gridspace/basic-ftp": "github:gridspace/basic-ftp#v5.0.5-gridspace",
|
"@gridspace/basic-ftp": "github:gridspace/basic-ftp#v5.0.5-gridspace",
|
||||||
"@gridspace/net-level-client": "^0.2.3",
|
"@gridspace/net-level-client": "^0.2.3",
|
||||||
"@gridspace/raster-path": "^1.0.8",
|
"@gridspace/raster-path": "^1.1.1",
|
||||||
"@tracespace/parser": "^5.0.0-next.0",
|
"@tracespace/parser": "^5.0.0-next.0",
|
||||||
"@tweenjs/tween.js": "^16.6.0",
|
"@tweenjs/tween.js": "^16.6.0",
|
||||||
"aedes": "^0.51.3",
|
"aedes": "^0.51.3",
|
||||||
|
|
@ -60,6 +60,7 @@
|
||||||
"mqtt": "^5.10.3",
|
"mqtt": "^5.10.3",
|
||||||
"npm": "^11.4.2",
|
"npm": "^11.4.2",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
|
"quickjs-emscripten": "^0.20.0",
|
||||||
"react-responsive-carousel": "^3.2.23",
|
"react-responsive-carousel": "^3.2.23",
|
||||||
"serve-static": "^2.2.0",
|
"serve-static": "^2.2.0",
|
||||||
"three": "^0.182.0",
|
"three": "^0.182.0",
|
||||||
|
|
@ -69,8 +70,8 @@
|
||||||
"ws": "^8.18.2"
|
"ws": "^8.18.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@docusaurus/core": "^3.9.0",
|
"@docusaurus/core": "3.9.1",
|
||||||
"@docusaurus/preset-classic": "^3.9.0",
|
"@docusaurus/preset-classic": "3.9.1",
|
||||||
"@electron/notarize": "^3.0.0",
|
"@electron/notarize": "^3.0.0",
|
||||||
"copy-webpack-plugin": "^12.0.2",
|
"copy-webpack-plugin": "^12.0.2",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
|
|
@ -104,7 +105,7 @@
|
||||||
"docs-build": "docusaurus build --config conf/docusaurus.config.js",
|
"docs-build": "docusaurus build --config conf/docusaurus.config.js",
|
||||||
"docs-check": "prettier --config ./conf/prettier.config.js ./docs --check",
|
"docs-check": "prettier --config ./conf/prettier.config.js ./docs --check",
|
||||||
"docs-dev": "docusaurus start --config conf/docusaurus.config.js",
|
"docs-dev": "docusaurus start --config conf/docusaurus.config.js",
|
||||||
"docs-serve": "docusaurus serve --config conf/docusaurus.config.js",
|
"docs-serve": "docusaurus serve --config conf/docusaurus.config.js --port 4004",
|
||||||
"dryrun": "gs-app-server --debug --single --dryrun",
|
"dryrun": "gs-app-server --debug --single --dryrun",
|
||||||
"dryrun-prod": "gs-app-server --single --dryrun",
|
"dryrun-prod": "gs-app-server --single --dryrun",
|
||||||
"mac-verify": "spctl --assess -vv --type install dist/*/*.app",
|
"mac-verify": "spctl --assess -vv --type install dist/*/*.app",
|
||||||
|
|
@ -120,7 +121,8 @@
|
||||||
"start-ddb": "npm run prebuild && electron . --devel --debugg",
|
"start-ddb": "npm run prebuild && electron . --devel --debugg",
|
||||||
"start-dev": "npm run prebuild prod && electron . --devel",
|
"start-dev": "npm run prebuild prod && electron . --devel",
|
||||||
"start": "npm run prebuild && electron .",
|
"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-src": "node bin/esbuild.config.mjs",
|
||||||
"webpack-three": "npx webpack --config bin/webpack-three-esm.js",
|
"webpack-three": "npx webpack --config bin/webpack-three-esm.js",
|
||||||
"webpack-zip": "npx webpack --config bin/webpack-jszip-esm.js"
|
"webpack-zip": "npx webpack --config bin/webpack-jszip-esm.js"
|
||||||
|
|
|
||||||
|
|
@ -676,8 +676,8 @@ export const config = {
|
||||||
// default # of decimal places in generated gcode
|
// default # of decimal places in generated gcode
|
||||||
gcode_decimals: 4,
|
gcode_decimals: 4,
|
||||||
// heal disjoint polygons in slicing (experimental)
|
// heal disjoint polygons in slicing (experimental)
|
||||||
bridgeLineGapDistance: 0.05,
|
bridgeLineGapDistance: 0.05, // may no longer be used
|
||||||
bridgeLineGapDistanceMax: 25,
|
bridgeLineGapDistanceMax: 3,
|
||||||
// Bounds default margin nearTo
|
// Bounds default margin nearTo
|
||||||
// Polygon.offset mindist2 offset precision
|
// Polygon.offset mindist2 offset precision
|
||||||
precision_offset: 0.05,
|
precision_offset: 0.05,
|
||||||
|
|
|
||||||
101
src/geo/paths.js
101
src/geo/paths.js
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import { base } from './base.js';
|
import { base } from './base.js';
|
||||||
import { newPoint } from './point.js';
|
import { newPoint } from './point.js';
|
||||||
|
import { newPolygon } from './polygon.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emit each element in array based on next closest endpoint
|
* Emit each element in array based on next closest endpoint
|
||||||
|
|
@ -18,23 +19,26 @@ export function tip2tipEmit(array, startPoint, emitter) {
|
||||||
for (;;) {
|
for (;;) {
|
||||||
found = null;
|
found = null;
|
||||||
mindist = Infinity;
|
mindist = Infinity;
|
||||||
array.forEach(function (el) {
|
for (let el of array) {
|
||||||
if (el.delete) return;
|
if (el.delete) continue;
|
||||||
dist = startPoint.distTo2D(el.first);
|
dist = startPoint.distTo2D(el.first);
|
||||||
if (dist < mindist) {
|
if (dist < mindist) {
|
||||||
found = { el: el, first: el.first, last: el.last };
|
found = { el: el, first: el.first, last: el.last };
|
||||||
mindist = dist;
|
mindist = dist;
|
||||||
}
|
}
|
||||||
|
// when heads is set, it's first to first
|
||||||
|
// not first/last to closest first/last
|
||||||
|
if (el.heads) continue;
|
||||||
dist = startPoint.distTo2D(el.last);
|
dist = startPoint.distTo2D(el.last);
|
||||||
if (dist < mindist) {
|
if (dist < mindist) {
|
||||||
found = { el: el, first: el.last, last: el.first };
|
found = { el: el, first: el.last, last: el.first };
|
||||||
mindist = dist;
|
mindist = dist;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
if (found) {
|
if (found) {
|
||||||
found.el.delete = true;
|
found.el.delete = true;
|
||||||
startPoint = found.last;
|
startPoint = found.last;
|
||||||
emitter(found.el, found.first, ++count);
|
emitter(found.el, found.first, ++count, mindist);
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -42,6 +46,38 @@ export function tip2tipEmit(array, startPoint, emitter) {
|
||||||
return startPoint;
|
return startPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit each element in array based on next closest endpoint
|
||||||
|
* Uses greedy nearest-neighbor algorithm to minimize travel distance
|
||||||
|
* When polygons are close, a new polygon is injected that joins them
|
||||||
|
* and the next polygon is reversed so that tips would merge on output
|
||||||
|
* @param {Array} paths - Array of elements with { first, last } Point properties
|
||||||
|
* @param {Point} startPoint - Starting point for path ordering
|
||||||
|
* @param {Number} mergeDist - threshold for merging Polygons
|
||||||
|
* @returns {Array} reduced merged Polygon array
|
||||||
|
*/
|
||||||
|
export function tip2tipJoin(paths, startPoint, mergeDist) {
|
||||||
|
let newp = [];
|
||||||
|
tip2tipEmit(paths.map(path => ({
|
||||||
|
path,
|
||||||
|
last: path.last(),
|
||||||
|
first: path.first()
|
||||||
|
})), startPoint, (next, point, count, dist) => {
|
||||||
|
if (next.last === point) {
|
||||||
|
next.path.reverse();
|
||||||
|
}
|
||||||
|
if (newp.length && dist < mergeDist) {
|
||||||
|
let lastpath = newp.peek();
|
||||||
|
newp.push(newPolygon().addPoints([
|
||||||
|
lastpath.last(),
|
||||||
|
next.path.first()
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
newp.push(next.path);
|
||||||
|
});
|
||||||
|
return newp;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emit polygons in optimal order based on nearest point
|
* Emit polygons in optimal order based on nearest point
|
||||||
* Like tip2tipEmit but accepts polygons where next closest point can be anywhere
|
* Like tip2tipEmit but accepts polygons where next closest point can be anywhere
|
||||||
|
|
@ -691,65 +727,10 @@ export function arcToPath(start, end, arcdivs = 24, opts) {
|
||||||
return arr
|
return arr
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Dynamically growing Float32Array with efficient memory management
|
|
||||||
* Used for accumulating vertex/normal data when final size is unknown
|
|
||||||
* Automatically expands when capacity reached, optimizes final output
|
|
||||||
* @class
|
|
||||||
*/
|
|
||||||
export class FloatPacker {
|
|
||||||
/**
|
|
||||||
* Create new float packer with initial size
|
|
||||||
* @param {number} size - Initial array size
|
|
||||||
* @param {number} [factor=1.2] - Growth factor when expanding (clamped to max 1.2)
|
|
||||||
*/
|
|
||||||
constructor(size, factor) {
|
|
||||||
this.size = size;
|
|
||||||
this.factor = Math.min(factor || 1.2, 1.1);
|
|
||||||
this.array = new Float32Array(size);
|
|
||||||
this.pos = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Push multiple float values onto array
|
|
||||||
* Automatically expands array if needed
|
|
||||||
* @param {...number} values - Float values to append
|
|
||||||
*/
|
|
||||||
push() {
|
|
||||||
const array = this.array;
|
|
||||||
const size = this.size;
|
|
||||||
const args = arguments.length;
|
|
||||||
if (this.pos + args >= size) {
|
|
||||||
let nusize = ((size * this.factor) | 0) + args;
|
|
||||||
let nuarray = new Float32Array(nusize);
|
|
||||||
nuarray.set(array);
|
|
||||||
this.array = nuarray;
|
|
||||||
this.size = nusize;
|
|
||||||
}
|
|
||||||
for (let i = 0; i < args; i++) {
|
|
||||||
array[this.pos++] = arguments[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get final array trimmed to actual size
|
|
||||||
* Uses subarray (view) if >90% full, otherwise copies to save memory
|
|
||||||
* @returns {Float32Array} Array containing only pushed values
|
|
||||||
*/
|
|
||||||
finalize() {
|
|
||||||
if (this.pos / this.size >= 0.9) {
|
|
||||||
return this.array.subarray(0, this.pos);
|
|
||||||
} else {
|
|
||||||
return this.array.slice(0, this.pos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const paths = {
|
export const paths = {
|
||||||
tip2tipEmit,
|
tip2tipEmit,
|
||||||
poly2polyEmit,
|
poly2polyEmit,
|
||||||
shapeToPath,
|
shapeToPath,
|
||||||
pointsToPath,
|
pointsToPath,
|
||||||
pathTo3D,
|
pathTo3D
|
||||||
FloatPacker
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -877,6 +877,7 @@ class Point {
|
||||||
const { x, y } = this;
|
const { x, y } = this;
|
||||||
this.x = x * Math.cos(angle) - y * Math.sin(angle);
|
this.x = x * Math.cos(angle) - y * Math.sin(angle);
|
||||||
this.y = y * Math.cos(angle) + x * Math.sin(angle);
|
this.y = y * Math.cos(angle) + x * Math.sin(angle);
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
326
src/geo/poly/arcs.js
Normal file
326
src/geo/poly/arcs.js
Normal file
|
|
@ -0,0 +1,326 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
import { base, util } from '../base.js';
|
||||||
|
import { newPoint } from '../point.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Arc detection utility functions for polygons.
|
||||||
|
* These utilities detect and annotate circular arc sequences in polygon points.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the center point of a circle defined by three points.
|
||||||
|
* Using perimeter length, finds 3 equally spaced points to compute circle center.
|
||||||
|
*
|
||||||
|
* @param {Polygon} poly - polygon assumed to be roughly circular
|
||||||
|
* @returns {Point} center point of the circle
|
||||||
|
*/
|
||||||
|
export function calcCircleCenter(poly) {
|
||||||
|
let pe = poly.perimeter(),
|
||||||
|
pe1 = pe / 3, // point 2
|
||||||
|
pe2 = pe1 * 2, // point 3
|
||||||
|
p = poly.points,
|
||||||
|
l = p.length,
|
||||||
|
i = 1,
|
||||||
|
td = 0,
|
||||||
|
lp = p[0], // first point
|
||||||
|
ap = [ lp ]; // array of 3 points
|
||||||
|
while (i < l) {
|
||||||
|
let np = p[i++];
|
||||||
|
let d = lp.distTo2D(np);
|
||||||
|
td += d;
|
||||||
|
if (ap.length === 1 && td >= pe1) {
|
||||||
|
ap.push(np);
|
||||||
|
} else if (ap.length === 2 && (td >= pe2 || i >= l)) {
|
||||||
|
ap.push(np);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
lp = np;
|
||||||
|
}
|
||||||
|
let center = util.center2d(...ap);
|
||||||
|
return newPoint(center.x, center.y, p[0].z, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterate over polygon points and find sequences of 5 or more points
|
||||||
|
* with a common center point and collect as midpoint/radius.
|
||||||
|
*
|
||||||
|
* @param {Polygon} poly - polygon to analyze
|
||||||
|
* @param {Object} opt - options
|
||||||
|
* @param {number} opt.tolerance - center matching tolerance (default 1e-2)
|
||||||
|
* @param {boolean} opt.inside - require centers be inside polygon (default true)
|
||||||
|
* @returns {Array} array of arc center records
|
||||||
|
*/
|
||||||
|
export function findArcCenters(poly, opt = {}) {
|
||||||
|
if (poly.length < 6) return [];
|
||||||
|
let tolerance = opt.tolerance || 1e-2,
|
||||||
|
inside = opt.inside ?? true,
|
||||||
|
seq = poly.points.slice(),
|
||||||
|
util = base.util;
|
||||||
|
if (poly.isClosed()) seq.appendAll(seq.slice(0,5));
|
||||||
|
let recs = [], // accumulated arc points
|
||||||
|
cand = []; // candidate center array
|
||||||
|
for (let pos=3; pos<seq.length; pos++) {
|
||||||
|
let next = util.circleCenter(...seq.slice(pos-3, pos));
|
||||||
|
let prev = cand.peek();
|
||||||
|
if (inside && !newPoint(next.x,next.y).inPolygon(poly)) {
|
||||||
|
// require point be inside current polygon
|
||||||
|
next = null;
|
||||||
|
}
|
||||||
|
if (next && !prev) {
|
||||||
|
// seed candidate list
|
||||||
|
cand.push(next);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!next) {
|
||||||
|
// next is a bust, reset candidate list
|
||||||
|
cand.length = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (util.dist2D(next,prev) < tolerance) {
|
||||||
|
// add new candidate
|
||||||
|
cand.push(next);
|
||||||
|
continue;
|
||||||
|
} else if (cand.length >= 3) {
|
||||||
|
// emit record on 5 points
|
||||||
|
recs.push(cand.peek());
|
||||||
|
}
|
||||||
|
// reset candidate array
|
||||||
|
cand = [ next ];
|
||||||
|
}
|
||||||
|
if (cand.length >= 3) {
|
||||||
|
recs.push(cand.peek());
|
||||||
|
}
|
||||||
|
// filter dups
|
||||||
|
recs.sort((a,b) => {
|
||||||
|
return util.dist2D(a,b);
|
||||||
|
});
|
||||||
|
recs = recs.filter((r,i) => {
|
||||||
|
if (i > 0) {
|
||||||
|
return util.dist2D(r, recs[i-1]) < tolerance*10 ? null : r;
|
||||||
|
} else {
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return recs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect and annotate arc sequences in polygon points.
|
||||||
|
* When an arc is detected, the first point is annotated with arc metadata
|
||||||
|
* and intermediate points remain in the array but should be skipped during emission.
|
||||||
|
*
|
||||||
|
* @param {Polygon} poly - polygon to analyze (will be mutated with arc annotations)
|
||||||
|
* @param {Object} opts - detection options
|
||||||
|
* @param {number} opts.tolerance - arc detection tolerance (default 0.1)
|
||||||
|
* @param {number} opts.arcRes - arc resolution in degrees (default 1)
|
||||||
|
* @param {number} opts.minPoints - minimum points to consider an arc (default 4)
|
||||||
|
* @returns {Polygon} this polygon with arc annotations
|
||||||
|
*/
|
||||||
|
export function detectArcs(poly, opts = {}) {
|
||||||
|
const {
|
||||||
|
tolerance = 0.1,
|
||||||
|
arcRes = 1,
|
||||||
|
minPoints = 4
|
||||||
|
} = opts;
|
||||||
|
|
||||||
|
const points = poly.points;
|
||||||
|
const length = points.length;
|
||||||
|
const arcResRadians = arcRes * (Math.PI / 180);
|
||||||
|
|
||||||
|
if (length < minPoints) {
|
||||||
|
return poly;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any existing arc annotations
|
||||||
|
for (let p of points) {
|
||||||
|
delete p.arc;
|
||||||
|
}
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
// Process all points except we can't start an arc at the last point
|
||||||
|
// because we need at least minPoints to form an arc
|
||||||
|
while (i < length - minPoints + 1) {
|
||||||
|
// Try to detect arc starting at position i
|
||||||
|
const arcData = detectArcAt(poly, i, points, length, tolerance, arcResRadians, minPoints);
|
||||||
|
|
||||||
|
if (arcData) {
|
||||||
|
// Annotate the starting point
|
||||||
|
points[i].arc = arcData;
|
||||||
|
// Skip past the arc points
|
||||||
|
i += arcData.skip + 1; // +1 to move to point after arc end
|
||||||
|
} else {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return poly;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to detect an arc starting at the given index.
|
||||||
|
* Uses a greedy approach: grow the arc as long as possible, then validate.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {Polygon} poly - polygon being analyzed
|
||||||
|
* @param {number} startIdx - starting index
|
||||||
|
* @param {Point[]} points - polygon points
|
||||||
|
* @param {number} length - points length
|
||||||
|
* @param {number} tolerance - detection tolerance
|
||||||
|
* @param {number} arcRes - arc resolution
|
||||||
|
* @param {number} minPoints - minimum points for arc
|
||||||
|
* @returns {Object|null} arc data or null if no arc detected
|
||||||
|
*/
|
||||||
|
function detectArcAt(poly, startIdx, points, length, tolerance, arcRes, minPoints) {
|
||||||
|
// Greedy approach: collect as many points as possible that might form an arc
|
||||||
|
let candidates = [];
|
||||||
|
|
||||||
|
for (let idx = startIdx; idx < length; idx++) {
|
||||||
|
const p1 = points[idx];
|
||||||
|
|
||||||
|
// Last point - add and done
|
||||||
|
if (idx >= length - 1) {
|
||||||
|
candidates.push(p1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const p2 = points[idx + 1];
|
||||||
|
|
||||||
|
// Skip duplicate points
|
||||||
|
if (p1.distTo2D(p2) < 0.001) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates.push(p1);
|
||||||
|
|
||||||
|
// Stop if we've collected enough to test
|
||||||
|
if (candidates.length >= minPoints) {
|
||||||
|
// Try to validate the arc with current candidates
|
||||||
|
const arcData = validateArc(poly, candidates, tolerance, arcRes, minPoints);
|
||||||
|
|
||||||
|
if (!arcData) {
|
||||||
|
// Current set doesn't form valid arc, back up one point
|
||||||
|
candidates.pop();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final validation with all collected candidates
|
||||||
|
return validateArc(poly, candidates, tolerance, arcRes, minPoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate if a set of points forms a valid arc.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {Polygon} poly - polygon being analyzed
|
||||||
|
* @param {Point[]} arcPoints - candidate arc points
|
||||||
|
* @param {number} tolerance - detection tolerance
|
||||||
|
* @param {number} arcRes - arc resolution
|
||||||
|
* @param {number} minPoints - minimum points for arc
|
||||||
|
* @returns {Object|null} arc data or null if invalid
|
||||||
|
*/
|
||||||
|
function validateArc(poly, arcPoints, tolerance, arcRes, minPoints) {
|
||||||
|
if (arcPoints.length < minPoints) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate center using well-distributed points
|
||||||
|
const center = findBestCenter(arcPoints, tolerance);
|
||||||
|
if (!center) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if all points lie on the circle within tolerance
|
||||||
|
let maxRadiusError = 0;
|
||||||
|
let sumRadiusError = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < arcPoints.length; i++) {
|
||||||
|
const p = arcPoints[i];
|
||||||
|
const radius = Math.hypot(p.x - center.x, p.y - center.y);
|
||||||
|
const radiusError = Math.abs(radius - center.r);
|
||||||
|
|
||||||
|
maxRadiusError = Math.max(maxRadiusError, radiusError);
|
||||||
|
sumRadiusError += radiusError;
|
||||||
|
|
||||||
|
// Hard limit on any single point
|
||||||
|
if (radiusError > tolerance * 2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check average error is reasonable
|
||||||
|
const avgRadiusError = sumRadiusError / arcPoints.length;
|
||||||
|
if (avgRadiusError > tolerance) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check angular resolution (don't want points too far apart)
|
||||||
|
for (let i = 0; i < arcPoints.length - 1; i++) {
|
||||||
|
const curr = arcPoints[i];
|
||||||
|
const next = arcPoints[i + 1];
|
||||||
|
const dist = curr.distTo2D(next);
|
||||||
|
const radius = Math.hypot(curr.x - center.x, curr.y - center.y);
|
||||||
|
|
||||||
|
if (radius > 0) {
|
||||||
|
const angle = 2 * Math.asin(Math.min(1, dist / (2 * radius)));
|
||||||
|
if (Math.abs(angle) > arcRes) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine arc direction
|
||||||
|
const p0 = arcPoints[0];
|
||||||
|
const p1 = arcPoints[Math.min(1, arcPoints.length - 1)];
|
||||||
|
const vec1 = { x: p1.x - p0.x, y: p1.y - p0.y };
|
||||||
|
const vec2 = { x: center.x - p0.x, y: center.y - p0.y };
|
||||||
|
const cross = vec1.x * vec2.y - vec1.y * vec2.x;
|
||||||
|
const clockwise = cross < 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
center: newPoint(center.x, center.y, p0.z),
|
||||||
|
clockwise,
|
||||||
|
skip: arcPoints.length - 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the best-fit center for a set of arc points.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {Point[]} arcPoints - candidate arc points
|
||||||
|
* @param {number} tolerance - detection tolerance
|
||||||
|
* @returns {Object|null} center with x, y, r properties or null
|
||||||
|
*/
|
||||||
|
function findBestCenter(arcPoints, tolerance) {
|
||||||
|
const len = arcPoints.length;
|
||||||
|
|
||||||
|
// Use 3 well-spaced points for initial center calculation
|
||||||
|
let idx1 = 0;
|
||||||
|
let idx2 = Math.floor(len / 2);
|
||||||
|
let idx3 = len - 1;
|
||||||
|
|
||||||
|
// If first and last are very close (near-circle), use different points
|
||||||
|
if (arcPoints[idx1].distTo2D(arcPoints[idx3]) < tolerance * 2) {
|
||||||
|
idx1 = Math.floor(len * 0.25);
|
||||||
|
idx2 = Math.floor(len * 0.5);
|
||||||
|
idx3 = Math.floor(len * 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
const center = util.center2d(
|
||||||
|
arcPoints[idx1],
|
||||||
|
arcPoints[idx2],
|
||||||
|
arcPoints[idx3],
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!center || center.hasNaN?.() || !isFinite(center.r)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return center;
|
||||||
|
}
|
||||||
280
src/geo/poly/geo.js
Normal file
280
src/geo/poly/geo.js
Normal file
|
|
@ -0,0 +1,280 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
import { base, config, util } from '../base.js';
|
||||||
|
import { calc_normal, calc_vertex } from '../paths.js';
|
||||||
|
import { newPolygon, slopeDiff } from '../polygon.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Geometric utility functions for polygons.
|
||||||
|
* These are extracted from the Polygon class to improve modularity.
|
||||||
|
* All functions take a polygon as the first parameter.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove points that are closer than `dist` to the previous point.
|
||||||
|
* Returns a new polygon with filtered points, or null if result would have < 2 points.
|
||||||
|
*
|
||||||
|
* @param {Polygon} poly - source polygon
|
||||||
|
* @param {number} dist - minimum distance between points (defaults to precision_merge)
|
||||||
|
* @returns {Polygon|null} new filtered polygon or null
|
||||||
|
*/
|
||||||
|
export function debur(poly, dist) {
|
||||||
|
if (poly.length < 2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const pa = poly.points,
|
||||||
|
pln = pa.length,
|
||||||
|
open = poly.open,
|
||||||
|
newp = newPolygon().copyZ(poly.getZ()),
|
||||||
|
min = dist || base.config.precision_merge;
|
||||||
|
let lo;
|
||||||
|
newp.push(lo = pa[0]);
|
||||||
|
for (let i = 1; i < pln; i++) {
|
||||||
|
if (lo.distTo2D(pa[i]) >= min) {
|
||||||
|
newp.push(lo = pa[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newp.open = open;
|
||||||
|
newp.parent = poly.parent;
|
||||||
|
if (newp.length < 2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return newp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate miter joins for polygon points where angle exceeds 90 degrees.
|
||||||
|
* Returns either the original polygon or a new one with miter adjustments.
|
||||||
|
*
|
||||||
|
* @param {Polygon} poly - source polygon
|
||||||
|
* @param {boolean} debug - debug flag
|
||||||
|
* @returns {Polygon} polygon with miters applied
|
||||||
|
*/
|
||||||
|
export function miter(poly, debug) {
|
||||||
|
if (poly.length < 3) return poly;
|
||||||
|
|
||||||
|
const slo = [],
|
||||||
|
pa = poly.points,
|
||||||
|
pln = pa.length,
|
||||||
|
open = poly.open;
|
||||||
|
let last;
|
||||||
|
for (let i = 1; i < pln; i++) {
|
||||||
|
slo.push(pa[i - 1].slopeTo(last = pa[i]));
|
||||||
|
}
|
||||||
|
if (!open) {
|
||||||
|
slo.push(last.slopeTo(pa[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
const ang = new Array(pln).fill(0);
|
||||||
|
let redo = false;
|
||||||
|
const aln = open ? pln - 1 : pln;
|
||||||
|
for (let i = 1; i < aln; i++) {
|
||||||
|
ang[i] = slopeDiff(slo[i - 1], slo[i]);
|
||||||
|
redo |= ang[i] > 90;
|
||||||
|
}
|
||||||
|
if (!open) {
|
||||||
|
ang[0] = slopeDiff(slo[pln - 1], slo[0]);
|
||||||
|
redo |= ang[pln - 1] > 90;
|
||||||
|
redo |= ang[0] > 90;
|
||||||
|
}
|
||||||
|
if (redo) {
|
||||||
|
const newp = newPolygon().copyZ(poly.z);
|
||||||
|
newp.open = open;
|
||||||
|
for (let i = 0; i < pln; i++) {
|
||||||
|
const p = pa[(i + pln) % pln];
|
||||||
|
const d = ang[(i + pln) % pln];
|
||||||
|
if (d > 179) {
|
||||||
|
const s = slo[(i + pln) % pln];
|
||||||
|
const pp = pa[(i + pln - 1) % pln];
|
||||||
|
const ps = slo[(i + pln - 1) % pln];
|
||||||
|
newp.push(p.follow(p.slopeTo(pp).normal(), 0.001));
|
||||||
|
newp.push(p.follow(s.clone().normal().invert(), 0.001));
|
||||||
|
} else if (d > 90) {
|
||||||
|
const s = slo[(i + pln) % pln];
|
||||||
|
const pp = pa[(i + pln - 1) % pln];
|
||||||
|
const ps = slo[(i + pln - 1) % pln];
|
||||||
|
newp.push(p.follow(p.slopeTo(pp), 0.001));
|
||||||
|
newp.push(p.follow(s, 0.001));
|
||||||
|
} else {
|
||||||
|
p.parent = newp;
|
||||||
|
newp.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newp;
|
||||||
|
}
|
||||||
|
return poly;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create convex hull from points and add to polygon (mutates polygon).
|
||||||
|
* Only used in fdm/slice.js for enclosing supports.
|
||||||
|
*
|
||||||
|
* @param {Polygon} poly - target polygon (will be mutated)
|
||||||
|
* @param {Point[]} points - points to form convex hull
|
||||||
|
* @returns {Polygon} the mutated polygon
|
||||||
|
*/
|
||||||
|
export function createConvexHull(poly, points) {
|
||||||
|
function removeMiddle(a, b, c) {
|
||||||
|
let cross = (a.x - b.x) * (c.y - b.y) - (a.y - b.y) * (c.x - b.x);
|
||||||
|
let dot = (a.x - b.x) * (c.x - b.x) + (a.y - b.y) * (c.y - b.y);
|
||||||
|
return cross < 0 || cross == 0 && dot <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
points.sort(function(a, b) {
|
||||||
|
return a.x != b.x ? a.x - b.x : a.y - b.y;
|
||||||
|
});
|
||||||
|
|
||||||
|
let n = points.length;
|
||||||
|
let hull = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < 2 * n; i++) {
|
||||||
|
let j = i < n ? i : 2 * n - 1 - i;
|
||||||
|
while (hull.length >= 2 && removeMiddle(hull[hull.length - 2], hull[hull.length - 1], points[j]))
|
||||||
|
hull.pop();
|
||||||
|
hull.push(points[j]);
|
||||||
|
}
|
||||||
|
|
||||||
|
hull.pop();
|
||||||
|
poly.addPoints(hull);
|
||||||
|
|
||||||
|
return poly;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all intersections of a line segment with polygon edges.
|
||||||
|
* Results are sorted by distance from lp1.
|
||||||
|
*
|
||||||
|
* @param {Polygon} poly - polygon to test
|
||||||
|
* @param {Point} lp1 - line segment start
|
||||||
|
* @param {Point} lp2 - line segment end
|
||||||
|
* @param {boolean} deep - recurse into inner polygons
|
||||||
|
* @returns {Point[]} array of intersection points
|
||||||
|
*/
|
||||||
|
export function intersections(poly, lp1, lp2, deep) {
|
||||||
|
let list = [];
|
||||||
|
poly.forEachSegment(function(pp1, pp2, ip1, ip2) {
|
||||||
|
let int = util.intersect(lp1, lp2, pp1, pp2, base.key.SEGINT, false);
|
||||||
|
if (int) {
|
||||||
|
list.push(int);
|
||||||
|
pp1.pos = ip1;
|
||||||
|
pp2.pos = ip2;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (deep && poly.inner) {
|
||||||
|
poly.inner.forEach(p => {
|
||||||
|
let ints = intersections(p, lp1, lp2);
|
||||||
|
if (ints) list.appendAll(ints);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
list.sort(function(p1, p2) {
|
||||||
|
return util.distSq(lp1, p1) - util.distSq(lp1, p2);
|
||||||
|
});
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Walk points noting z deltas and smoothing z sawtooth patterns.
|
||||||
|
* Used to smooth low-rez surface contouring (mutates polygon).
|
||||||
|
*
|
||||||
|
* @param {Polygon} poly - polygon to smooth (mutated)
|
||||||
|
* @param {number} passes - number of smoothing passes
|
||||||
|
* @returns {Polygon} the mutated polygon
|
||||||
|
*/
|
||||||
|
export function refine(poly, passes = 0) {
|
||||||
|
for (let j = 0; j < passes; j++) {
|
||||||
|
let points = poly.points,
|
||||||
|
length = points.length,
|
||||||
|
sn = [], // segment normals
|
||||||
|
vn = []; // vertex normals
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
let p1 = points[i];
|
||||||
|
let p2 = points[(i + 1) % length];
|
||||||
|
sn.push(calc_normal(p1, p2));
|
||||||
|
}
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
let n1 = sn[(i + length - 1) % length];
|
||||||
|
let n2 = sn[i];
|
||||||
|
let vi = calc_vertex(n1, n2, 1);
|
||||||
|
vn.push(vi);
|
||||||
|
let vl = Math.abs(1 - vi.vl).round(2);
|
||||||
|
// vl should be close to zero on smooth / continuous curves
|
||||||
|
// factoring out hard turns, we smooth the z using the weighted
|
||||||
|
// z values of the points before and after the current point
|
||||||
|
if (vl === 0) {
|
||||||
|
let p0 = points[(i + length - 1) % length];
|
||||||
|
let p1 = points[i];
|
||||||
|
let p2 = points[(i + 1) % length];
|
||||||
|
p1.z = (p0.z + p2.z + p1.z) / 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return poly;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test if two polygons are essentially equivalent in shape and position.
|
||||||
|
* Checks area, bounds, circularity, and point-to-segment distances.
|
||||||
|
*
|
||||||
|
* @param {Polygon} poly1 - first polygon
|
||||||
|
* @param {Polygon} poly2 - second polygon
|
||||||
|
* @param {boolean} recurse - also check inner polygons
|
||||||
|
* @param {number} precision - tolerance for comparisons
|
||||||
|
* @returns {boolean} true if polygons are equivalent
|
||||||
|
*/
|
||||||
|
export function isEquivalent(poly1, poly2, recurse, precision) {
|
||||||
|
let area1 = Math.abs(poly1.area());
|
||||||
|
let area2 = Math.abs(poly2.area());
|
||||||
|
if (util.isCloseTo(area1, area2, precision || config.precision_poly_area) &&
|
||||||
|
poly1.bounds.equals(poly2.bounds, precision || config.precision_poly_bounds)) {
|
||||||
|
// use circularity near 1 to eliminate the extensive check below
|
||||||
|
let c1 = poly1.circularity(),
|
||||||
|
c2 = poly2.circularity();
|
||||||
|
if (Math.abs(c1 - c2) < config.precision_circularity && ((1 - c1) < config.precision_circularity)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recurse) {
|
||||||
|
let i, ai = poly1.inner,
|
||||||
|
bi = poly2.inner;
|
||||||
|
if (ai !== bi) {
|
||||||
|
if (ai === null || bi === null || ai.length != bi.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (i = 0; i < ai.length; i++) {
|
||||||
|
if (!isEquivalent(ai[i], bi[i])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let exit = true,
|
||||||
|
pointok,
|
||||||
|
dist,
|
||||||
|
min;
|
||||||
|
|
||||||
|
poly1.forEachPoint(i2p => {
|
||||||
|
pointok = false;
|
||||||
|
poly2.forEachSegment((i1p1, i1p2) => {
|
||||||
|
// if point is close to poly, terminate search, go to next point
|
||||||
|
if ((dist = i2p.distToLine(i1p1, i1p2)) < config.precision_poly_merge) {
|
||||||
|
return pointok = true;
|
||||||
|
}
|
||||||
|
// otherwise track min and keep searching
|
||||||
|
min = Math.min(min, dist);
|
||||||
|
});
|
||||||
|
// fail poly if one point is bad
|
||||||
|
if (!pointok) {
|
||||||
|
exit = false;
|
||||||
|
// terminate search
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return exit;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
275
src/geo/poly/mesh.js
Normal file
275
src/geo/poly/mesh.js
Normal file
|
|
@ -0,0 +1,275 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
import { earcut as earcutLib } from '../base.js';
|
||||||
|
import { polygons as POLY } from '../polygons.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mesh generation utility functions for polygons.
|
||||||
|
* These utilities convert 2D polygons into 3D meshes and geometry.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triangulate polygon using earcut library.
|
||||||
|
* Returns array of triangular polygons with preserved alignment/swap info.
|
||||||
|
*
|
||||||
|
* @param {Polygon} poly - polygon to triangulate
|
||||||
|
* @returns {Polygon[]} array of triangular polygons
|
||||||
|
*/
|
||||||
|
export function earcut(poly) {
|
||||||
|
// gather all points into a single array including inner polys
|
||||||
|
// keeping track of array offset indices for inners
|
||||||
|
let out = [];
|
||||||
|
let holes = [];
|
||||||
|
|
||||||
|
// flatten points into array for earcut()
|
||||||
|
poly.points.forEach(p => {
|
||||||
|
out.push(p.x, p.y, p.z);
|
||||||
|
});
|
||||||
|
|
||||||
|
// add hole offsets for inner polygons
|
||||||
|
if (poly.inner) {
|
||||||
|
poly.inner.forEach(p => {
|
||||||
|
holes.push(out.length / 3);
|
||||||
|
p.points.forEach(p => {
|
||||||
|
out.push(p.x, p.y, p.z);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// perform earcut()
|
||||||
|
let cut = earcutLib(out, holes, 3);
|
||||||
|
let ret = [];
|
||||||
|
|
||||||
|
// preserve swaps in new polys
|
||||||
|
for (let i = 0; i < cut.length; i += 3) {
|
||||||
|
let p = new (poly.constructor)();
|
||||||
|
p._aligned = poly._aligned;
|
||||||
|
p._swapped = poly._swapped;
|
||||||
|
for (let j = 0; j < 3; j++) {
|
||||||
|
let n = cut[i + j] * 3;
|
||||||
|
p.add(out[n], out[n + 1], out[n + 2]);
|
||||||
|
}
|
||||||
|
ret.push(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn 2D polygon into a 2.5D ribbon extruded in Z.
|
||||||
|
* Returns flat array of vertices for mesh rendering.
|
||||||
|
*
|
||||||
|
* @param {Polygon} poly - polygon to extrude as ribbon
|
||||||
|
* @param {number} z - Z height (default 1)
|
||||||
|
* @param {number} zadd - Z offset (default 0)
|
||||||
|
* @param {boolean} rev - reverse winding (default false)
|
||||||
|
* @returns {number[]} flat array of triangle vertices
|
||||||
|
*/
|
||||||
|
export function ribbonZ(poly, z = 1, zadd = 0, rev) {
|
||||||
|
let clone = poly.clone().setClockwise();
|
||||||
|
let faces = [];
|
||||||
|
let points = clone.points;
|
||||||
|
let length = points.length;
|
||||||
|
if (rev) {
|
||||||
|
points = points.slice().reverse();
|
||||||
|
}
|
||||||
|
for (let i=0; i<length; i++) {
|
||||||
|
let p0 = points[i];
|
||||||
|
let p1 = points[(i + 1) % length];
|
||||||
|
faces.push(p0.x, p0.y, p0.z + zadd);
|
||||||
|
faces.push(p1.x, p1.y, p1.z + z + zadd);
|
||||||
|
faces.push(p1.x, p1.y, p0.z + zadd);
|
||||||
|
faces.push(p0.x, p0.y, p0.z + zadd);
|
||||||
|
faces.push(p0.x, p0.y, p0.z + z + zadd);
|
||||||
|
faces.push(p1.x, p1.y, p1.z + z + zadd);
|
||||||
|
}
|
||||||
|
return faces;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For turning a poly with an inner offset into a 3D mesh.
|
||||||
|
* Requires inner has same circularity and <= num points.
|
||||||
|
* Primarily used to make chamfers in mesh:tool.
|
||||||
|
*
|
||||||
|
* @param {Polygon} poly - polygon with exactly one inner
|
||||||
|
* @param {boolean} swap - swap winding for normals
|
||||||
|
* @returns {number[]|undefined} flat array of triangle vertices or undefined if invalid
|
||||||
|
*/
|
||||||
|
export function ribbonMesh(poly, swap) {
|
||||||
|
if (!(poly.inner && poly.inner.length === 1)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
let outer = poly.clone().setClockwise();
|
||||||
|
let inner = poly.inner[0].clone().setClockwise();
|
||||||
|
let c0 = outer.circularity();
|
||||||
|
let c1 = inner.circularity();
|
||||||
|
let n0 = outer.points.length;
|
||||||
|
let n1 = inner.points.length;
|
||||||
|
let p0 = outer.points.slice();
|
||||||
|
let p1 = inner.points.slice();
|
||||||
|
let min = { d: Infinity, i:0, j:0 };
|
||||||
|
// find the closests two points inner/outer
|
||||||
|
for (let i=0; i<p0.length; i++) {
|
||||||
|
for (let j=0; j<p1.length; j++) {
|
||||||
|
let d = p0[i].distTo2D(p1[j]);
|
||||||
|
if (d < min.d) {
|
||||||
|
min = { d, i, j };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// slide the arrays until the closest points are aligned at index = 0
|
||||||
|
p0 = p0.slice(min.i).concat(p0.slice(0, min.i)); p0.push(p0[0]);
|
||||||
|
p1 = p1.slice(min.j).concat(p1.slice(0, min.j)); p1.push(p1[0]);
|
||||||
|
// walk both arrays moving to the next poly + point that forms
|
||||||
|
// the shortest line segment between the two points (inner / outer)
|
||||||
|
let faces = [];
|
||||||
|
let pi0 = 0;
|
||||||
|
let pi1 = 0;
|
||||||
|
let pp0 = p0[pi0];
|
||||||
|
let pp1 = p1[pi1];
|
||||||
|
for (;;) {
|
||||||
|
let pn0 = p0[pi0 + 1];
|
||||||
|
let pn1 = p1[pi1 + 1];
|
||||||
|
if ((!pn0 && pn1) || (pn1 && pp0.distTo2D(pn1) < pp1.distTo2D(pn0))) {
|
||||||
|
// emit and increment bottom
|
||||||
|
faces.push(pp0.x, pp0.y, pp0.z);
|
||||||
|
if (swap) {
|
||||||
|
faces.push(pp1.x, pp1.y, pp1.z);
|
||||||
|
faces.push(pn1.x, pn1.y, pn1.z);
|
||||||
|
} else {
|
||||||
|
faces.push(pn1.x, pn1.y, pn1.z);
|
||||||
|
faces.push(pp1.x, pp1.y, pp1.z);
|
||||||
|
}
|
||||||
|
pi1++;
|
||||||
|
pp1 = p1[pi1];
|
||||||
|
} else if (pn0) {
|
||||||
|
// emit and increment top
|
||||||
|
faces.push(pp0.x, pp0.y, pp0.z);
|
||||||
|
if (swap) {
|
||||||
|
faces.push(pp1.x, pp1.y, pp1.z);
|
||||||
|
faces.push(pn0.x, pn0.y, pn0.z);
|
||||||
|
} else {
|
||||||
|
faces.push(pn0.x, pn0.y, pn0.z);
|
||||||
|
faces.push(pp1.x, pp1.y, pp1.z);
|
||||||
|
}
|
||||||
|
pi0++;
|
||||||
|
pp0 = p0[pi0];
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return faces;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrude polygon (with inner voids) into 3D mesh.
|
||||||
|
* Supports chamfers on top and/or bottom edges.
|
||||||
|
*
|
||||||
|
* @param {Polygon} poly - polygon to extrude
|
||||||
|
* @param {number} z - Z height (default 1)
|
||||||
|
* @param {Object} opt - options
|
||||||
|
* @param {number} opt.zadd - Z offset for bottom (default 0)
|
||||||
|
* @param {number} opt.chamfer - chamfer size for both top and bottom (default 0)
|
||||||
|
* @param {number} opt.chamfer_top - chamfer size for top (overrides chamfer)
|
||||||
|
* @param {number} opt.chamfer_bottom - chamfer size for bottom (overrides chamfer)
|
||||||
|
* @returns {number[]} flat array of triangle vertices
|
||||||
|
*/
|
||||||
|
export function extrude(poly, z = 1, opt = {}) {
|
||||||
|
let earcutFaces = earcut(poly); // array of 3-point polygons
|
||||||
|
|
||||||
|
// return just the 2D face when no Z depth specified
|
||||||
|
// used primarily by mesh.sketch render()
|
||||||
|
if (z === 0) {
|
||||||
|
return earcutFaces.map(face => face.points.map(p => [ p.x, p.y, p.z ])).flat().flat();
|
||||||
|
}
|
||||||
|
|
||||||
|
let inv = z < 0;
|
||||||
|
|
||||||
|
if (inv) {
|
||||||
|
z = -z;
|
||||||
|
}
|
||||||
|
|
||||||
|
let chamfer = opt.chamfer || 0;
|
||||||
|
let chamfer_top = opt.chamfer_top || chamfer;
|
||||||
|
let chamfer_bottom = opt.chamfer_bottom || chamfer;
|
||||||
|
|
||||||
|
if (inv) {
|
||||||
|
let tmp = chamfer_top;
|
||||||
|
chamfer_top = chamfer_bottom;
|
||||||
|
chamfer_bottom = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
let zadd = (typeof opt === 'number' ? opt : opt.zadd || 0); // z bottom
|
||||||
|
let obj = []; // flat output vertex array (float-x,float-y,float-z,...)
|
||||||
|
let top_face = earcutFaces;
|
||||||
|
let bottom_face = earcutFaces;
|
||||||
|
let z_top = z + zadd;
|
||||||
|
let z_bottom = zadd;
|
||||||
|
let z_side_top = z;
|
||||||
|
let z_side_bottom = z_bottom;
|
||||||
|
|
||||||
|
// chamfer bottom only on negative chamfer
|
||||||
|
if (chamfer < 0) {
|
||||||
|
chamfer_top = 0;
|
||||||
|
chamfer_bottom = -chamfer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create chamfers (when defined)
|
||||||
|
if (chamfer_top) {
|
||||||
|
let inset = poly.offset(chamfer_top);
|
||||||
|
if (inset.length === 1) {
|
||||||
|
inset[0].setZ(z_top);
|
||||||
|
top_face = earcut(inset[0]);
|
||||||
|
z_side_top -= chamfer_top;
|
||||||
|
let renest = POLY.renest([poly.clone(true).setZ(z_side_top), inset[0]]);
|
||||||
|
for (let rnpoly of renest) {
|
||||||
|
obj.appendAll(ribbonMesh(rnpoly, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chamfer_bottom) {
|
||||||
|
let inset = poly.offset(chamfer_bottom);
|
||||||
|
if (inset.length === 1) {
|
||||||
|
inset[0].setZ(0);
|
||||||
|
bottom_face = earcut(inset[0]);
|
||||||
|
z_side_top -= chamfer_top || chamfer_bottom;
|
||||||
|
z_side_bottom += chamfer_bottom;
|
||||||
|
let renest = POLY.renest([poly.clone(true).setZ(z_side_bottom), inset[0]]);
|
||||||
|
for (let rnpoly of renest) {
|
||||||
|
obj.appendAll(ribbonMesh(rnpoly, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let p of top_face) {
|
||||||
|
for (let point of p.points) {
|
||||||
|
obj.push(point.x, point.y, z_top);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// bottom face (reversed to reverse normals)
|
||||||
|
for (let p of bottom_face) {
|
||||||
|
for (let point of p.points.reverse()) {
|
||||||
|
obj.push(point.x, point.y, z_bottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// outside wall
|
||||||
|
let rib = z - chamfer_top - chamfer_bottom;
|
||||||
|
obj.appendAll(ribbonZ(poly, rib, z_side_bottom));
|
||||||
|
for (let inner of poly.inner || []) {
|
||||||
|
// inside wall(s)
|
||||||
|
obj.appendAll(ribbonZ(inner, rib, z_side_bottom, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inv) {
|
||||||
|
for (let i=2; i<obj.length; i+=3) {
|
||||||
|
obj[i] -= z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
374
src/geo/poly/path.js
Normal file
374
src/geo/poly/path.js
Normal file
|
|
@ -0,0 +1,374 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
import { util } from '../base.js';
|
||||||
|
import { newPoint } from '../point.js';
|
||||||
|
import { newPolygon } from '../polygon.js';
|
||||||
|
import { newSlopeFromAngle } from '../slope.js';
|
||||||
|
import { polygons as POLY } from '../polygons.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toolpath and CNC utility functions for polygons.
|
||||||
|
* These utilities handle path generation, segmentation, and CNC-specific operations.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a trace path around the inside of a polygon including inner polys.
|
||||||
|
* Returns the noodle and the remainder of the polygon with the noodle removed.
|
||||||
|
* TODO: relocate this code to fdm/post.js
|
||||||
|
*
|
||||||
|
* @param {Polygon} poly - polygon to trace
|
||||||
|
* @param {number} width - trace width
|
||||||
|
* @returns {Object} { noodle: Polygon[], remain: Polygon[] }
|
||||||
|
*/
|
||||||
|
export function noodle(poly, width) {
|
||||||
|
let clone = poly.clone(true);
|
||||||
|
let ins = clone.offset(width) ?? [];
|
||||||
|
let remain = ins.clone(true);
|
||||||
|
let nood = POLY.nest(POLY.flatten([ clone, ...ins ], [], true));
|
||||||
|
return { noodle: nood, remain };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate center crossing point cloud for thin wall filling.
|
||||||
|
* Only used for fdm thin-wall type 1 (fdm/post.js).
|
||||||
|
* TODO: relocate this code to fdm/post.js
|
||||||
|
*
|
||||||
|
* @param {Polygon} poly - polygon to analyze
|
||||||
|
* @param {number} step - step size for sampling
|
||||||
|
* @param {number} z - Z height
|
||||||
|
* @param {number} min - minimum distance threshold
|
||||||
|
* @param {number} max - maximum distance threshold
|
||||||
|
* @param {Object} opt - options
|
||||||
|
* @param {boolean} opt.lines - return lines instead of points (default false)
|
||||||
|
* @param {number} opt.mindist - minimum distance for point joining (default step * 1.5)
|
||||||
|
* @returns {Polygon[]|Point[]} array of polygons or points
|
||||||
|
*/
|
||||||
|
export function centers(poly, step, z, min, max, opt = {}) {
|
||||||
|
let cloud = [],
|
||||||
|
bounds = poly.bounds,
|
||||||
|
lines = opt.lines || false,
|
||||||
|
stepoff = step / 2,
|
||||||
|
set = [poly.points];
|
||||||
|
|
||||||
|
if (poly.inner) {
|
||||||
|
for (let inner of poly.inner) {
|
||||||
|
set.push(inner.points);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let y of util.lerp(bounds.miny + stepoff, bounds.maxy - stepoff, step, true)) {
|
||||||
|
let ints = [];
|
||||||
|
for (let points of set) {
|
||||||
|
let length = points.length;
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
let p1 = points[i % length];
|
||||||
|
let p2 = points[(i + 1) % length];
|
||||||
|
if (
|
||||||
|
(p1.y <= y && p2.y > y) ||
|
||||||
|
(p1.y > y && p2.y <= y)
|
||||||
|
) ints.push([p1, p2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let cntr = [];
|
||||||
|
if (ints.length && ints.length % 2 === 0) {
|
||||||
|
for (let int of ints) {
|
||||||
|
let [p1, p2] = int;
|
||||||
|
if (p2.y < p1.y) {
|
||||||
|
let tp = p1;
|
||||||
|
p1 = p2;
|
||||||
|
p2 = tp;
|
||||||
|
}
|
||||||
|
let minx = Math.min(p1.x, p2.x);
|
||||||
|
let maxx = Math.max(p1.x, p2.x);
|
||||||
|
let miny = Math.min(p1.y, p2.y);
|
||||||
|
let maxy = Math.max(p1.y, p2.y);
|
||||||
|
let dx = p2.x - p1.x;
|
||||||
|
let dy = maxy - miny;
|
||||||
|
let pct = (y - miny) / dy;
|
||||||
|
let xpo = p1.x + pct * dx;
|
||||||
|
cntr.push(xpo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cntr.sort((a, b) => {
|
||||||
|
return b - a;
|
||||||
|
});
|
||||||
|
let lp, eo = 0;
|
||||||
|
for (let x of cntr) {
|
||||||
|
let p = newPoint(x, y, z);
|
||||||
|
if (eo++ % 2) {
|
||||||
|
let d = lp.distTo2D(p);
|
||||||
|
if (d >= min && d <= max) {
|
||||||
|
if (lines) {
|
||||||
|
cloud.push(lp);
|
||||||
|
cloud.push(p);
|
||||||
|
} else {
|
||||||
|
cloud.push(newPoint(
|
||||||
|
(lp.x + p.x) / 2, y, z
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lp = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let x of util.lerp(bounds.minx + stepoff, bounds.maxx - stepoff, step, true)) {
|
||||||
|
let ints = [];
|
||||||
|
for (let points of set) {
|
||||||
|
let length = points.length;
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
let p1 = points[i % length];
|
||||||
|
let p2 = points[(i + 1) % length];
|
||||||
|
if (
|
||||||
|
(p1.x <= x && p2.x > x) ||
|
||||||
|
(p1.x > x && p2.x <= x)
|
||||||
|
) ints.push([p1, p2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let cntr = [];
|
||||||
|
if (ints.length && ints.length % 2 === 0) {
|
||||||
|
for (let int of ints) {
|
||||||
|
let [p1, p2] = int;
|
||||||
|
if (p2.x < p1.x) {
|
||||||
|
let tp = p1;
|
||||||
|
p1 = p2;
|
||||||
|
p2 = tp;
|
||||||
|
}
|
||||||
|
let minx = Math.min(p1.x, p2.x);
|
||||||
|
let maxx = Math.max(p1.x, p2.x);
|
||||||
|
let miny = Math.min(p1.y, p2.y);
|
||||||
|
let maxy = Math.max(p1.y, p2.y);
|
||||||
|
let dx = maxx - minx;
|
||||||
|
let dy = p2.y - p1.y;
|
||||||
|
let pct = (x - minx) / dx;
|
||||||
|
let ypo = p1.y + pct * dy;
|
||||||
|
cntr.push(ypo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cntr.sort((a, b) => {
|
||||||
|
return b - a;
|
||||||
|
});
|
||||||
|
let lp, eo = 0;
|
||||||
|
for (let y of cntr) {
|
||||||
|
let p = newPoint(x, y, z);
|
||||||
|
if (eo++ % 2) {
|
||||||
|
let d = lp.distTo2D(p);
|
||||||
|
if (d >= min && d <= max) {
|
||||||
|
if (lines) {
|
||||||
|
cloud.push(lp);
|
||||||
|
cloud.push(p);
|
||||||
|
} else {
|
||||||
|
cloud.push(newPoint(
|
||||||
|
x, (lp.y + p.y) / 2, z
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lp = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lines) {
|
||||||
|
return cloud;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mindist = opt.mindist || step * 1.5;
|
||||||
|
|
||||||
|
function build(polyPoints) {
|
||||||
|
let lastp = polyPoints.last();
|
||||||
|
let minp;
|
||||||
|
let mind = Infinity;
|
||||||
|
for (let point of cloud) {
|
||||||
|
let dist = point.distTo2D(lastp);
|
||||||
|
if (dist < mindist && dist < mind) {
|
||||||
|
mind = dist;
|
||||||
|
minp = point;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (minp) {
|
||||||
|
cloud = cloud.filter(p => p !== minp);
|
||||||
|
polyPoints.push(minp);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// join points into polys
|
||||||
|
let polys = [];
|
||||||
|
let polyPoints = [];
|
||||||
|
while (cloud.length) {
|
||||||
|
if (polyPoints.length === 0) {
|
||||||
|
polyPoints = [cloud.shift()];
|
||||||
|
polys.push(polyPoints);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (build(polyPoints)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!polyPoints.flip) {
|
||||||
|
polyPoints.reverse();
|
||||||
|
polyPoints.flip = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (polyPoints.length) {
|
||||||
|
polyPoints = [];
|
||||||
|
} else {
|
||||||
|
throw "whoop there it is";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return polys
|
||||||
|
.filter(polyPoints => polyPoints.length > 1)
|
||||||
|
.map(polyPoints => {
|
||||||
|
let np = newPolygon().setOpen();
|
||||||
|
for (let p of polyPoints) {
|
||||||
|
np.push(p);
|
||||||
|
}
|
||||||
|
if (np.last().distTo2D(np.first()) <= max) {
|
||||||
|
np.setClosed();
|
||||||
|
}
|
||||||
|
np = np.clean();
|
||||||
|
return np;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split long straight lines into segments no longer than `max`.
|
||||||
|
* Returns a new polygon. Optionally `mark` points with their derived
|
||||||
|
* segment start point (hinting for thin walls), `wrap` the poly first
|
||||||
|
* point by appending to the end (adaptive walls), or stop segmentation at `maxoff`.
|
||||||
|
*
|
||||||
|
* @param {Polygon} poly - polygon to segment
|
||||||
|
* @param {number} max - maximum segment length (default 1)
|
||||||
|
* @param {boolean} mark - mark points with segment start (default false)
|
||||||
|
* @param {boolean} wrap - wrap first point to end (default false)
|
||||||
|
* @param {number} maxoff - maximum offset for segmentation (default Infinity)
|
||||||
|
* @returns {Polygon} new segmented polygon
|
||||||
|
*/
|
||||||
|
export function segment(poly, max = 1, mark = false, wrap = false, maxoff = Infinity) {
|
||||||
|
const newp = [];
|
||||||
|
const points = poly.points;
|
||||||
|
const length = points.length;
|
||||||
|
const l0 = poly.open ? length - 1 : length;
|
||||||
|
for (let i=0, p=points, l1=length, l2=l1+1; i<l0; i++) {
|
||||||
|
const p1 = p[i];
|
||||||
|
const p2 = p[(i + 1) % l1];
|
||||||
|
const dx = p2.x - p1.x;
|
||||||
|
const dy = p2.y - p1.y;
|
||||||
|
const dl = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
newp.push(p1);
|
||||||
|
if (mark) p1.segment = p1;
|
||||||
|
// if segment shorter than max (delta) or
|
||||||
|
// maxoff is exhausted (only segmenting up to some length)
|
||||||
|
// then skip sub-segmenting the current segment
|
||||||
|
if (dl < max || maxoff < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
maxoff -= dl;
|
||||||
|
const div = dl / max;
|
||||||
|
const fit = div | 0;
|
||||||
|
const add = fit - 1;
|
||||||
|
const ix = dx / fit;
|
||||||
|
const iy = dy / fit;
|
||||||
|
let ox = p1.x + ix;
|
||||||
|
let oy = p1.y + iy;
|
||||||
|
for (let i=0; i<add; i++) {
|
||||||
|
newp.push(newPoint(ox, oy, (p1.z + p2.z) / 2));
|
||||||
|
ox += ix;
|
||||||
|
oy += iy;
|
||||||
|
// mark new point with first point of originating segment
|
||||||
|
if (mark) newp.peek().segment = p1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newp.length > length) {
|
||||||
|
if (wrap) {
|
||||||
|
newp.push(newp[0]);
|
||||||
|
}
|
||||||
|
return newPolygon().addPoints(newp.map(p => p.clone(['segment']))).setOpenValue(poly.open);
|
||||||
|
}
|
||||||
|
return poly;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For any poly points closer than `dist`, replace them with their midpoint.
|
||||||
|
* Only used by cam/op-pocket.js.
|
||||||
|
*
|
||||||
|
* @param {Polygon} poly - polygon to process
|
||||||
|
* @param {number} dist - distance threshold (default 0.01)
|
||||||
|
* @returns {Polygon} new polygon with midpoints or original if no changes
|
||||||
|
*/
|
||||||
|
export function midpoints(poly, dist = 0.01) {
|
||||||
|
const newp = [];
|
||||||
|
const points = poly.points;
|
||||||
|
const length = points.length;
|
||||||
|
const l0 = poly.open ? length - 1 : length;
|
||||||
|
let mod = 0;
|
||||||
|
for (let i=0, p=points; i<l0; i++) {
|
||||||
|
const p1 = p[i];
|
||||||
|
const p2 = p[(i + 1) % length];
|
||||||
|
const dx = p2.x - p1.x;
|
||||||
|
const dy = p2.y - p1.y;
|
||||||
|
const ln = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
if (ln < dist) {
|
||||||
|
newp.push(p1.midPointTo(p2));
|
||||||
|
mod++;
|
||||||
|
} else {
|
||||||
|
newp.push(p1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mod) {
|
||||||
|
return newPolygon().addPoints(newp.map(p => p.clone())).setOpenValue(poly.open);
|
||||||
|
}
|
||||||
|
return poly;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add dogbone joints for CNC operations at sharp interior corners.
|
||||||
|
* Mutates the polygon in place. Recursively processes inner polygons.
|
||||||
|
*
|
||||||
|
* @param {Polygon} poly - polygon to modify (mutated)
|
||||||
|
* @param {number} dist - dogbone distance/size
|
||||||
|
* @param {boolean} reverse - reverse winding direction
|
||||||
|
* @returns {Polygon} the mutated polygon
|
||||||
|
*/
|
||||||
|
export function addDogbones(poly, dist, reverse) {
|
||||||
|
let open = poly.open;
|
||||||
|
let isCW = poly.isClockwise();
|
||||||
|
if (reverse || poly.parent) isCW = !isCW;
|
||||||
|
let oldpts = poly.points.slice();
|
||||||
|
let lastpt = oldpts[oldpts.length - 1];
|
||||||
|
let lastsl = lastpt.slopeTo(oldpts[0]).toUnit();
|
||||||
|
let length = oldpts.length + (open ? 0 : 1);
|
||||||
|
let newpts = [];
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
let nextpt = oldpts[i % oldpts.length];
|
||||||
|
let nextsl = lastpt.slopeTo(nextpt).toUnit();
|
||||||
|
let adiff = lastsl.angleDiff(nextsl, true);
|
||||||
|
let bdiff = ((adiff < 0 ? (180 - adiff) : (180 + adiff)) / 2) + 180;
|
||||||
|
if (!open || (i > 1 && i < length)) {
|
||||||
|
if (isCW && adiff > 45) {
|
||||||
|
let newa = newSlopeFromAngle(lastsl.angle + bdiff);
|
||||||
|
newpts.push(lastpt.projectOnSlope(newa, dist));
|
||||||
|
newpts.push(lastpt.clone());
|
||||||
|
} else if (!isCW && adiff < -45) {
|
||||||
|
let newa = newSlopeFromAngle(lastsl.angle - bdiff);
|
||||||
|
newpts.push(lastpt.projectOnSlope(newa, dist));
|
||||||
|
newpts.push(lastpt.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastsl = nextsl;
|
||||||
|
lastpt = nextpt;
|
||||||
|
if (i < oldpts.length) {
|
||||||
|
newpts.push(nextpt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
poly.points = newpts;
|
||||||
|
if (poly.inner) {
|
||||||
|
poly.inner.forEach(inner => addDogbones(inner, dist, true));
|
||||||
|
}
|
||||||
|
return poly;
|
||||||
|
}
|
||||||
1024
src/geo/polygon.js
1024
src/geo/polygon.js
File diff suppressed because it is too large
Load diff
|
|
@ -61,6 +61,7 @@ const POLYS = {
|
||||||
offset,
|
offset,
|
||||||
outer,
|
outer,
|
||||||
points,
|
points,
|
||||||
|
print,
|
||||||
rayIntersect,
|
rayIntersect,
|
||||||
reconnect,
|
reconnect,
|
||||||
renest,
|
renest,
|
||||||
|
|
@ -71,6 +72,7 @@ const POLYS = {
|
||||||
toClipper,
|
toClipper,
|
||||||
trimTo,
|
trimTo,
|
||||||
union,
|
union,
|
||||||
|
unionFaces,
|
||||||
xor,
|
xor,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -211,6 +213,15 @@ export function renest(polygons, deep) {
|
||||||
return nest(flatten(polygons, [], true), deep);
|
return nest(flatten(polygons, [], true), deep);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function print(polys, pad = ' ', buf) {
|
||||||
|
buf = buf || [];
|
||||||
|
for (let poly of polys) {
|
||||||
|
buf.push(`${pad} - ${poly.points.length}`);
|
||||||
|
if (poly.inner) print(poly.inner, pad + pad, buf);
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* todo use clipper polytree?
|
* todo use clipper polytree?
|
||||||
*
|
*
|
||||||
|
|
@ -480,13 +491,89 @@ export function union(polys, minarea, all, opt = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (i=0; i<out.length; i++) {
|
for (i=0; i<out.length; i++) {
|
||||||
if (out[i]) uset.push(out[i]);
|
if (out[i]) {
|
||||||
|
if (!minarea || out[i].area() >= minarea) {
|
||||||
|
uset.push(out[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
opt.changes = length(uset) - lpre;
|
opt.changes = length(uset) - lpre;
|
||||||
return uset;
|
return uset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively process triangle soup from mesh by bucketing them
|
||||||
|
* by grid cell locality with max depth. This minimizes the number
|
||||||
|
* of progressive small unions. Results in a few unions of larger polys.
|
||||||
|
* Optimized for unioning 1000+ triangular faces from mesh slicing.
|
||||||
|
*
|
||||||
|
* @param {Polygon[]} polys - array of polygons (typically triangular faces)
|
||||||
|
* @param {number} depth - recursion depth (internal, default 0)
|
||||||
|
* @returns {Polygon[]} unioned polygons
|
||||||
|
*/
|
||||||
|
export function unionFaces(polys, depth = 0) {
|
||||||
|
if (depth < 5 && polys.length > 1000) {
|
||||||
|
// Calculate bounding box of all triangles
|
||||||
|
let minX = Infinity, maxX = -Infinity;
|
||||||
|
let minY = Infinity, maxY = -Infinity;
|
||||||
|
for (let tri of polys) {
|
||||||
|
for (let pt of tri.points) {
|
||||||
|
minX = Math.min(minX, pt.x);
|
||||||
|
maxX = Math.max(maxX, pt.x);
|
||||||
|
minY = Math.min(minY, pt.y);
|
||||||
|
maxY = Math.max(maxY, pt.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create NxN grid
|
||||||
|
const gridSize = 3;
|
||||||
|
const cellWidth = (maxX - minX) / gridSize;
|
||||||
|
const cellHeight = (maxY - minY) / gridSize;
|
||||||
|
|
||||||
|
// Partition polygons into grid cells
|
||||||
|
const grid = Array(gridSize).fill(0).map(() => Array(gridSize).fill(0).map(() => []));
|
||||||
|
|
||||||
|
for (let i = 0; i < polys.length; i++) {
|
||||||
|
const poly = polys[i];
|
||||||
|
// Use polygon centroid to assign to cell
|
||||||
|
const bounds = poly.bounds;
|
||||||
|
const cx = (bounds.minx + bounds.maxx) / 2;
|
||||||
|
const cy = (bounds.miny + bounds.maxy) / 2;
|
||||||
|
|
||||||
|
// Clamp to grid bounds
|
||||||
|
let gx = Math.floor((cx - minX) / cellWidth);
|
||||||
|
let gy = Math.floor((cy - minY) / cellHeight);
|
||||||
|
gx = Math.max(0, Math.min(gridSize - 1, gx));
|
||||||
|
gy = Math.max(0, Math.min(gridSize - 1, gy));
|
||||||
|
|
||||||
|
grid[gx][gy].push(poly);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Union within each cell
|
||||||
|
const cellResults = [];
|
||||||
|
let totalInCell = 0;
|
||||||
|
for (let gx = 0; gx < gridSize; gx++) {
|
||||||
|
for (let gy = 0; gy < gridSize; gy++) {
|
||||||
|
const cellPolys = grid[gx][gy];
|
||||||
|
if (cellPolys.length > 0) {
|
||||||
|
totalInCell += cellPolys.length;
|
||||||
|
const cellUnion = unionFaces(cellPolys, depth + 1);
|
||||||
|
cellResults.push(...cellUnion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Union the cell results
|
||||||
|
polys = unionFaces(cellResults, depth + 1);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
polys = union(polys, 0, true, { wasm: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
return polys;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Polygon} poly clipping mask
|
* @param {Polygon} poly clipping mask
|
||||||
* @returns {?Polygon[]}
|
* @returns {?Polygon[]}
|
||||||
|
|
|
||||||
|
|
@ -380,7 +380,7 @@ export async function sliceZ(z, points, options = {}) {
|
||||||
|
|
||||||
let rval = { z, lines };
|
let rval = { z, lines };
|
||||||
if (groupFn) {
|
if (groupFn) {
|
||||||
let groups = groupFn(lines, z, options);
|
let groups = groupFn(lines, options);
|
||||||
if (options.xor) {
|
if (options.xor) {
|
||||||
groups = polygons.xor(groups);
|
groups = polygons.xor(groups);
|
||||||
}
|
}
|
||||||
|
|
@ -448,12 +448,12 @@ export async function sliceZ(z, points, options = {}) {
|
||||||
* interesection. Eliminate used points and repeat. Unjoined lines
|
* interesection. Eliminate used points and repeat. Unjoined lines
|
||||||
* are permitted and handled after all other cases are handled.
|
* are permitted and handled after all other cases are handled.
|
||||||
*
|
*
|
||||||
* @param {Line[]} input
|
* @param {Line[]} input array of lines (soup)
|
||||||
* @param {number} [index]
|
* @param {Object} opt options
|
||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
*/
|
*/
|
||||||
export function sliceConnect(input, z, opt = {}) {
|
export function sliceConnect(input, opt = {}) {
|
||||||
let { debug, both } = opt;
|
let { both, debug, dirty, strict } = opt;
|
||||||
|
|
||||||
if (both) {
|
if (both) {
|
||||||
if (debug) console.log('unable to connect lines sliced with "both" option');
|
if (debug) console.log('unable to connect lines sliced with "both" option');
|
||||||
|
|
@ -466,8 +466,8 @@ export function sliceConnect(input, z, opt = {}) {
|
||||||
output = [],
|
output = [],
|
||||||
connect = [],
|
connect = [],
|
||||||
emitted = 0,
|
emitted = 0,
|
||||||
forks = false,
|
forks = [], // points which contain forks
|
||||||
frays = false,
|
stubs = [], // dead end points
|
||||||
bridge = config.bridgeLineGapDistance,
|
bridge = config.bridgeLineGapDistance,
|
||||||
bridgeMax = config.bridgeLineGapDistanceMax,
|
bridgeMax = config.bridgeLineGapDistanceMax,
|
||||||
p1, p2, gl;
|
p1, p2, gl;
|
||||||
|
|
@ -480,11 +480,17 @@ export function sliceConnect(input, z, opt = {}) {
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add p2 to p1's group array
|
||||||
function addConnected(p1, p2) {
|
function addConnected(p1, p2) {
|
||||||
|
if (p1 === p2) {
|
||||||
|
// console.log('drop same point connect');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!p1.group) p1.group = [ p2 ];
|
if (!p1.group) p1.group = [ p2 ];
|
||||||
else p1.group.push(p2);
|
else p1.group.addOnce(p2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// calculate and store array perimeter length
|
||||||
function perimeter(array) {
|
function perimeter(array) {
|
||||||
if (!array.perimeter) {
|
if (!array.perimeter) {
|
||||||
array.perimeter = newPolygon().addPoints(array).perimeter();
|
array.perimeter = newPolygon().addPoints(array).perimeter();
|
||||||
|
|
@ -492,142 +498,230 @@ export function sliceConnect(input, z, opt = {}) {
|
||||||
return array.perimeter;
|
return array.perimeter;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// calculate and store array area
|
||||||
* follow points through connected lines to form candidate output paths
|
function area(array) {
|
||||||
*/
|
if (!array.area) {
|
||||||
function findNextPath(point, current, branches, depth = 1) {
|
array.area = newPolygon().addPoints(array).area();
|
||||||
let path = [];
|
|
||||||
if (current) {
|
|
||||||
current.push(path);
|
|
||||||
}
|
}
|
||||||
|
return array.area;
|
||||||
|
}
|
||||||
|
|
||||||
|
function debug_points() {
|
||||||
|
let clone = points.map(p => {
|
||||||
|
let pc = p.clone();
|
||||||
|
pc.group = p.group.slice();
|
||||||
|
return pc;
|
||||||
|
});
|
||||||
|
clone.sort((a,b) => a._key < b._key ? -1 : 1);
|
||||||
|
console.log({ points: clone });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* construct a new path starting at point (may be a branch)
|
||||||
|
* @param {Point} point current point we're following
|
||||||
|
* @param {Point} base first point for start of all paths
|
||||||
|
* @param {Array} branches collected branches when recursing
|
||||||
|
* @param {Integer} depth recursion depth
|
||||||
|
*/
|
||||||
|
function findNextPath(point, { base, branches, depth, path } = { }) {
|
||||||
|
let marked = [];
|
||||||
|
|
||||||
|
base = base ?? point;
|
||||||
|
branches = branches ?? [];
|
||||||
|
depth = depth ?? 1;
|
||||||
|
path = path ?? [];
|
||||||
|
|
||||||
|
branches.push(path);
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
// prevent point re-use
|
// if the next point is the root, emit closed path
|
||||||
|
if (point === base && (path.length || branches.length > 1)) {
|
||||||
|
if (debug) console.log('term at start');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we can't pass thru a used point
|
||||||
|
if (point.del) {
|
||||||
|
if (debug) console.log('term at used point');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// prevent passing through the same point twice in a search
|
||||||
point.del = true;
|
point.del = true;
|
||||||
|
|
||||||
// add point to path
|
// add point to path
|
||||||
path.push(point);
|
path.push(point);
|
||||||
|
|
||||||
|
// track marked points so they can be cleared
|
||||||
|
marked.push(point);
|
||||||
|
|
||||||
|
// find unused "next" points
|
||||||
let links = point.group.filter(p => !p.del);
|
let links = point.group.filter(p => !p.del);
|
||||||
|
|
||||||
// no need to recurse at the start
|
if (debug) console.log({ point, depth, path, links: links.length });
|
||||||
if (links.length === 2 && depth === 1) {
|
|
||||||
point = links[0];
|
|
||||||
// if (debug) console.log({start_mid: point, depth});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if fork in the road, follow all paths to their end
|
if (links.length === 0) {
|
||||||
// and find the longest path
|
// for dangling paths, terminate at any junction
|
||||||
let root = !current, nc;
|
if (debug) console.log('term at stub or no branch')
|
||||||
if (links.length > 1) {
|
break;
|
||||||
// if (debug) console.log('fork!', {links: links.length, depth, root});
|
} else if (links.length === 1) {
|
||||||
if (root) {
|
// one choice is easy, keep going
|
||||||
current = [ path ];
|
point = links[0];
|
||||||
branches = [ ];
|
} else if (links.length > 1) {
|
||||||
}
|
// if fork in the road, follow all paths to their end
|
||||||
|
// then find the path representing the shortest perimeter
|
||||||
|
if (debug) console.log('fork!', { base, links: links.length, depth });
|
||||||
if (branches.length < 500)
|
if (branches.length < 500)
|
||||||
for (let p of links) {
|
for (let p of links) {
|
||||||
branches.push(nc = current.slice());
|
findNextPath(p, {
|
||||||
let rpath = findNextPath(p, nc, branches, depth + 1);
|
base,
|
||||||
// allow point re-use in other path searches
|
branches,
|
||||||
for (let p of rpath) p.del = false;
|
depth: depth + 1,
|
||||||
|
marked,
|
||||||
|
path: path.slice()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// flatten and sort in ascending perimeter
|
break;
|
||||||
let flat = branches.map(b => b.flat()).sort((a,b) => {
|
|
||||||
return perimeter(b) - perimeter(a);
|
|
||||||
});
|
|
||||||
let npath = flat[0];
|
|
||||||
if (debug) console.log({
|
|
||||||
root,
|
|
||||||
branches: branches.slice(),
|
|
||||||
flat, path, npath
|
|
||||||
});
|
|
||||||
if (root) {
|
|
||||||
for (let p of npath) p.del = true;
|
|
||||||
return npath;
|
|
||||||
} else {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
// return root ? npath : path;
|
|
||||||
} else {
|
|
||||||
// choose next (unused) point
|
|
||||||
point = links[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// hit an open end or branch
|
|
||||||
if (!point || point.del) {
|
|
||||||
return path;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw "invalid state";
|
for (let point of marked) {
|
||||||
|
point.del = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we only return data at the start of recursion
|
||||||
|
if (depth === 1) {
|
||||||
|
// flatten and sort branch paths in ascending perimeter
|
||||||
|
branches.sort((a,b) => {
|
||||||
|
return area(b) - area(a);
|
||||||
|
});
|
||||||
|
let npath = branches[0];
|
||||||
|
if (debug) console.log({
|
||||||
|
branches,
|
||||||
|
npath,
|
||||||
|
path,
|
||||||
|
base
|
||||||
|
});
|
||||||
|
for (let p of npath) {
|
||||||
|
p.del = true;
|
||||||
|
}
|
||||||
|
return npath;
|
||||||
|
} else {
|
||||||
|
if (debug) console.log('end branch', { depth });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// emit a polygon if it can be cleaned and still have 2 or more points
|
// emit a polygon if it can be cleaned and still have 2 or more points
|
||||||
function emit(poly) {
|
function emit(poly) {
|
||||||
|
let gap = poly.first().distTo2D(poly.last());
|
||||||
|
if (poly.open && strict && gap > bridgeMax) {
|
||||||
|
if (debug) console.log('drop', gap, poly);
|
||||||
|
return;
|
||||||
|
}
|
||||||
emitted += poly.length;
|
emitted += poly.length;
|
||||||
if (!opt.dirty) poly = poly.clean();
|
if (!dirty) poly = poly.clean();
|
||||||
if (poly.length > 2 || true) output.push(poly);
|
if (poly.length > 2) output.push(poly);
|
||||||
if (debug) console.log('xray',poly);
|
if (debug) console.log('xray', poly);
|
||||||
}
|
}
|
||||||
|
|
||||||
// given an array of paths, emit longest to shortest
|
// given an array of paths, emit longest to shortest
|
||||||
// eliminating points from the paths as they are emitted
|
// eliminating points from the paths as they are emitted
|
||||||
// shorter paths any point eliminated are eliminated as candidates.
|
// shorter paths any point eliminated are eliminated as candidates.
|
||||||
function emitPath(path) {
|
function emitPath(path) {
|
||||||
|
// test if last point is in the first point's connected list
|
||||||
let closed = path[0].group.indexOf(path.peek()) >= 0;
|
let closed = path[0].group.indexOf(path.peek()) >= 0;
|
||||||
if (closed && path.length > 2) {
|
if (closed && path.length > 2) {
|
||||||
if (debug) console.log({ closed: path.length, path });
|
if (debug) console.log('emit', { closed: path.length, path });
|
||||||
emit(newPolygon().addPoints(path));
|
emit(newPolygon().addPoints(path));
|
||||||
} else if (path.length > 1) {
|
} else if (path.length > 2) {
|
||||||
let gap = path[0].distTo2D(path.peek()).round(4);
|
let gap = path[0].distTo2D(path.peek()).round(4);
|
||||||
if (debug) console.log({ open: path.length, gap, path });
|
if (gap < bridgeMax) {
|
||||||
connect.push(path);
|
connect.push(path);
|
||||||
|
}
|
||||||
|
if (debug) console.log('emit', { open: path.length, gap, path });
|
||||||
|
} else {
|
||||||
|
if (debug) console.log('emit', { drop: path });
|
||||||
}
|
}
|
||||||
|
if (debug) debug_points();
|
||||||
}
|
}
|
||||||
|
|
||||||
// create point map, unique point list and point group arrays
|
// create point map, unique point list and point group arrays
|
||||||
input.forEach(function(line) {
|
for (let line of input) {
|
||||||
p1 = cachedPoint(line.p1);
|
p1 = cachedPoint(line.p1);
|
||||||
p2 = cachedPoint(line.p2);
|
p2 = cachedPoint(line.p2);
|
||||||
addConnected(p1,p2);
|
addConnected(p1,p2);
|
||||||
addConnected(p2,p1);
|
addConnected(p2,p1);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// exclude dangling points (from lines with the same endpoints)
|
||||||
|
points = points.filter(point => point.group);
|
||||||
|
|
||||||
// console.log({points, forks: points.filter(p => p.group.length !== 2)});
|
// console.log({points, forks: points.filter(p => p.group.length !== 2)});
|
||||||
// for each unused point, find the longest non-intersecting path
|
// for each unused point, find the longest non-intersecting path
|
||||||
|
|
||||||
for (let point of points) {
|
for (let point of points) {
|
||||||
gl = point.group.length;
|
gl = point.group.length;
|
||||||
forks = forks || gl > 2;
|
if (gl > 2) forks.push(point);
|
||||||
frays = frays || gl < 2;
|
if (gl < 2) stubs.push(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
// show found forks and stubs in debug/xray mode
|
||||||
|
if (debug) {
|
||||||
|
console.log({ forks, stubs });
|
||||||
|
debug_points();
|
||||||
|
}
|
||||||
|
|
||||||
|
// for each dangling endpoint, try to find the closest dangling point
|
||||||
|
// in the soup which is less than the max bridging distance.
|
||||||
|
// if a candidate is found, synthesize a line between the endpoint
|
||||||
|
// and the candidate.
|
||||||
|
if (stubs.length) {
|
||||||
|
for (let i=0; i<stubs.length; i++) {
|
||||||
|
let min = { dist: Infinity };
|
||||||
|
let pi = stubs[i];
|
||||||
|
if (!pi) continue;
|
||||||
|
for (let j=1+i; j<stubs.length; j++) {
|
||||||
|
let pj = stubs[j];
|
||||||
|
if (!pj) continue;
|
||||||
|
let dist = pi.distTo2D(pj);
|
||||||
|
if (dist < min.dist) {
|
||||||
|
min.dist = dist;
|
||||||
|
min.p = pj;
|
||||||
|
min.j = j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (min.p && min.dist < bridgeMax) {
|
||||||
|
let pj = min.p;
|
||||||
|
if (debug) console.log({ connect_stubs: [ pi, pj ]});
|
||||||
|
input.push({ p1: pi, p2: pj });
|
||||||
|
addConnected(pi, pj);
|
||||||
|
addConnected(pj, pi);
|
||||||
|
if (pi.group.length > 2) forks.push(pi);
|
||||||
|
if (pj.group.length > 2) forks.push(pj);
|
||||||
|
stubs[i] = undefined;
|
||||||
|
stubs[min.j] = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stubs = stubs.filter(p => p);
|
||||||
|
if (debug) console.log({ filter_stubs: stubs });
|
||||||
}
|
}
|
||||||
if (debug && (forks || frays)) console.log({forks, frays});
|
|
||||||
|
|
||||||
// process paths starting with forks
|
// process paths starting with forks
|
||||||
if (forks) {
|
// find the path back to the starting point
|
||||||
|
// representing the greatest enclosed area
|
||||||
if (debug) console.log('process forks');
|
if (debug) console.log('process forks');
|
||||||
for (let point of points) {
|
for (let point of forks) {
|
||||||
// must not have been used and be a dangling end
|
// must not have been used and be a dangling end
|
||||||
if (!point.del && point.group.length > 2) {
|
if (!point.del && point.group.length > 2) {
|
||||||
let path = findNextPath(point);
|
let path = findNextPath(point);
|
||||||
if (path) emitPath(path);
|
if (path) emitPath(path);
|
||||||
}
|
}
|
||||||
} }
|
}
|
||||||
|
|
||||||
// process paths with dangling endpoints
|
// process remaining paths
|
||||||
if (frays) {
|
// find the path back to the starting point
|
||||||
if (debug) console.log('process frays');
|
// representing the greatest enclosed area
|
||||||
for (let point of points) {
|
if (debug) console.log('process norms');
|
||||||
// must not have been used and be a dangling end
|
|
||||||
if (!point.del && point.group.length === 1) {
|
|
||||||
let path = findNextPath(point);
|
|
||||||
if (path) emitPath(path);
|
|
||||||
}
|
|
||||||
} }
|
|
||||||
|
|
||||||
// process normal paths
|
|
||||||
if (debug) console.log('process mids');
|
|
||||||
for (let point of points) {
|
for (let point of points) {
|
||||||
// must not have been used and be a dangling end
|
// must not have been used and be a dangling end
|
||||||
if (!point.del) {
|
if (!point.del) {
|
||||||
|
|
@ -649,7 +743,7 @@ export function sliceConnect(input, z, opt = {}) {
|
||||||
// progressively connect open polygons within a bridge distance
|
// progressively connect open polygons within a bridge distance
|
||||||
let iter = 1000;
|
let iter = 1000;
|
||||||
let mingap;
|
let mingap;
|
||||||
if (true) do {
|
if (false) do {
|
||||||
mingap = Infinity;
|
mingap = Infinity;
|
||||||
|
|
||||||
outer: for (let i=0; i<connect.length; i++) {
|
outer: for (let i=0; i<connect.length; i++) {
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,18 @@
|
||||||
|
|
||||||
import { api } from './api.js';
|
import { api } from './api.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active alert records. Each record is an array: [message, timestamp, duration, active]
|
||||||
|
* @type {Array<[string, number, number, boolean]>}
|
||||||
|
*/
|
||||||
let alerts = [];
|
let alerts = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display an alert message to the user
|
||||||
|
* @param {string} message - The message to display
|
||||||
|
* @param {number} time - Duration in seconds to show the alert
|
||||||
|
* @returns {Array|undefined} Alert record [message, timestamp, duration, active] or result of update()
|
||||||
|
*/
|
||||||
function show(message, time) {
|
function show(message, time) {
|
||||||
if (message === undefined || message === null) {
|
if (message === undefined || message === null) {
|
||||||
return update(true);
|
return update(true);
|
||||||
|
|
@ -19,6 +29,11 @@ function show(message, time) {
|
||||||
return rec;
|
return rec;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide one or more alerts
|
||||||
|
* @param {Array|Array<Array>} rec - Single alert record or array of records to hide
|
||||||
|
* @param {Array<Array>} [recs] - Optional array of alert records (deprecated parameter style)
|
||||||
|
*/
|
||||||
function hide(rec, recs) {
|
function hide(rec, recs) {
|
||||||
if (Array.isArray(recs)) {
|
if (Array.isArray(recs)) {
|
||||||
for (let r of recs) {
|
for (let r of recs) {
|
||||||
|
|
@ -36,6 +51,11 @@ function hide(rec, recs) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the alert display by filtering expired/inactive alerts and rendering active ones.
|
||||||
|
* Filters alerts by age (based on duration) and active flag, limits display to 5 alerts.
|
||||||
|
* @param {boolean} [clear] - If true, clears all alerts before updating
|
||||||
|
*/
|
||||||
function update(clear) {
|
function update(clear) {
|
||||||
if (clear) {
|
if (clear) {
|
||||||
alerts = [];
|
alerts = [];
|
||||||
294
src/kiri/app/api.js
Normal file
294
src/kiri/app/api.js
Normal file
|
|
@ -0,0 +1,294 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
import '../../ext/base64.js';
|
||||||
|
|
||||||
|
import alerts from './alerts.js';
|
||||||
|
import settings from './conf/manager.js';
|
||||||
|
import STACKS from './stacks.js';
|
||||||
|
import web from '../../moto/webui.js';
|
||||||
|
|
||||||
|
import { beta, version } from '../../moto/license.js';
|
||||||
|
import { broker } from '../../moto/broker.js';
|
||||||
|
import { client as workers } from './workers.js';
|
||||||
|
import { consts, LISTS as lists } from './consts.js';
|
||||||
|
import { device, devices } from './devices.js';
|
||||||
|
import { functions } from './function.js';
|
||||||
|
import { group as groupModule } from './groups.js';
|
||||||
|
import { help as helpModule } from './help.js';
|
||||||
|
import { image as imageModule } from './image.js';
|
||||||
|
import { Index } from '../../data/index.js';
|
||||||
|
import { LANG } from './language.js';
|
||||||
|
import { local } from './local.js';
|
||||||
|
import { local as dataLocal } from '../../data/local.js';
|
||||||
|
import { modal } from './modal.js';
|
||||||
|
import { mode as modeModule, process as processModule } from './mode.js';
|
||||||
|
import { newWidget } from './widget.js';
|
||||||
|
import { noop, ajax, o2js, js2o, utils } from '../core/utils.js';
|
||||||
|
import { openFiles } from './files.js';
|
||||||
|
import { platform } from './platform.js';
|
||||||
|
import { selection } from './selected.js';
|
||||||
|
import { settingsUI } from './conf/dialog.js';
|
||||||
|
import { showDevices } from './devices.js';
|
||||||
|
import { showTools } from '../mode/cam/app/tools.js';
|
||||||
|
import { space as SPACE } from '../../moto/space.js';
|
||||||
|
import { stats } from './stats.js';
|
||||||
|
import { types as load } from '../../load/file.js';
|
||||||
|
import { UI } from './inputs.js';
|
||||||
|
import { updateTool } from '../mode/cam/app/tools.js';
|
||||||
|
import { view as viewModule } from './view-mode.js';
|
||||||
|
import { visuals } from './visuals.js';
|
||||||
|
import { widgets } from './widgets.js';
|
||||||
|
import { workspace } from './workspace.js';
|
||||||
|
|
||||||
|
// environment setup
|
||||||
|
let LOC = self.location,
|
||||||
|
EVENT = broker,
|
||||||
|
SETUP = utils.parseOpt(LOC.search.substring(1)),
|
||||||
|
FILES = openFiles(new Index(SETUP.d ? SETUP.d[0] : 'kiri')),
|
||||||
|
LOCAL = self.debug && !SETUP.remote,
|
||||||
|
SECURE = isSecure(LOC.protocol);
|
||||||
|
|
||||||
|
// todo: fix in widget.js b/c front-end and back-end do not share api
|
||||||
|
self.kiri_catalog = FILES;
|
||||||
|
FILES.show = () => modal.show('files');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broker compatibility patch - adds 'on' method as alias for subscribe
|
||||||
|
*/
|
||||||
|
EVENT.on = (topic, listener) => {
|
||||||
|
EVENT.subscribe(topic, listener);
|
||||||
|
return EVENT;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if protocol is secure (HTTPS).
|
||||||
|
* @param {string} proto - Protocol string (e.g., "https:", "http:")
|
||||||
|
* @returns {boolean} True if protocol starts with "https"
|
||||||
|
*/
|
||||||
|
function isSecure(proto) {
|
||||||
|
return proto.toLowerCase().indexOf("https") === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download data as a file using browser download mechanism.
|
||||||
|
* Creates temporary object URL and triggers download via hidden link click.
|
||||||
|
* @param {Blob|ArrayBuffer|string} data - Data to download
|
||||||
|
* @param {string} filename - Filename for download
|
||||||
|
*/
|
||||||
|
function download(data, filename) {
|
||||||
|
let url = window.URL.createObjectURL(new Blob([data], {type: "octet/stream"}));
|
||||||
|
$('mod-any').innerHTML = `<a id="_dexport_" href="${url}" download="${filename}">x</a>`;
|
||||||
|
$('_dexport_').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Busy state counter for tracking active operations */
|
||||||
|
let busyVal = 0,
|
||||||
|
/** Hover feature flag */
|
||||||
|
isHover = false,
|
||||||
|
/** Explicit undefined for object defaults */
|
||||||
|
undef = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* organize modules for easy use in other modules
|
||||||
|
* without having to know the paths to all of them
|
||||||
|
*/
|
||||||
|
export const api = {
|
||||||
|
ajax,
|
||||||
|
beta,
|
||||||
|
alerts,
|
||||||
|
/**
|
||||||
|
* Busy state management for tracking active operations.
|
||||||
|
* Emits 'busy' events when state changes.
|
||||||
|
*/
|
||||||
|
busy: {
|
||||||
|
val() { return busyVal },
|
||||||
|
inc() { api.event.emit("busy", ++busyVal) },
|
||||||
|
dec() { api.event.emit("busy", --busyVal) }
|
||||||
|
},
|
||||||
|
catalog: FILES,
|
||||||
|
client: workers,
|
||||||
|
clip(text) {
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(text)
|
||||||
|
.catch(err => console.error('Clipboard Error:', err));
|
||||||
|
},
|
||||||
|
clone: Object.clone,
|
||||||
|
conf: settings.conf,
|
||||||
|
const: {
|
||||||
|
LANG, LOCAL, SETUP, SECURE, SPACE, STACKS,
|
||||||
|
...consts,
|
||||||
|
URL: {
|
||||||
|
path: LOC.pathname,
|
||||||
|
hash: LOC.hash.substring(1),
|
||||||
|
query: LOC.search.substring(1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Development/debug utilities
|
||||||
|
*/
|
||||||
|
devel: {
|
||||||
|
get enabled() {
|
||||||
|
return settings.ctrl().devel;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Enable X-ray view of specific layers for debugging slicing.
|
||||||
|
* Converts layer indices to Z-heights and triggers re-slice.
|
||||||
|
* @param {number|number[]} layers - Layer index or array of indices to view
|
||||||
|
* @param {boolean} [raw] - If true, use raw values without height calculation
|
||||||
|
*/
|
||||||
|
xray(layers, raw) {
|
||||||
|
let proc = api.conf.get().process,
|
||||||
|
size = proc.sliceHeight || proc.slaSlice || 1,
|
||||||
|
base = (proc.firstSliceHeight || size);
|
||||||
|
layers = Array.isArray(layers) ? layers : [layers];
|
||||||
|
proc.xray = layers.map(l => raw ? l : base + l * size - size / 2);
|
||||||
|
proc.xrayi = layers.slice();
|
||||||
|
api.function.slice(
|
||||||
|
// () => SPACE.platform.showGrid(false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
device,
|
||||||
|
devices,
|
||||||
|
dialog: {
|
||||||
|
show: (which) => modal.show(which),
|
||||||
|
hide: () => modal.hide(),
|
||||||
|
update_process_list: settingsUI.update_list
|
||||||
|
},
|
||||||
|
doit: {
|
||||||
|
undo: noop, // do.js
|
||||||
|
redo: noop // do.js
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
alerts(clr) { alerts.update(clr) },
|
||||||
|
bind(t,m,o) { return EVENT.bind(t,m,o) },
|
||||||
|
emit(t,m,o) { return EVENT.publish(t,m,o) },
|
||||||
|
import() { api.ui.load.click() },
|
||||||
|
listeners(topic) { return EVENT.targets(topic) },
|
||||||
|
on(t,l) { EVENT.on(t,l); return api.event },
|
||||||
|
settings: settingsUI.trigger_event,
|
||||||
|
topics() { return EVENT.topics() }
|
||||||
|
},
|
||||||
|
electron: navigator.userAgent.includes('Electron'),
|
||||||
|
/**
|
||||||
|
* Feature flags and hook functions for customizing application behavior.
|
||||||
|
* Many of these are hook points for external integrations or plugins.
|
||||||
|
*/
|
||||||
|
feature: {
|
||||||
|
seed: true, // seed profiles on first use
|
||||||
|
meta: true, // show selected widget metadata
|
||||||
|
frame: true, // receive frame events
|
||||||
|
alert_event: false, // emit alerts as events instead of display
|
||||||
|
controls: true, // show or not side menus
|
||||||
|
device_filter: undef, // function to limit devices shown
|
||||||
|
drop_group: undef, // optional array to group multi drop
|
||||||
|
drop_layout: true, // layout on new drop
|
||||||
|
hoverAdds: false, // when true only searches widget additions
|
||||||
|
on_key: undef, // function override default key handlers
|
||||||
|
on_key2: [], // allows for multiple key handlers
|
||||||
|
on_load: undef, // function override file drop loads
|
||||||
|
on_add_stl: undef, // legacy override stl drop loads
|
||||||
|
on_mouse_up: undef, // function intercepts mouse up select
|
||||||
|
on_mouse_down: undef, // function intercepts mouse down
|
||||||
|
work_alerts: true, // allow disabling work progress alerts
|
||||||
|
pmode: consts.PMODES.SPEED, // preview modes
|
||||||
|
/**
|
||||||
|
* Hover feature flag. Setting this publishes a "feature.hover" event.
|
||||||
|
*/
|
||||||
|
get hover() {
|
||||||
|
return isHover;
|
||||||
|
},
|
||||||
|
set hover(b) {
|
||||||
|
isHover = b;
|
||||||
|
broker.publish("feature.hover", b);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function: functions,
|
||||||
|
group: groupModule,
|
||||||
|
help: helpModule,
|
||||||
|
hide: {
|
||||||
|
alert(rec, recs) { alerts.hide(...arguments) },
|
||||||
|
import: noop,
|
||||||
|
slider: visuals.hide_slider
|
||||||
|
},
|
||||||
|
image: imageModule,
|
||||||
|
js2o,
|
||||||
|
language: LANG,
|
||||||
|
lists,
|
||||||
|
load,
|
||||||
|
local,
|
||||||
|
modal,
|
||||||
|
mode: modeModule,
|
||||||
|
new: {
|
||||||
|
widget: newWidget
|
||||||
|
},
|
||||||
|
noop,
|
||||||
|
o2js,
|
||||||
|
onkey(fn) {
|
||||||
|
api.feature.on_key2.push(fn);
|
||||||
|
},
|
||||||
|
platform,
|
||||||
|
process: processModule,
|
||||||
|
sdb: dataLocal,
|
||||||
|
selection,
|
||||||
|
settings,
|
||||||
|
show: {
|
||||||
|
alert() { return alerts.show(...arguments) },
|
||||||
|
controls() { console.trace('deprecated') },
|
||||||
|
devices: showDevices,
|
||||||
|
import() { api.ui.import.style.display = '' },
|
||||||
|
layer: visuals.set_visible_layer,
|
||||||
|
local() { console.trace('deprecated') },
|
||||||
|
progress: visuals.set_progress,
|
||||||
|
slices: visuals.show_slices,
|
||||||
|
tools: showTools
|
||||||
|
},
|
||||||
|
space: workspace,
|
||||||
|
stacks: STACKS,
|
||||||
|
stats,
|
||||||
|
tool: {
|
||||||
|
update: updateTool
|
||||||
|
},
|
||||||
|
tweak: {
|
||||||
|
line_precision(v) { api.work.config({ base: { clipperClean: v } }) },
|
||||||
|
gcode_decimals(v) { api.work.config({ base: { gcode_decimals: v } }) }
|
||||||
|
},
|
||||||
|
uc: UI.prefix('kiri').inputAction(settings.conf.update),
|
||||||
|
ui: {},
|
||||||
|
util: {
|
||||||
|
isSecure,
|
||||||
|
download,
|
||||||
|
ui2rec() { api.conf.update_from(...arguments) },
|
||||||
|
rec2ui() { api.conf.update_fields(...arguments) },
|
||||||
|
/**
|
||||||
|
* Encode object to base64 string via JSON serialization
|
||||||
|
* @param {*} obj - Object to encode
|
||||||
|
* @returns {string} Base64 encoded string
|
||||||
|
*/
|
||||||
|
b64enc(obj) { return base64js.fromByteArray(new TextEncoder().encode(JSON.stringify(obj))) },
|
||||||
|
/**
|
||||||
|
* Decode base64 string to object via JSON parsing
|
||||||
|
* @param {string} obj - Base64 encoded string
|
||||||
|
* @returns {*} Decoded object
|
||||||
|
*/
|
||||||
|
b64dec(obj) { return JSON.parse(new TextDecoder().decode(base64js.toByteArray(obj))) }
|
||||||
|
},
|
||||||
|
// var: {
|
||||||
|
// layer_lo: 0,
|
||||||
|
// layer_hi: 0,
|
||||||
|
// layer_max: 0
|
||||||
|
// },
|
||||||
|
version,
|
||||||
|
view: {
|
||||||
|
...viewModule,
|
||||||
|
...visuals,
|
||||||
|
snapshot: null
|
||||||
|
},
|
||||||
|
visuals,
|
||||||
|
web,
|
||||||
|
widgets,
|
||||||
|
work: workers,
|
||||||
|
};
|
||||||
|
|
||||||
|
// allow widget to straddle client / worker FOR NOW
|
||||||
|
self.kiri_api = api;
|
||||||
69
src/kiri/app/color/base.js
Normal file
69
src/kiri/app/color/base.js
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BASE_SCHEME: Shared colors used across all device modes
|
||||||
|
* All mode-specific schemes inherit from this base
|
||||||
|
*/
|
||||||
|
const BASE_SCHEME = {
|
||||||
|
// Widget colors for selected/deselected states
|
||||||
|
widget: {
|
||||||
|
selected: {
|
||||||
|
light: [ 0xbbff00, 0xbbee00, 0xbbdd00, 0xbb9900 ],
|
||||||
|
dark: [ 0xbbff00, 0xbbee00, 0xbbdd00, 0xbb9900 ]
|
||||||
|
},
|
||||||
|
deselected: {
|
||||||
|
light: [ 0xffff00, 0xffdd00, 0xffbb00, 0xff9900 ],
|
||||||
|
dark: [ 0xffff00, 0xffdd00, 0xffbb00, 0xff9900 ]
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
// Computed via avgc(0x888888, baseColor, 3)
|
||||||
|
mixWith: 0x888888,
|
||||||
|
mixRatio: 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Edge rendering settings
|
||||||
|
edges: {
|
||||||
|
color: {
|
||||||
|
light: 0x888888,
|
||||||
|
dark: 0x444444
|
||||||
|
},
|
||||||
|
angle: 20 // Default edge detection angle threshold
|
||||||
|
},
|
||||||
|
|
||||||
|
// Wireframe rendering settings
|
||||||
|
wireframe: {
|
||||||
|
color: {
|
||||||
|
light: 0x000000,
|
||||||
|
dark: 0xaaaaaa
|
||||||
|
},
|
||||||
|
opacity: {
|
||||||
|
light: 0.5,
|
||||||
|
dark: 0.25
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Grid colors for platform
|
||||||
|
grid: {
|
||||||
|
major: {
|
||||||
|
light: 0x999999,
|
||||||
|
dark: 0x666666
|
||||||
|
},
|
||||||
|
minor: {
|
||||||
|
light: 0xcccccc,
|
||||||
|
dark: 0x333333
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// View-specific opacity defaults (common across modes)
|
||||||
|
views: {
|
||||||
|
ARRANGE: {
|
||||||
|
model_opacity: 1.0
|
||||||
|
},
|
||||||
|
SLICE: {
|
||||||
|
slicing_opacity: 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { BASE_SCHEME };
|
||||||
58
src/kiri/app/color/cam.js
Normal file
58
src/kiri/app/color/cam.js
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
import { BASE_SCHEME } from './base.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CAM_SCHEME: Colors specific to CAM (3-axis milling/machining) mode
|
||||||
|
* Inherits from BASE_SCHEME and overrides/extends with CAM-specific values
|
||||||
|
*/
|
||||||
|
const CAM_SCHEME = {
|
||||||
|
// Inherit all base colors
|
||||||
|
...BASE_SCHEME,
|
||||||
|
|
||||||
|
// CAM-specific view overrides
|
||||||
|
views: {
|
||||||
|
...BASE_SCHEME.views,
|
||||||
|
SLICE: {
|
||||||
|
...BASE_SCHEME.views.SLICE,
|
||||||
|
sliced_opacity: 0.2 // CAM shows more during slice (vs FDM 0.0)
|
||||||
|
},
|
||||||
|
PREVIEW: {
|
||||||
|
preview: {
|
||||||
|
light: 0xdddddd,
|
||||||
|
dark: 0x888888
|
||||||
|
},
|
||||||
|
preview_opacity: {
|
||||||
|
light: 0.2,
|
||||||
|
dark: 0.2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// CAM-specific operations
|
||||||
|
operations: {
|
||||||
|
// Tab visualization (persistent geometry)
|
||||||
|
tabs: {
|
||||||
|
color: {
|
||||||
|
light: 0x0000dd, // Blue in light mode
|
||||||
|
dark: 0x00ddff // Cyan in dark mode
|
||||||
|
},
|
||||||
|
opacity: {
|
||||||
|
light: 0.6,
|
||||||
|
dark: 0.75
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Gcode preview colors (same as FDM, but documented for CAM)
|
||||||
|
gcode: {
|
||||||
|
head: 0x888888,
|
||||||
|
move: {
|
||||||
|
light: 0xaaaaaa,
|
||||||
|
dark: 0x666666
|
||||||
|
},
|
||||||
|
print: 0x777700
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { CAM_SCHEME };
|
||||||
42
src/kiri/app/color/fdm.js
Normal file
42
src/kiri/app/color/fdm.js
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
import { BASE_SCHEME } from './base.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FDM_SCHEME: Colors specific to FDM (Fused Deposition Modeling) mode
|
||||||
|
* Inherits from BASE_SCHEME and overrides/extends with FDM-specific values
|
||||||
|
*/
|
||||||
|
const FDM_SCHEME = {
|
||||||
|
// Inherit all base colors
|
||||||
|
...BASE_SCHEME,
|
||||||
|
|
||||||
|
// FDM-specific view overrides
|
||||||
|
views: {
|
||||||
|
...BASE_SCHEME.views,
|
||||||
|
PREVIEW: {
|
||||||
|
preview_opacity: 0.0, // FDM hides widget in preview
|
||||||
|
sliced_opacity: 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// FDM-specific operations
|
||||||
|
operations: {
|
||||||
|
// Support painting overlay
|
||||||
|
paint: {
|
||||||
|
overlay: 0x4488ff, // Light blue for painted support regions
|
||||||
|
opacity: 0.7
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Gcode preview colors
|
||||||
|
gcode: {
|
||||||
|
head: 0x888888,
|
||||||
|
move: {
|
||||||
|
light: 0xaaaaaa,
|
||||||
|
dark: 0x666666
|
||||||
|
},
|
||||||
|
print: 0x777700
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { FDM_SCHEME };
|
||||||
28
src/kiri/app/color/laser.js
Normal file
28
src/kiri/app/color/laser.js
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
import { BASE_SCHEME } from './base.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LASER_SCHEME: Colors for LASER, DRAG (knife), WJET (waterjet), and WEDM (wire-edm) modes
|
||||||
|
* All 2D device types share similar visual characteristics
|
||||||
|
* Inherits from BASE_SCHEME with minimal overrides
|
||||||
|
*/
|
||||||
|
const LASER_SCHEME = {
|
||||||
|
// Inherit all base colors
|
||||||
|
...BASE_SCHEME,
|
||||||
|
|
||||||
|
// Laser-specific view overrides (similar to CAM)
|
||||||
|
views: {
|
||||||
|
...BASE_SCHEME.views,
|
||||||
|
PREVIEW: {
|
||||||
|
preview_opacity: 0.2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// DRAG, WJET, and WEDM all use the same scheme as LASER
|
||||||
|
const DRAG_SCHEME = LASER_SCHEME;
|
||||||
|
const WJET_SCHEME = LASER_SCHEME;
|
||||||
|
const WEDM_SCHEME = LASER_SCHEME;
|
||||||
|
|
||||||
|
export { LASER_SCHEME, DRAG_SCHEME, WJET_SCHEME, WEDM_SCHEME };
|
||||||
145
src/kiri/app/color/schemes.js
Normal file
145
src/kiri/app/color/schemes.js
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
import { FDM_SCHEME } from './fdm.js';
|
||||||
|
import { CAM_SCHEME } from './cam.js';
|
||||||
|
import { SLA_SCHEME } from './sla.js';
|
||||||
|
import { LASER_SCHEME, DRAG_SCHEME, WJET_SCHEME, WEDM_SCHEME } from './laser.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ColorSchemeRegistry: Central registry for all mode-specific color schemes
|
||||||
|
* Provides theme-aware color resolution for UI rendering
|
||||||
|
*/
|
||||||
|
class ColorSchemeRegistry {
|
||||||
|
constructor() {
|
||||||
|
// Map mode IDs to their schemes
|
||||||
|
this.schemes = {
|
||||||
|
1: FDM_SCHEME, // FDM mode
|
||||||
|
2: LASER_SCHEME, // LASER mode
|
||||||
|
3: CAM_SCHEME, // CAM mode
|
||||||
|
4: SLA_SCHEME, // SLA mode
|
||||||
|
5: DRAG_SCHEME, // DRAG knife mode
|
||||||
|
6: WJET_SCHEME, // Waterjet mode
|
||||||
|
7: WEDM_SCHEME // Wire-EDM mode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the complete scheme for a given mode and theme
|
||||||
|
* @param {number|string} mode - Mode ID (1=FDM, 2=LASER, 3=CAM, etc) or name
|
||||||
|
* @param {string} theme - 'light' or 'dark'
|
||||||
|
* @returns {object} Resolved scheme object
|
||||||
|
*/
|
||||||
|
getScheme(mode, theme = 'light') {
|
||||||
|
// Convert mode name to ID if needed
|
||||||
|
if (typeof mode === 'string') {
|
||||||
|
mode = this._modeNameToId(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheme = this.schemes[mode];
|
||||||
|
if (!scheme) {
|
||||||
|
console.warn(`Unknown mode: ${mode}, using FDM scheme as fallback`);
|
||||||
|
return this._resolveTheme(this.schemes[1], theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._resolveTheme(scheme, theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific color value from a scheme
|
||||||
|
* @param {number|string} mode - Mode ID or name
|
||||||
|
* @param {string} theme - 'light' or 'dark'
|
||||||
|
* @param {string} path - Dot-separated path (e.g., 'widget.selected', 'edges.color')
|
||||||
|
* @returns {*} Color value or object
|
||||||
|
*/
|
||||||
|
getColor(mode, theme, path) {
|
||||||
|
const scheme = this.getScheme(mode, theme);
|
||||||
|
return this._getNestedValue(scheme, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute disabled color by mixing base color with gray
|
||||||
|
* Implements: avgc(0x888888, baseColor, 3)
|
||||||
|
* @param {number} baseColor - Base color hex value
|
||||||
|
* @returns {number} Mixed color hex value
|
||||||
|
*/
|
||||||
|
computeDisabledColor(baseColor) {
|
||||||
|
// Extract RGB components
|
||||||
|
const r1 = (0x888888 >> 16) & 0xFF;
|
||||||
|
const g1 = (0x888888 >> 8) & 0xFF;
|
||||||
|
const b1 = 0x888888 & 0xFF;
|
||||||
|
|
||||||
|
const r2 = (baseColor >> 16) & 0xFF;
|
||||||
|
const g2 = (baseColor >> 8) & 0xFF;
|
||||||
|
const b2 = baseColor & 0xFF;
|
||||||
|
|
||||||
|
// Average with ratio 3:1 (3 parts gray, 1 part base)
|
||||||
|
const r = Math.floor((r1 * 3 + r2) / 4);
|
||||||
|
const g = Math.floor((g1 * 3 + g2) / 4);
|
||||||
|
const b = Math.floor((b1 * 3 + b2) / 4);
|
||||||
|
|
||||||
|
return (r << 16) | (g << 8) | b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve theme-specific values throughout the scheme
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_resolveTheme(scheme, theme) {
|
||||||
|
const resolved = JSON.parse(JSON.stringify(scheme)); // Deep clone
|
||||||
|
|
||||||
|
this._resolveThemeRecursive(resolved, theme);
|
||||||
|
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively resolve theme values in an object
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_resolveThemeRecursive(obj, theme) {
|
||||||
|
for (const key in obj) {
|
||||||
|
const value = obj[key];
|
||||||
|
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
// Check if this object has light/dark properties
|
||||||
|
if ('light' in value && 'dark' in value) {
|
||||||
|
// Replace the object with the theme-specific value
|
||||||
|
obj[key] = value[theme];
|
||||||
|
} else {
|
||||||
|
// Recurse into nested objects
|
||||||
|
this._resolveThemeRecursive(value, theme);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get nested value from object using dot-separated path
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_getNestedValue(obj, path) {
|
||||||
|
return path.split('.').reduce((current, key) => current?.[key], obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert mode name to ID
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_modeNameToId(name) {
|
||||||
|
const modes = {
|
||||||
|
'FDM': 1,
|
||||||
|
'LASER': 2,
|
||||||
|
'CAM': 3,
|
||||||
|
'SLA': 4,
|
||||||
|
'DRAG': 5,
|
||||||
|
'WJET': 6,
|
||||||
|
'WEDM': 7
|
||||||
|
};
|
||||||
|
return modes[name.toUpperCase()] || 1; // Default to FDM
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create singleton instance
|
||||||
|
const colorSchemeRegistry = new ColorSchemeRegistry();
|
||||||
|
|
||||||
|
export { colorSchemeRegistry, ColorSchemeRegistry };
|
||||||
23
src/kiri/app/color/sla.js
Normal file
23
src/kiri/app/color/sla.js
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
import { BASE_SCHEME } from './base.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SLA_SCHEME: Colors specific to SLA (stereolithography/resin) mode
|
||||||
|
* Inherits from BASE_SCHEME with minimal overrides
|
||||||
|
*/
|
||||||
|
const SLA_SCHEME = {
|
||||||
|
// Inherit all base colors
|
||||||
|
...BASE_SCHEME,
|
||||||
|
|
||||||
|
// SLA-specific view overrides (similar to FDM)
|
||||||
|
views: {
|
||||||
|
...BASE_SCHEME.views,
|
||||||
|
PREVIEW: {
|
||||||
|
preview_opacity: 0.0,
|
||||||
|
sliced_opacity: 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { SLA_SCHEME };
|
||||||
|
|
@ -261,7 +261,9 @@ export const conf = {
|
||||||
// process defaults FDM:Process
|
// process defaults FDM:Process
|
||||||
p:{
|
p:{
|
||||||
antiBacklash: 0,
|
antiBacklash: 0,
|
||||||
arcTolerance: 0,
|
fdmArcEnabled: false,
|
||||||
|
fdmArcResolution: 1,
|
||||||
|
fdmArcTolerance: 0.005,
|
||||||
firstLayerBedTemp: 0,
|
firstLayerBedTemp: 0,
|
||||||
firstLayerBeltBump: 0,
|
firstLayerBeltBump: 0,
|
||||||
firstLayerBeltLead: 3,
|
firstLayerBeltLead: 3,
|
||||||
|
|
@ -335,18 +337,17 @@ export const conf = {
|
||||||
sliceShells: 3,
|
sliceShells: 3,
|
||||||
sliceSolidify: false,
|
sliceSolidify: false,
|
||||||
sliceSolidMinArea: 1,
|
sliceSolidMinArea: 1,
|
||||||
|
sliceSolidMinThick: 0.4,
|
||||||
sliceSupportAngle: 50,
|
sliceSupportAngle: 50,
|
||||||
sliceSupportArea: 0.1,
|
|
||||||
sliceSupportDensity: 0.1,
|
sliceSupportDensity: 0.1,
|
||||||
sliceSupportEnable: false,
|
|
||||||
sliceSupportExtra: 0,
|
sliceSupportExtra: 0,
|
||||||
sliceSupportGap: 1,
|
sliceSupportGap: 1,
|
||||||
sliceSupportGrow: 0,
|
|
||||||
sliceSupportNozzle: 0,
|
sliceSupportNozzle: 0,
|
||||||
sliceSupportOffset: 1.0,
|
sliceSupportOffset: 1.0,
|
||||||
sliceSupportOutline: true,
|
sliceSupportOutline: true,
|
||||||
sliceSupportSize: 5,
|
|
||||||
sliceSupportSpan: 5,
|
sliceSupportSpan: 5,
|
||||||
|
sliceSupportType: "disabled",
|
||||||
|
sliceSupportTree: false,
|
||||||
sliceTopLayers: 3,
|
sliceTopLayers: 3,
|
||||||
sliceZInterleave: false,
|
sliceZInterleave: false,
|
||||||
zHopDistance: 0.2,
|
zHopDistance: 0.2,
|
||||||
|
|
@ -443,9 +444,12 @@ export const conf = {
|
||||||
camAreaExpand: 0,
|
camAreaExpand: 0,
|
||||||
camAreaRefine: 0,
|
camAreaRefine: 0,
|
||||||
camAreaSmooth: 1,
|
camAreaSmooth: 1,
|
||||||
|
camAreaShadow: false,
|
||||||
camAreaDogbones: false,
|
camAreaDogbones: false,
|
||||||
camAreaRevbones: false,
|
camAreaRevbones: false,
|
||||||
camAreaOutline: false,
|
camAreaOutline: false,
|
||||||
|
camAreaWalls: false,
|
||||||
|
camAreaZigZag: true,
|
||||||
camContourAngle: 85,
|
camContourAngle: 85,
|
||||||
camContourBottom: false,
|
camContourBottom: false,
|
||||||
camContourBridge: 0,
|
camContourBridge: 0,
|
||||||
|
|
@ -517,6 +521,8 @@ export const conf = {
|
||||||
camLatheAngle: 1,
|
camLatheAngle: 1,
|
||||||
camLatheLinear: true,
|
camLatheLinear: true,
|
||||||
camLatheOver: 0.1,
|
camLatheOver: 0.1,
|
||||||
|
camLatheDown: 0,
|
||||||
|
camLatheExpand: 0,
|
||||||
camLatheSpeed: 500,
|
camLatheSpeed: 500,
|
||||||
camLatheSpindle: 1000,
|
camLatheSpindle: 1000,
|
||||||
camLatheTool: 1000,
|
camLatheTool: 1000,
|
||||||
|
|
@ -601,6 +607,7 @@ export const conf = {
|
||||||
camTraceLines: false,
|
camTraceLines: false,
|
||||||
camTraceMerge: false,
|
camTraceMerge: false,
|
||||||
camTraceOffOver: 0,
|
camTraceOffOver: 0,
|
||||||
|
camTraceOffset: "none",
|
||||||
camTraceOver: 0.5,
|
camTraceOver: 0.5,
|
||||||
camTracePlunge: 200,
|
camTracePlunge: 200,
|
||||||
camTraceSpeed: 250,
|
camTraceSpeed: 250,
|
||||||
|
|
@ -614,7 +621,6 @@ export const conf = {
|
||||||
camZBottom: 0,
|
camZBottom: 0,
|
||||||
camZClearance: 1,
|
camZClearance: 1,
|
||||||
camZOffset: 0,
|
camZOffset: 0,
|
||||||
// camZThru: 0,
|
|
||||||
camZTop: 0,
|
camZTop: 0,
|
||||||
op2: [], // flip ops
|
op2: [], // flip ops
|
||||||
ops: [], // current ops
|
ops: [], // current ops
|
||||||
|
|
@ -998,6 +1004,7 @@ export const conf = {
|
||||||
widget: {},
|
widget: {},
|
||||||
// legacy localStorage settings (like octo print)
|
// legacy localStorage settings (like octo print)
|
||||||
local: {
|
local: {
|
||||||
|
'model.opacity': 1.0,
|
||||||
'model.edges': false,
|
'model.edges': false,
|
||||||
'cam.anim.trans': true,
|
'cam.anim.trans': true,
|
||||||
'cam.anim.model': false,
|
'cam.anim.model': false,
|
||||||
123
src/kiri/app/conf/dialog.js
Normal file
123
src/kiri/app/conf/dialog.js
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
import { api } from '../api.js';
|
||||||
|
import { modal } from '../modal.js';
|
||||||
|
import { settings } from './manager.js';
|
||||||
|
import { version } from '../../../moto/license.js';
|
||||||
|
|
||||||
|
const DOC = self.document;
|
||||||
|
|
||||||
|
function triggerSettingsEvent() {
|
||||||
|
api.event.emit('settings', settings.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
function editSettings(e) {
|
||||||
|
let current = settings.get(),
|
||||||
|
mode = settings.mode(),
|
||||||
|
name = e.target.getAttribute("name"),
|
||||||
|
load = current.sproc[mode][name],
|
||||||
|
loadstr = JSON.stringify(load,null,4).split('\n');
|
||||||
|
api.uc.prompt(`settings for "${name}"`, loadstr).then(edit => {
|
||||||
|
if (edit) {
|
||||||
|
try {
|
||||||
|
current.sproc[mode][name] = JSON.parse(edit);
|
||||||
|
if (name === settings.proc().processName) {
|
||||||
|
api.conf.load(null, name);
|
||||||
|
}
|
||||||
|
api.conf.save();
|
||||||
|
api.settings.sync.put();
|
||||||
|
} catch (e) {
|
||||||
|
console.log({ malformed_settings: e });
|
||||||
|
api.uc.alert('malformed settings object');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportSettings(e) {
|
||||||
|
let current = settings.get(),
|
||||||
|
mode = settings.mode(),
|
||||||
|
name = e.target.getAttribute("name"),
|
||||||
|
data = api.util.b64enc({
|
||||||
|
process: current.sproc[mode][name],
|
||||||
|
version,
|
||||||
|
moto: moto.id,
|
||||||
|
time: Date.now(),
|
||||||
|
mode,
|
||||||
|
name
|
||||||
|
});
|
||||||
|
api.uc.prompt("Export Process Filename", name).then(name => {
|
||||||
|
if (name) {
|
||||||
|
api.util.download(data, `${name}.km`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteSettings(e) {
|
||||||
|
let current = settings.get();
|
||||||
|
let name = e.target.getAttribute("del");
|
||||||
|
delete current.sproc[settings.mode()][name];
|
||||||
|
api.settings.sync.put();
|
||||||
|
updateProcessList();
|
||||||
|
api.conf.save();
|
||||||
|
triggerSettingsEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProcessList() {
|
||||||
|
let current = settings.get();
|
||||||
|
let list = [], s = current, sp = s.sproc[settings.mode()] || {}, table = api.ui.settingsList;
|
||||||
|
table.innerHTML = '';
|
||||||
|
for (let k in sp) {
|
||||||
|
if (sp.hasOwnProperty(k)) list.push(k);
|
||||||
|
}
|
||||||
|
list.filter(n => n !=='default').sort().forEach(function(sk) {
|
||||||
|
let row = DOC.createElement('div'),
|
||||||
|
load = DOC.createElement('button'),
|
||||||
|
edit = DOC.createElement('button'),
|
||||||
|
xprt = DOC.createElement('button'),
|
||||||
|
del = DOC.createElement('button'),
|
||||||
|
name = sk;
|
||||||
|
|
||||||
|
load.setAttribute('load', sk);
|
||||||
|
load.onclick = (ev) => {
|
||||||
|
api.conf.load(undefined, sk);
|
||||||
|
updateProcessList();
|
||||||
|
modal.hide();
|
||||||
|
}
|
||||||
|
load.appendChild(DOC.createTextNode(sk));
|
||||||
|
if (sk == settings.proc().processName) {
|
||||||
|
load.setAttribute('class', 'selected')
|
||||||
|
}
|
||||||
|
api.ui.settingsName.value = settings.proc().processName;
|
||||||
|
|
||||||
|
del.setAttribute('del', sk);
|
||||||
|
del.setAttribute('title', "remove '"+sk+"'");
|
||||||
|
del.innerHTML = '<i class="far fa-trash-alt"></i>';
|
||||||
|
del.onclick = deleteSettings;
|
||||||
|
|
||||||
|
edit.setAttribute('name', sk);
|
||||||
|
edit.setAttribute('title', 'edit');
|
||||||
|
edit.innerHTML = '<i class="far fa-edit"></i>';
|
||||||
|
edit.onclick = editSettings;
|
||||||
|
|
||||||
|
xprt.setAttribute('name', sk);
|
||||||
|
xprt.setAttribute('title', 'export');
|
||||||
|
xprt.innerHTML = '<i class="fas fa-download"></i>';
|
||||||
|
xprt.onclick = exportSettings;
|
||||||
|
|
||||||
|
row.setAttribute("class", "flow-row");
|
||||||
|
row.appendChild(edit);
|
||||||
|
row.appendChild(load);
|
||||||
|
row.appendChild(xprt);
|
||||||
|
row.appendChild(del);
|
||||||
|
table.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const settingsUI = {
|
||||||
|
edit: editSettings,
|
||||||
|
export: exportSettings,
|
||||||
|
delete: deleteSettings,
|
||||||
|
update_list: updateProcessList,
|
||||||
|
trigger_event: triggerSettingsEvent
|
||||||
|
};
|
||||||
|
|
@ -1,20 +1,31 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
import { $, h } from '../../moto/webui.js';
|
import { $, h } from '../../../moto/webui.js';
|
||||||
import { api } from './api.js';
|
import { api } from '../api.js';
|
||||||
import { conf } from './conf.js';
|
import { conf } from './defaults.js';
|
||||||
import { codec } from './codec.js';
|
import { codec } from '../../core/codec.js';
|
||||||
import { consts } from './consts.js';
|
import { consts } from '../consts.js';
|
||||||
import { space } from '../../moto/space.js';
|
import { space } from '../../../moto/space.js';
|
||||||
import { local } from '../../data/local.js';
|
import { local } from '../../../data/local.js';
|
||||||
import { utils } from './utils.js';
|
import { newWidget } from '../widget.js';
|
||||||
import { version } from '../../moto/license.js';
|
import { utils } from '../../core/utils.js';
|
||||||
import { JSZip } from '../../ext/jszip-esm.js';
|
import { version } from '../../../moto/license.js';
|
||||||
|
import { JSZip } from '../../../ext/jszip-esm.js';
|
||||||
|
|
||||||
|
import { colorSchemeRegistry } from '../color/schemes.js';
|
||||||
|
|
||||||
const { areEqual, ls2o, js2o } = utils;
|
const { areEqual, ls2o, js2o } = utils;
|
||||||
const { COLOR } = consts;
|
|
||||||
const { clone } = Object;
|
const { clone } = Object;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current color scheme
|
||||||
|
*/
|
||||||
|
function getColorScheme() {
|
||||||
|
const mode = api.mode.get_id();
|
||||||
|
const theme = api.space.is_dark() ? 'dark' : 'light';
|
||||||
|
return colorSchemeRegistry.getScheme(mode, theme);
|
||||||
|
}
|
||||||
|
|
||||||
const localFilterKey ='kiri-gcode-filters';
|
const localFilterKey ='kiri-gcode-filters';
|
||||||
const localFilters = js2o(local.getItem(localFilterKey)) || [];
|
const localFilters = js2o(local.getItem(localFilterKey)) || [];
|
||||||
|
|
||||||
|
|
@ -115,7 +126,7 @@ function updateSettings(opt = {}) {
|
||||||
if (settings.mode === 'FDM' && api.view.is_slice()) {
|
if (settings.mode === 'FDM' && api.view.is_slice()) {
|
||||||
let changes = {};
|
let changes = {};
|
||||||
let values = process;
|
let values = process;
|
||||||
let { layer_lo, layer_hi, layer_max } = api.var;
|
const { lo: layer_lo, hi: layer_hi, max: layer_max } = api.slider.getRange();
|
||||||
let range = { lo: layer_lo, hi: layer_hi };
|
let range = { lo: layer_lo, hi: layer_hi };
|
||||||
let add = false;
|
let add = false;
|
||||||
if (layer_lo > 0 || layer_hi < layer_max) {
|
if (layer_lo > 0 || layer_hi < layer_max) {
|
||||||
|
|
@ -321,9 +332,10 @@ function updateExtruderFields(device) {
|
||||||
onclick() {
|
onclick() {
|
||||||
api.selection.for_widgets(w => {
|
api.selection.for_widgets(w => {
|
||||||
w.anno.extruder = i;
|
w.anno.extruder = i;
|
||||||
|
const scheme = getColorScheme();
|
||||||
w.setColor(
|
w.setColor(
|
||||||
api.selection.contains(w) ?
|
api.selection.contains(w) ?
|
||||||
COLOR.selected : COLOR.deselected,
|
scheme.widget.selected : scheme.widget.deselected,
|
||||||
undefined,
|
undefined,
|
||||||
false);
|
false);
|
||||||
});
|
});
|
||||||
|
|
@ -331,7 +343,7 @@ function updateExtruderFields(device) {
|
||||||
}),
|
}),
|
||||||
h.div({
|
h.div({
|
||||||
class: "splat",
|
class: "splat",
|
||||||
style: `background-color: #${COLOR.deselected[i].toString(16)}`
|
style: `background-color: #${getColorScheme().widget.deselected[i].toString(16)}`
|
||||||
})
|
})
|
||||||
])));
|
])));
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -450,8 +462,8 @@ function updateFieldsFromRange() {
|
||||||
let match = 0;
|
let match = 0;
|
||||||
let values = {};
|
let values = {};
|
||||||
let restores = Object.clone(overrides);
|
let restores = Object.clone(overrides);
|
||||||
let { layer_lo, layer_hi } = api.var;
|
const { lo: layer_lo, hi: layer_hi } = api.slider.getRange();
|
||||||
for (let range of getOverlappingRanges(api.var.layer_lo, api.var.layer_hi)) {
|
for (let range of getOverlappingRanges(layer_lo, layer_hi)) {
|
||||||
for (let key of Object.keys(range.fields)) {
|
for (let key of Object.keys(range.fields)) {
|
||||||
values[key] = range.fields[key];
|
values[key] = range.fields[key];
|
||||||
overrides[key] = settings.process[key];
|
overrides[key] = settings.process[key];
|
||||||
|
|
@ -608,6 +620,7 @@ function settingsImport(data, ask) {
|
||||||
work.type = 100;
|
work.type = 100;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
codec.setFactory(codec.TYPE.WIDGET, newWidget);
|
||||||
codec.decode(data.work).forEach(widget => {
|
codec.decode(data.work).forEach(widget => {
|
||||||
api.platform.add(widget, 0, true, true);
|
api.platform.add(widget, 0, true, true);
|
||||||
});
|
});
|
||||||
|
|
@ -756,11 +769,11 @@ function settingsPrusaConvert(data) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setThreaded(bool) {
|
// function setThreaded(bool) {
|
||||||
bool = bool ? true : false;
|
// bool = bool ? true : false;
|
||||||
settings.controller.threaded = bool;
|
// settings.controller.threaded = bool;
|
||||||
api.event.emit("set.threaded", bool);
|
// api.event.emit("set.threaded", bool);
|
||||||
}
|
// }
|
||||||
|
|
||||||
function setEnableWASM(bool) {
|
function setEnableWASM(bool) {
|
||||||
bool = bool ? true : false;
|
bool = bool ? true : false;
|
||||||
|
|
@ -786,7 +799,8 @@ const apiSet = {
|
||||||
update_fields: updateFieldsFromSettings,
|
update_fields: updateFieldsFromSettings,
|
||||||
update_fields_from_range: updateFieldsFromRange,
|
update_fields_from_range: updateFieldsFromRange,
|
||||||
set_enable_wasm: setEnableWASM,
|
set_enable_wasm: setEnableWASM,
|
||||||
set_threaded: setThreaded,
|
// set_threaded: setThreaded,
|
||||||
|
set_threaded() { console.trace('set_threaded deprecated' )},
|
||||||
restore: restoreSettings,
|
restore: restoreSettings,
|
||||||
export: settingsExport,
|
export: settingsExport,
|
||||||
import: settingsImport,
|
import: settingsImport,
|
||||||
129
src/kiri/app/conf/settings.js
Normal file
129
src/kiri/app/conf/settings.js
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
import { $ } from '../../../moto/webui.js';
|
||||||
|
import { api } from '../api.js';
|
||||||
|
import { settings as set_ctrl } from './manager.js';
|
||||||
|
|
||||||
|
function export_device(exp, name) {
|
||||||
|
name = (name || "device")
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ /g,'_')
|
||||||
|
.replace(/\./g,'_');
|
||||||
|
api.uc.prompt("Export Device Filename", name).then(name => {
|
||||||
|
if (name) {
|
||||||
|
api.util.download(exp, `${name}.km`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function export_objects(format = "stl") {
|
||||||
|
// return selection.export();
|
||||||
|
api.uc.confirm("Export Filename", {ok:true, cancel: false}, `selected.${format}`).then(name => {
|
||||||
|
if (!name) return;
|
||||||
|
if (name.toLowerCase().indexOf(`.${format}`) < 0) {
|
||||||
|
name = `${name}.${format}`;
|
||||||
|
}
|
||||||
|
api.util.download(api.selection.export(format), name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function export_profile() {
|
||||||
|
const opt = {pre: [
|
||||||
|
"<div class='f-col a-center gap5 mlr10'>",
|
||||||
|
" <h3>Workspace Export</h3>",
|
||||||
|
" <label>This will create a backup of your</label>",
|
||||||
|
" <label>workspace, devices, and settings</label>",
|
||||||
|
" <span class='mt10'><input id='excwork' type='checkbox'> Exclude meshes</span>",
|
||||||
|
"</div>"
|
||||||
|
]};
|
||||||
|
let suggestion = "workspace";
|
||||||
|
let file = api.widgets.all()[0]?.meta.file || '';
|
||||||
|
if (file) {
|
||||||
|
suggestion = `${suggestion}_${file.split('.')[0]}`.replaceAll(' ','_');
|
||||||
|
};
|
||||||
|
api.uc.confirm("Filename", {ok:true, cancel: false}, suggestion, opt).then(name => {
|
||||||
|
if (!name) return;
|
||||||
|
|
||||||
|
let work = !$('excwork').checked;
|
||||||
|
let json = api.conf.export({work, clear:true});
|
||||||
|
|
||||||
|
api.client.zip([
|
||||||
|
{name:"workspace.json", data:JSON.stringify(json)}
|
||||||
|
], progress => {
|
||||||
|
api.show.progress(progress.percent/100, "compressing workspace");
|
||||||
|
}, output => {
|
||||||
|
api.show.progress(0);
|
||||||
|
api.util.download(output, `${name}.kmz`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function new_workspace() {
|
||||||
|
api.uc.confirm("Clear Workspace?", {ok:true, cancel: false}).then(value => {
|
||||||
|
if (value === true) {
|
||||||
|
let proc = set_ctrl.proc();
|
||||||
|
proc.ops && (proc.ops.length = 0);
|
||||||
|
proc.op2 && (proc.op2.length = 0);
|
||||||
|
api.platform.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function settings_save(ev, name) {
|
||||||
|
if (ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
api.dialog.hide();
|
||||||
|
let mode = api.mode.get(),
|
||||||
|
s = api.conf.get(),
|
||||||
|
def = "default",
|
||||||
|
cp = s.process,
|
||||||
|
pl = s.sproc[mode],
|
||||||
|
lp = s.cproc[mode],
|
||||||
|
saveAs = (name) => {
|
||||||
|
if (!name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let np = pl[name] = {};
|
||||||
|
cp.processName = name;
|
||||||
|
pl[name] = Object.clone(cp);
|
||||||
|
for (let k in cp) {
|
||||||
|
if (!cp.hasOwnProperty(k)) continue;
|
||||||
|
np[k] = cp[k];
|
||||||
|
}
|
||||||
|
s.cproc[mode] = name;
|
||||||
|
s.devproc[s.device.deviceName] = name;
|
||||||
|
api.conf.save();
|
||||||
|
api.conf.update();
|
||||||
|
api.event.settings();
|
||||||
|
api.settings.sync.put();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
saveAs(name);
|
||||||
|
} else {
|
||||||
|
api.uc.prompt("Save Settings As", cp ? lp || def : def).then(saveAs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function settings_load() {
|
||||||
|
api.conf.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_platform_size() {
|
||||||
|
api.conf.update();
|
||||||
|
api.platform.update_size();
|
||||||
|
api.platform.update_origin();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const settingsOps = {
|
||||||
|
export_device,
|
||||||
|
export_objects,
|
||||||
|
export_profile,
|
||||||
|
new_workspace,
|
||||||
|
settings_save,
|
||||||
|
settings_load,
|
||||||
|
update_device: update_platform_size,
|
||||||
|
};
|
||||||
|
|
@ -1,21 +1,6 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
const COLOR = {
|
// COLOR table has been migrated to src/kiri/app/color/ scheme system
|
||||||
wireframe: 0x444444,
|
|
||||||
wireframe_opacity: 0.25,
|
|
||||||
selected: [ 0xbbff00, 0xbbee00, 0xbbdd00, 0xbb9900 ],
|
|
||||||
deselected: [ 0xffff00, 0xffdd00, 0xffbb00, 0xff9900 ],
|
|
||||||
model_opacity: 1.0,
|
|
||||||
preview_opacity: 0.0,
|
|
||||||
slicing: 0xffaaaa,
|
|
||||||
slicing_opacity: 0.5,
|
|
||||||
sliced_opacity: 0.0,
|
|
||||||
cam_sliced_opacity: 0.2,
|
|
||||||
cam_preview: 0xdddddd,
|
|
||||||
cam_preview_opacity: 0.2,
|
|
||||||
cam_preview_dark: 0x888888,
|
|
||||||
cam_preview_opacity_dark: 0.2,
|
|
||||||
};
|
|
||||||
|
|
||||||
const LISTS = {
|
const LISTS = {
|
||||||
shell: [
|
shell: [
|
||||||
|
|
@ -23,6 +8,11 @@ const LISTS = {
|
||||||
{ name: "out-in" },
|
{ name: "out-in" },
|
||||||
{ name: "alternate" }
|
{ name: "alternate" }
|
||||||
],
|
],
|
||||||
|
support: [
|
||||||
|
{ name: "disabled" },
|
||||||
|
{ name: "automatic" },
|
||||||
|
{ name: "manual" },
|
||||||
|
],
|
||||||
start: [
|
start: [
|
||||||
{ name: "last" },
|
{ name: "last" },
|
||||||
{ name: "center" },
|
{ name: "center" },
|
||||||
|
|
@ -171,22 +161,17 @@ const PMODES = {
|
||||||
TOOLS: 2
|
TOOLS: 2
|
||||||
};
|
};
|
||||||
|
|
||||||
const PATHS = {
|
const LAST = 'kiri-last';
|
||||||
work: "../lib/kiri/run/worker.js",
|
|
||||||
pool: "./minion.js"
|
|
||||||
};
|
|
||||||
|
|
||||||
const SEED = 'kiri-seed';
|
const SEED = 'kiri-seed';
|
||||||
|
|
||||||
export let consts = {
|
export let consts = {
|
||||||
COLOR,
|
LAST,
|
||||||
LISTS,
|
LISTS,
|
||||||
MODES,
|
MODES,
|
||||||
PATHS,
|
|
||||||
PMODES,
|
PMODES,
|
||||||
VIEWS,
|
VIEWS,
|
||||||
SEED,
|
SEED,
|
||||||
beltfact: Math.cos(Math.PI / 4)
|
beltfact: Math.cos(Math.PI / 4)
|
||||||
};
|
};
|
||||||
|
|
||||||
export { COLOR, LISTS, MODES, VIEWS, PATHS, PMODES, SEED };
|
export { LAST, LISTS, MODES, VIEWS, PMODES, SEED };
|
||||||
|
|
@ -2,10 +2,10 @@
|
||||||
|
|
||||||
import { $, h } from '../../moto/webui.js';
|
import { $, h } from '../../moto/webui.js';
|
||||||
import { api } from './api.js';
|
import { api } from './api.js';
|
||||||
import { conf } from './conf.js';
|
import { conf } from './conf/defaults.js';
|
||||||
import { space } from '../../moto/space.js';
|
import { space } from '../../moto/space.js';
|
||||||
import { devices as devlist } from '../../pack/kiri-devs.js';
|
import { devices as devlist } from '../../pack/kiri-devs.js';
|
||||||
import { settings, conf as setconf } from './settings.js';
|
import { settings, conf as setconf } from './conf/manager.js';
|
||||||
|
|
||||||
export const device = {
|
export const device = {
|
||||||
clone: cloneDevice,
|
clone: cloneDevice,
|
||||||
|
|
@ -92,7 +92,11 @@ function selectDevice(devicename) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// only for local filters
|
/**
|
||||||
|
* Clone the current device to create a customizable local copy.
|
||||||
|
* Naming logic: if device already has "My" prefix, appends " copy", otherwise adds "My" prefix.
|
||||||
|
* Only works for local devices (not stock devices).
|
||||||
|
*/
|
||||||
function cloneDevice() {
|
function cloneDevice() {
|
||||||
let name = `${getSelectedDevice().replace(/\./g,' ')}`;
|
let name = `${getSelectedDevice().replace(/\./g,' ')}`;
|
||||||
let code = api.clone(setconf.get().device);
|
let code = api.clone(setconf.get().device);
|
||||||
|
|
@ -113,6 +117,18 @@ function updateLaserState() {
|
||||||
$('laser-off').style.display = dev.useLaser ? 'flex' : 'none';
|
$('laser-off').style.display = dev.useLaser ? 'flex' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set device configuration from code and initialize device state.
|
||||||
|
* Complex initialization that:
|
||||||
|
* - Parses device code (string or object)
|
||||||
|
* - Fills missing device fields with defaults
|
||||||
|
* - Handles first-time device setup with profiles
|
||||||
|
* - Updates UI elements and platform configuration
|
||||||
|
* - Manages device/process associations
|
||||||
|
* - Emits device.select and device.selected events
|
||||||
|
* @param {string|object} code - Device configuration code or object
|
||||||
|
* @param {string} devicename - Name of the device being set
|
||||||
|
*/
|
||||||
function setDeviceCode(code, devicename) {
|
function setDeviceCode(code, devicename) {
|
||||||
api.event.emit('device.select', devicename);
|
api.event.emit('device.select', devicename);
|
||||||
try {
|
try {
|
||||||
|
|
@ -213,24 +229,24 @@ function setDeviceCode(code, devicename) {
|
||||||
e.disabled = !local;
|
e.disabled = !local;
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.deviceSave.disabled = !local;
|
ui.devices.save.disabled = !local;
|
||||||
ui.deviceDelete.disabled = !local;
|
ui.devices.delete.disabled = !local;
|
||||||
ui.deviceRename.disabled = !local;
|
ui.devices.rename.disabled = !local;
|
||||||
ui.deviceExport.disabled = !local;
|
ui.devices.export.disabled = !local;
|
||||||
ui.deviceAdd.style.display = mode === 'SLA' ? 'none' : '';
|
ui.devices.add.style.display = mode === 'SLA' ? 'none' : '';
|
||||||
|
|
||||||
if (local) {
|
if (local) {
|
||||||
ui.deviceAdd.innerText = "copy";
|
ui.devices.add.innerText = "copy";
|
||||||
ui.deviceDelete.style.display = '';
|
ui.devices.delete.style.display = '';
|
||||||
ui.deviceRename.style.display = '';
|
ui.devices.rename.style.display = '';
|
||||||
ui.deviceExport.style.display = '';
|
ui.devices.export.style.display = '';
|
||||||
} else {
|
} else {
|
||||||
ui.deviceAdd.innerText = "customize";
|
ui.devices.add.innerText = "customize";
|
||||||
ui.deviceDelete.style.display = 'none';
|
ui.devices.delete.style.display = 'none';
|
||||||
ui.deviceRename.style.display = 'none';
|
ui.devices.rename.style.display = 'none';
|
||||||
ui.deviceExport.style.display = 'none';
|
ui.devices.export.style.display = 'none';
|
||||||
}
|
}
|
||||||
ui.deviceAdd.disabled = dev.noclone;
|
ui.devices.add.disabled = dev.noclone;
|
||||||
|
|
||||||
setconf.update_fields();
|
setconf.update_fields();
|
||||||
space.platform.setBelt(isBelt());
|
space.platform.setBelt(isBelt());
|
||||||
|
|
@ -277,6 +293,12 @@ function setDeviceCode(code, devicename) {
|
||||||
api.event.settings();
|
api.event.settings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render device selection UI with both stock and custom devices.
|
||||||
|
* Builds device list dropdown, sets up event handlers for save/add/delete/rename/export,
|
||||||
|
* and separates local "My Devices" from "Stock Devices" in the UI.
|
||||||
|
* @param {string[]} devices - Array of stock device names for current mode
|
||||||
|
*/
|
||||||
function renderDevices(devices) {
|
function renderDevices(devices) {
|
||||||
let selected = api.device.get() || devices[0],
|
let selected = api.device.get() || devices[0],
|
||||||
features = api.feature,
|
features = api.feature,
|
||||||
|
|
@ -301,7 +323,7 @@ function renderDevices(devices) {
|
||||||
|
|
||||||
event.emit('devices.render', devices);
|
event.emit('devices.render', devices);
|
||||||
|
|
||||||
ui.deviceSave.onclick = function() {
|
ui.devices.save.onclick = function() {
|
||||||
event.emit('device.save');
|
event.emit('device.save');
|
||||||
api.function.clear();
|
api.function.clear();
|
||||||
setconf.save();
|
setconf.save();
|
||||||
|
|
@ -309,18 +331,18 @@ function renderDevices(devices) {
|
||||||
showDevices();
|
showDevices();
|
||||||
api.modal.hide();
|
api.modal.hide();
|
||||||
};
|
};
|
||||||
ui.deviceAdd.onclick = function() {
|
ui.devices.add.onclick = function() {
|
||||||
api.function.clear();
|
api.function.clear();
|
||||||
cloneDevice();
|
cloneDevice();
|
||||||
showDevices();
|
showDevices();
|
||||||
};
|
};
|
||||||
ui.deviceDelete.onclick = function() {
|
ui.devices.delete.onclick = function() {
|
||||||
api.function.clear();
|
api.function.clear();
|
||||||
removeLocalDevice(getSelectedDevice());
|
removeLocalDevice(getSelectedDevice());
|
||||||
selectDevice(getModeDevices()[0]);
|
selectDevice(getModeDevices()[0]);
|
||||||
showDevices();
|
showDevices();
|
||||||
};
|
};
|
||||||
ui.deviceRename.onclick = function() {
|
ui.devices.rename.onclick = function() {
|
||||||
api.uc.prompt(`Rename "${selected}`, selected).then(newname => {
|
api.uc.prompt(`Rename "${selected}`, selected).then(newname => {
|
||||||
if (newname) {
|
if (newname) {
|
||||||
updateDeviceName(newname);
|
updateDeviceName(newname);
|
||||||
|
|
@ -332,7 +354,7 @@ function renderDevices(devices) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
ui.deviceExport.onclick = function(event) {
|
ui.devices.export.onclick = function(event) {
|
||||||
const record = {
|
const record = {
|
||||||
version: api.version,
|
version: api.version,
|
||||||
device: selected,
|
device: selected,
|
||||||
|
|
@ -5,12 +5,22 @@ import { space } from '../../moto/space.js';
|
||||||
|
|
||||||
const { event } = api;
|
const { event } = api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Undo/Redo action stack. Each entry contains {undo, redo} action records.
|
||||||
|
* @type {Array<{undo: object, redo: object}>}
|
||||||
|
*/
|
||||||
let stack = [];
|
let stack = [];
|
||||||
|
|
||||||
|
/** Current position in the undo/redo stack */
|
||||||
let stpos = 0;
|
let stpos = 0;
|
||||||
|
|
||||||
|
/** Accumulator for drag movements - tracks cumulative x,y offset during selection drag */
|
||||||
let moved = { x: 0, y: 0 };
|
let moved = { x: 0, y: 0 };
|
||||||
|
|
||||||
|
/** Message ID for the current undo/redo alert message */
|
||||||
let msgid;
|
let msgid;
|
||||||
|
|
||||||
event.on("init-done", () => {
|
event.on("load-done", () => {
|
||||||
$('undo').onclick = undo;
|
$('undo').onclick = undo;
|
||||||
$('redo').onclick = redo;
|
$('redo').onclick = redo;
|
||||||
});
|
});
|
||||||
|
|
@ -31,6 +41,9 @@ let redo = api.doit.redo = function() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the entire undo/redo stack and reset moved accumulator
|
||||||
|
*/
|
||||||
let clear = api.doit.clear = function() {
|
let clear = api.doit.clear = function() {
|
||||||
stack = [];
|
stack = [];
|
||||||
stpos = 0;
|
stpos = 0;
|
||||||
|
|
@ -52,6 +65,13 @@ function message(txt) {
|
||||||
msgid = api.show.alert(txt);
|
msgid = api.show.alert(txt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute an action record (undo or redo).
|
||||||
|
* Dispatches based on action type: move, rotate, or scale.
|
||||||
|
* @param {object} rec - Action record with type and action-specific properties
|
||||||
|
* @param {string} rec.type - Action type: 'move', 'rotate', or 'scale'
|
||||||
|
* @param {Array} rec.widgets - Widgets to operate on
|
||||||
|
*/
|
||||||
function action(rec) {
|
function action(rec) {
|
||||||
switch (rec.type) {
|
switch (rec.type) {
|
||||||
case 'move':
|
case 'move':
|
||||||
|
|
@ -75,6 +95,11 @@ function action(rec) {
|
||||||
updateButtons();
|
updateButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push an undo/redo action pair onto the stack.
|
||||||
|
* Truncates stack at current position (clearing any "future" when adding new action).
|
||||||
|
* @param {object} ur - Action pair with {undo, redo} records
|
||||||
|
*/
|
||||||
function pushActions(ur) {
|
function pushActions(ur) {
|
||||||
stack.length = stpos++;
|
stack.length = stpos++;
|
||||||
stack.push(ur);
|
stack.push(ur);
|
||||||
|
|
@ -2,14 +2,18 @@
|
||||||
|
|
||||||
import { $ } from '../../moto/webui.js';
|
import { $ } from '../../moto/webui.js';
|
||||||
import { api } from './api.js';
|
import { api } from './api.js';
|
||||||
import { client } from './client.js';
|
import { client } from './workers.js';
|
||||||
import { local } from '../../data/local.js';
|
import { local } from '../../data/local.js';
|
||||||
import { util } from '../../geo/base.js';
|
import { util } from '../../geo/base.js';
|
||||||
import { MODES } from './consts.js';
|
import { MODES } from './consts.js';
|
||||||
import { LASER as laser_driver } from '../mode/laser/driver.js';
|
import { LASER as laser_driver } from '../mode/laser/init-work.js';
|
||||||
import { SLA as sla_client } from '../mode/sla/client.js';
|
import { SLA as sla_client } from '../mode/sla/app/init-ui.js';
|
||||||
import { hash } from '../../ext/md5.js';
|
import { hash } from '../../ext/md5.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sequential print counter for generating unique export filenames.
|
||||||
|
* Increments with each export and persists in local storage.
|
||||||
|
*/
|
||||||
let printSeq = parseInt(local['kiri-print-seq'] || local['print-seq'] || "0") + 1;
|
let printSeq = parseInt(local['kiri-print-seq'] || local['print-seq'] || "0") + 1;
|
||||||
|
|
||||||
function localGet(key) {
|
function localGet(key) {
|
||||||
|
|
@ -20,6 +24,12 @@ function localSet(key, val) {
|
||||||
return api.local.set(key, val);
|
return api.local.set(key, val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main export entry point. Dispatches to mode-specific export handlers.
|
||||||
|
* Extracts widget filenames to use for export filename suggestions.
|
||||||
|
* @param {object} options - Export options (mode-specific)
|
||||||
|
* @returns {*} Result from mode-specific export handler
|
||||||
|
*/
|
||||||
export function exportFile(options) {
|
export function exportFile(options) {
|
||||||
let mode = api.mode.get();
|
let mode = api.mode.get();
|
||||||
let names = api.widgets.all().map(w => w.meta ? w.meta.file : undefined)
|
let names = api.widgets.all().map(w => w.meta ? w.meta.file : undefined)
|
||||||
|
|
@ -39,6 +49,13 @@ export function exportFile(options) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export gcode for FDM or CAM modes.
|
||||||
|
* Calls worker to generate gcode, then presents dialog or invokes callback.
|
||||||
|
* @param {function} callback - Optional callback(gcode_string, output_info)
|
||||||
|
* @param {string} mode - Current mode ('FDM' or 'CAM')
|
||||||
|
* @param {string[]} names - Widget filenames for export naming
|
||||||
|
*/
|
||||||
function callExport(callback, mode, names) {
|
function callExport(callback, mode, names) {
|
||||||
let alert = api.feature.work_alerts ? api.show.alert("Exporting") : null;
|
let alert = api.feature.work_alerts ? api.show.alert("Exporting") : null;
|
||||||
let gcode = [];
|
let gcode = [];
|
||||||
|
|
@ -65,6 +82,12 @@ function callExport(callback, mode, names) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export laser/waterjet/drag knife toolpaths.
|
||||||
|
* Worker generates output, then presents export dialog with SVG/DXF/STL/GCode options.
|
||||||
|
* @param {object} options - Export options
|
||||||
|
* @param {string[]} names - Widget filenames for export naming
|
||||||
|
*/
|
||||||
function callExportLaser(options, names) {
|
function callExportLaser(options, names) {
|
||||||
client.export(api.conf.get(), (line) => {
|
client.export(api.conf.get(), (line) => {
|
||||||
// engine export uses lines
|
// engine export uses lines
|
||||||
|
|
@ -79,6 +102,12 @@ function callExportLaser(options, names) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export SLA print data.
|
||||||
|
* Worker generates layer images, then delegates to SLA client for download.
|
||||||
|
* @param {object} options - Export options
|
||||||
|
* @param {string[]} names - Widget filenames for export naming
|
||||||
|
*/
|
||||||
function callExportSLA(options, names) {
|
function callExportSLA(options, names) {
|
||||||
client.export(api.conf.get(), (line) => {
|
client.export(api.conf.get(), (line) => {
|
||||||
api.show.progress(line.progress, "exporting");
|
api.show.progress(line.progress, "exporting");
|
||||||
|
|
@ -92,6 +121,12 @@ function callExportSLA(options, names) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Present laser export dialog with format options (SVG, DXF, STL, GCode).
|
||||||
|
* Sets up UI handlers for downloading in each supported format.
|
||||||
|
* @param {Array} data - Layer data from export worker
|
||||||
|
* @param {string[]} names - Widget filenames for naming suggestion
|
||||||
|
*/
|
||||||
function exportLaserDialog(data, names) {
|
function exportLaserDialog(data, names) {
|
||||||
localSet('kiri-print-seq', printSeq++);
|
localSet('kiri-print-seq', printSeq++);
|
||||||
|
|
||||||
|
|
@ -101,9 +136,9 @@ function exportLaserDialog(data, names) {
|
||||||
const driver = laser_driver;
|
const driver = laser_driver;
|
||||||
const { process } = settings;
|
const { process } = settings;
|
||||||
if (process.ctOutStack) {
|
if (process.ctOutStack) {
|
||||||
$('print-stl').classList.remove('hide');
|
$('print-obj').classList.remove('hide');
|
||||||
} else {
|
} else {
|
||||||
$('print-stl').classList.add('hide');
|
$('print-obj').classList.add('hide');
|
||||||
}
|
}
|
||||||
|
|
||||||
function download_svg() {
|
function download_svg() {
|
||||||
|
|
@ -120,10 +155,10 @@ function exportLaserDialog(data, names) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function download_stl() {
|
function download_obj() {
|
||||||
api.util.download(
|
api.util.download(
|
||||||
driver.exportSTL(settings, data),
|
driver.exportOBJ(settings, data),
|
||||||
$('print-filename-laser').value + ".stl"
|
$('print-filename-laser').value + ".obj"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -143,17 +178,36 @@ function exportLaserDialog(data, names) {
|
||||||
$('print-lines').value = util.comma(segments);
|
$('print-lines').value = util.comma(segments);
|
||||||
$('print-svg').onclick = download_svg;
|
$('print-svg').onclick = download_svg;
|
||||||
$('print-dxf').onclick = download_dxf;
|
$('print-dxf').onclick = download_dxf;
|
||||||
$('print-stl').onclick = download_stl;
|
$('print-obj').onclick = download_obj;
|
||||||
$('print-lg').onclick = download_gcode;
|
$('print-lg').onclick = download_gcode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind an input field to persist its value to local storage on blur.
|
||||||
|
* @param {string} field - Element ID of the input field
|
||||||
|
* @param {string} varname - Local storage key to save value under
|
||||||
|
*/
|
||||||
function bindField(field, varname) {
|
function bindField(field, varname) {
|
||||||
$(field).onblur = function() {
|
$(field).onblur = function() {
|
||||||
console.log('save', field, 'to', varname);
|
console.log('save', field, 'to', varname);
|
||||||
localSet(varname, $(field).value.trim());
|
localSet(varname, $(field).value.trim());
|
||||||
};
|
};
|
||||||
|
$(field).onchange = $(field).onblur;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Present gcode export dialog with download/send options.
|
||||||
|
* Complex UI setup that handles:
|
||||||
|
* - Local download of gcode, zip (CAM operations), or 3MF (FDM)
|
||||||
|
* - OctoPrint integration for remote printing
|
||||||
|
* - Bambu printer integration (via api.bambu)
|
||||||
|
* - Print statistics (time, filament, weight)
|
||||||
|
* - Gcode preview
|
||||||
|
* @param {string[]} gcode - Array of gcode lines
|
||||||
|
* @param {object} [sections] - CAM operation sections for zip export
|
||||||
|
* @param {object} info - Export metadata (time, distance, bytes, etc.)
|
||||||
|
* @param {string[]} names - Widget filenames for naming suggestion
|
||||||
|
*/
|
||||||
function exportGCodeDialog(gcode, sections, info, names) {
|
function exportGCodeDialog(gcode, sections, info, names) {
|
||||||
localSet('kiri-print-seq', printSeq++);
|
localSet('kiri-print-seq', printSeq++);
|
||||||
|
|
||||||
|
|
@ -166,12 +220,7 @@ function exportGCodeDialog(gcode, sections, info, names) {
|
||||||
codeproc = settings.device.gcodeProc,
|
codeproc = settings.device.gcodeProc,
|
||||||
octo_host,
|
octo_host,
|
||||||
octo_apik,
|
octo_apik,
|
||||||
grid_host,
|
octo_type;
|
||||||
grid_apik,
|
|
||||||
grid_target,
|
|
||||||
grid_targets = {},
|
|
||||||
grid_local,
|
|
||||||
grid_uuid;
|
|
||||||
|
|
||||||
// join gcode array into a string
|
// join gcode array into a string
|
||||||
gcode = gcode.join('\r\n') + '\r\n';
|
gcode = gcode.join('\r\n') + '\r\n';
|
||||||
|
|
@ -187,13 +236,18 @@ function exportGCodeDialog(gcode, sections, info, names) {
|
||||||
{type:"application/octet-stream"});
|
{type:"application/octet-stream"});
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendto_octoprint() {
|
function printToHost() {
|
||||||
if (!(octo_host && octo_apik)) return;
|
sendToHost(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendToHost(start) {
|
||||||
|
if (!(octo_host && octo_apik && octo_type)) return;
|
||||||
|
|
||||||
let form = new FormData(),
|
let form = new FormData(),
|
||||||
ajax = new XMLHttpRequest(),
|
ajax = new XMLHttpRequest(),
|
||||||
host = octo_host.value.toLowerCase(),
|
host = octo_host.value.toLowerCase(),
|
||||||
apik = octo_apik.value;
|
apik = octo_apik.value,
|
||||||
|
type = octo_type.value;
|
||||||
|
|
||||||
if (host.indexOf("http") !== 0) {
|
if (host.indexOf("http") !== 0) {
|
||||||
api.show.alert("host missing protocol (http:// or https://)");
|
api.show.alert("host missing protocol (http:// or https://)");
|
||||||
|
|
@ -206,26 +260,43 @@ function exportGCodeDialog(gcode, sections, info, names) {
|
||||||
|
|
||||||
localSet('octo-host', host.trim());
|
localSet('octo-host', host.trim());
|
||||||
localSet('octo-apik', apik.trim());
|
localSet('octo-apik', apik.trim());
|
||||||
|
localSet('octo-type', type.trim());
|
||||||
|
|
||||||
filename = $('print-filename').value;
|
filename = $('print-filename').value + "." + fileext;
|
||||||
form.append("file", getBlob(), filename+"."+fileext);
|
form.append("file", getBlob(), filename);
|
||||||
ajax.onreadystatechange = function() {
|
ajax.onreadystatechange = function() {
|
||||||
|
console.log({ ajax_readyState: ajax.readyState });
|
||||||
if (ajax.readyState === 4) {
|
if (ajax.readyState === 4) {
|
||||||
let status = ajax.status;
|
let status = ajax.status;
|
||||||
api.stats.add(`ua_${api.mode.get_lower()}_print_octo_${status}`);
|
api.stats.add(`ua_${api.mode.get_lower()}_print_send_${status}`);
|
||||||
if (status >= 200 && status < 300) {
|
if (status >= 200 && status < 300) {
|
||||||
api.modal.hide();
|
api.modal.hide();
|
||||||
} else {
|
} else {
|
||||||
api.show.alert("octoprint error\nstatus: "+status+"\nmessage: "+ajax.responseText);
|
api.show.alert("send error\nstatus: "+status+"\nmessage: "+ajax.responseText);
|
||||||
}
|
}
|
||||||
api.show.progress(0);
|
api.show.progress(0);
|
||||||
|
if (start && type === 'moonraker') {
|
||||||
|
fetch(`${host}/printer/print/start`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ filename })
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => console.log('print start', data))
|
||||||
|
.catch(err => console.error('print start error', err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
api.show.progress(0);
|
api.show.progress(0);
|
||||||
};
|
};
|
||||||
ajax.upload.addEventListener('progress', function(evt) {
|
ajax.upload.addEventListener('progress', (evt) => {
|
||||||
|
console.log({ ajax_progress: evt });
|
||||||
api.show.progress(evt.loaded/evt.total, "sending");
|
api.show.progress(evt.loaded/evt.total, "sending");
|
||||||
});
|
});
|
||||||
ajax.open("POST", host+"/api/files/local");
|
if (type === 'moonraker') {
|
||||||
|
ajax.open("POST", host + "/server/files/upload");
|
||||||
|
} else {
|
||||||
|
ajax.open("POST", host + "/api/files/local");
|
||||||
|
}
|
||||||
if (apik) {
|
if (apik) {
|
||||||
ajax.setRequestHeader("X-Api-Key", apik);
|
ajax.setRequestHeader("X-Api-Key", apik);
|
||||||
}
|
}
|
||||||
|
|
@ -275,7 +346,8 @@ function exportGCodeDialog(gcode, sections, info, names) {
|
||||||
api.modal.show('xany');
|
api.modal.show('xany');
|
||||||
let set = api.conf.get();
|
let set = api.conf.get();
|
||||||
let fdm = MODE === MODES.FDM;
|
let fdm = MODE === MODES.FDM;
|
||||||
let octo = set.controller.exportOcto && MODE !== MODES.CAM;
|
let cam = MODE === MODES.CAM;
|
||||||
|
let octo = set.controller.exportOcto && !cam;
|
||||||
let preview = set.controller.exportPreview;
|
let preview = set.controller.exportPreview;
|
||||||
$('code-preview-head').style.display = preview ? '' : 'none';
|
$('code-preview-head').style.display = preview ? '' : 'none';
|
||||||
$('code-preview').style.display = preview ? '' : 'none';
|
$('code-preview').style.display = preview ? '' : 'none';
|
||||||
|
|
@ -284,8 +356,8 @@ function exportGCodeDialog(gcode, sections, info, names) {
|
||||||
$('print-filename').value = filename;
|
$('print-filename').value = filename;
|
||||||
$('print-filesize').value = util.comma(info.bytes);
|
$('print-filesize').value = util.comma(info.bytes);
|
||||||
$('print-length').value = Math.round(info.distance);
|
$('print-length').value = Math.round(info.distance);
|
||||||
if (set.controller.devel) {
|
if ((fdm || cam) && preview && set.controller.devel) {
|
||||||
$('code-preview-textarea').style.height = "30em";
|
document.body.classList.add('devel');
|
||||||
}
|
}
|
||||||
calcTime();
|
calcTime();
|
||||||
if (fdm) {
|
if (fdm) {
|
||||||
|
|
@ -295,6 +367,7 @@ function exportGCodeDialog(gcode, sections, info, names) {
|
||||||
// persist fields when changed
|
// persist fields when changed
|
||||||
bindField('octo-host', 'octo-host');
|
bindField('octo-host', 'octo-host');
|
||||||
bindField('octo-apik', 'octo-apik');
|
bindField('octo-apik', 'octo-apik');
|
||||||
|
bindField('octo-type', 'octo-type');
|
||||||
|
|
||||||
// in cam mode, show zip file option
|
// in cam mode, show zip file option
|
||||||
let downloadZip = $('print-zip');
|
let downloadZip = $('print-zip');
|
||||||
|
|
@ -336,6 +409,13 @@ function exportGCodeDialog(gcode, sections, info, names) {
|
||||||
// let bnds = settings.bounds;
|
// let bnds = settings.bounds;
|
||||||
// console.log({ wids, bnds });
|
// console.log({ wids, bnds });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate 3MF file for Bambu/BambuStudio printers.
|
||||||
|
* Creates ZIP archive with gcode, thumbnails, and metadata.
|
||||||
|
* @param {function} then - Callback to receive generated 3MF blob
|
||||||
|
* @param {string} [ptype='unknown'] - Printer type identifier
|
||||||
|
* @param {number[]} [ams=[0]] - AMS (filament) slot assignments
|
||||||
|
*/
|
||||||
function gen3mf(then, ptype = 'unknown', ams = [0]) {
|
function gen3mf(then, ptype = 'unknown', ams = [0]) {
|
||||||
let now = new Date();
|
let now = new Date();
|
||||||
let ymd = [
|
let ymd = [
|
||||||
|
|
@ -489,21 +569,23 @@ function exportGCodeDialog(gcode, sections, info, names) {
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
// octoprint setup
|
// remote host (klipper fluidd / octoprint)
|
||||||
$('send-to-octohead').style.display = octo ? '' : 'none';
|
$('send-remote-head').style.display = octo ? '' : 'none';
|
||||||
$('send-to-octoprint').style.display = octo ? '' : 'none';
|
$('send-remote-print').style.display = octo ? '' : 'none';
|
||||||
if (octo) try {
|
if (octo) try {
|
||||||
$('print-octoprint').onclick = sendto_octoprint;
|
$('send-remote').onclick = sendToHost;
|
||||||
|
$('print-remote').onclick = printToHost;
|
||||||
octo_host = $('octo-host');
|
octo_host = $('octo-host');
|
||||||
octo_apik = $('octo-apik');
|
octo_apik = $('octo-apik');
|
||||||
|
octo_type = $('octo-type');
|
||||||
if (MODE === MODES.CAM) {
|
if (MODE === MODES.CAM) {
|
||||||
$('send-to-octohead').style.display = 'none';
|
$('send-remote-head').style.display = 'none';
|
||||||
$('send-to-octoprint').style.display = 'none';
|
$('send-remote-print').style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
$('send-to-octohead').style.display = '';
|
$('send-remote-head').style.display = '';
|
||||||
$('send-to-octoprint').style.display = '';
|
$('send-remote-print').style.display = '';
|
||||||
}
|
}
|
||||||
// hide octoprint when hard-coded in the url
|
// hide remote host option when hard-coded in the url
|
||||||
if (api.const.OCTO) {
|
if (api.const.OCTO) {
|
||||||
$('ophost').style.display = 'none';
|
$('ophost').style.display = 'none';
|
||||||
$('opapik').style.display = 'none';
|
$('opapik').style.display = 'none';
|
||||||
|
|
@ -511,6 +593,7 @@ function exportGCodeDialog(gcode, sections, info, names) {
|
||||||
}
|
}
|
||||||
octo_host.value = localGet('octo-host') || '';
|
octo_host.value = localGet('octo-host') || '';
|
||||||
octo_apik.value = localGet('octo-apik') || '';
|
octo_apik.value = localGet('octo-apik') || '';
|
||||||
|
octo_type.value = localGet('octo-type') || '';
|
||||||
} catch (e) { console.log(e) }
|
} catch (e) { console.log(e) }
|
||||||
|
|
||||||
// preview of the generated GCODE (first 64k max)
|
// preview of the generated GCODE (first 64k max)
|
||||||
|
|
@ -1,20 +1,39 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Face selection tools for mesh manipulation.
|
||||||
|
* Provides interactive tools for:
|
||||||
|
* - Lay flat: Rotate selected face to be parallel with build plate
|
||||||
|
* - Left align: Rotate to align face normal with -X axis
|
||||||
|
* - Face up: Select face normal for custom operations
|
||||||
|
* - Camera focus: Select point for camera focus
|
||||||
|
*
|
||||||
|
* Tools use hover mode to display visual indicator on face under cursor.
|
||||||
|
* User clicks face to apply operation. ESC to cancel.
|
||||||
|
*/
|
||||||
|
|
||||||
import { api } from './api.js';
|
import { api } from './api.js';
|
||||||
import { THREE } from '../../ext/three.js';
|
import { THREE } from '../../ext/three.js';
|
||||||
import { space } from '../../moto/space.js';
|
import { space } from '../../moto/space.js';
|
||||||
|
|
||||||
const { Vector3, Quaternion } = THREE;
|
const { Vector3, Quaternion } = THREE;
|
||||||
|
|
||||||
|
/** X-axis unit vector for rotation calculations */
|
||||||
const XAXIS = new THREE.Vector3(1,0,0);
|
const XAXIS = new THREE.Vector3(1,0,0);
|
||||||
|
|
||||||
|
/** Z-axis unit vector for rotation calculations */
|
||||||
const ZAXIS = new THREE.Vector3(0,0,1);
|
const ZAXIS = new THREE.Vector3(0,0,1);
|
||||||
|
|
||||||
// circle
|
/** Circle mesh geometry for face indicator */
|
||||||
let pgeo = new THREE.CircleGeometry(8, 30);
|
let pgeo = new THREE.CircleGeometry(8, 30);
|
||||||
|
|
||||||
|
/** Semi-transparent material for face indicator */
|
||||||
let pmat = new THREE.MeshBasicMaterial({color: 0xddeeff, opacity: 0.6, transparent: true});
|
let pmat = new THREE.MeshBasicMaterial({color: 0xddeeff, opacity: 0.6, transparent: true});
|
||||||
|
|
||||||
|
/** Combined mesh for face indicator (circle + outline) */
|
||||||
let pmesh = new THREE.Mesh(pgeo, pmat);
|
let pmesh = new THREE.Mesh(pgeo, pmat);
|
||||||
|
|
||||||
// circle outline
|
// Build circle outline from geometry indices
|
||||||
let pi = pgeo.index.array;
|
let pi = pgeo.index.array;
|
||||||
let pp = pgeo.attributes.position.array;
|
let pp = pgeo.attributes.position.array;
|
||||||
let np = [];
|
let np = [];
|
||||||
|
|
@ -36,19 +55,39 @@ let wmat = new THREE.LineBasicMaterial({ color: 0x6699cc });
|
||||||
let wmesh = new THREE.LineSegments(wgeo, wmat);
|
let wmesh = new THREE.LineSegments(wgeo, wmat);
|
||||||
pmesh.add(wmesh);
|
pmesh.add(wmesh);
|
||||||
|
|
||||||
|
/** Alert handle for "ESC to end" message */
|
||||||
let alert;
|
let alert;
|
||||||
|
|
||||||
|
/** Last object (widget mesh) the indicator was attached to */
|
||||||
let lastobj;
|
let lastobj;
|
||||||
|
|
||||||
|
/** Last face that was hovered over */
|
||||||
let lastface;
|
let lastface;
|
||||||
|
|
||||||
|
/** Whether a tool is currently active */
|
||||||
let enabled = false;
|
let enabled = false;
|
||||||
|
|
||||||
|
/** Name of current operation for display */
|
||||||
let opName = 'lay flat';
|
let opName = 'lay flat';
|
||||||
|
|
||||||
|
/** Callback to execute when face is selected */
|
||||||
let onDone = onLayFlatSelect;
|
let onDone = onLayFlatSelect;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lay flat operation: Rotate widget so selected face is parallel to build plate.
|
||||||
|
* Calculates quaternion to align face normal with -Z axis (down).
|
||||||
|
*/
|
||||||
function onLayFlatSelect() {
|
function onLayFlatSelect() {
|
||||||
let q = new Quaternion().setFromUnitVectors(lastface.normal, new Vector3(0,0,-1));
|
let q = new Quaternion().setFromUnitVectors(lastface.normal, new Vector3(0,0,-1));
|
||||||
api.selection.rotate(q);
|
api.selection.rotate(q);
|
||||||
// endIt();
|
// endIt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Left align operation: Rotate widget so selected face normal aligns with -X axis.
|
||||||
|
* Projects face normal onto XY plane and calculates Z-axis rotation.
|
||||||
|
* Used for orienting parts for belt printers or side printing.
|
||||||
|
*/
|
||||||
function onLeftySelect() {
|
function onLeftySelect() {
|
||||||
// Project the face normal onto the XY plane (ignore Z component)
|
// Project the face normal onto the XY plane (ignore Z component)
|
||||||
let projectedNormal = new Vector3(lastface.normal.x, lastface.normal.y, 0).normalize();
|
let projectedNormal = new Vector3(lastface.normal.x, lastface.normal.y, 0).normalize();
|
||||||
|
|
@ -67,11 +106,21 @@ function onLeftySelect() {
|
||||||
api.selection.rotate(q);
|
api.selection.rotate(q);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Face select operation: Emit face normal for custom handling.
|
||||||
|
* Emits 'tool.mesh.face-normal' event with selected face normal.
|
||||||
|
*/
|
||||||
function onFaceUpSelect() {
|
function onFaceUpSelect() {
|
||||||
api.event.emit('tool.mesh.face-normal', lastface.normal);
|
api.event.emit('tool.mesh.face-normal', lastface.normal);
|
||||||
// endIt();
|
// endIt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start interactive face selection tool.
|
||||||
|
* Enables hover mode, displays alert, and begins tracking mouse.
|
||||||
|
* If already enabled, ends the current operation.
|
||||||
|
* Cannot start if another hover mode is active.
|
||||||
|
*/
|
||||||
function startIt() {
|
function startIt() {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
endIt();
|
endIt();
|
||||||
|
|
@ -86,6 +135,10 @@ function startIt() {
|
||||||
alert = api.show.alert(`[ESC] to end ${opName}`, 600000);
|
alert = api.show.alert(`[ESC] to end ${opName}`, 600000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* End current face selection tool.
|
||||||
|
* Disables hover mode, hides alert, and cleans up indicator mesh.
|
||||||
|
*/
|
||||||
function endIt() {
|
function endIt() {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
api.hide.alert(alert);
|
api.hide.alert(alert);
|
||||||
|
|
@ -96,6 +149,9 @@ function endIt() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove face indicator mesh from scene.
|
||||||
|
*/
|
||||||
function cleanup() {
|
function cleanup() {
|
||||||
if (lastobj) {
|
if (lastobj) {
|
||||||
lastobj.remove(pmesh);
|
lastobj.remove(pmesh);
|
||||||
|
|
@ -105,30 +161,52 @@ function cleanup() {
|
||||||
|
|
||||||
api.event.on('key.esc', endIt);
|
api.event.on('key.esc', endIt);
|
||||||
|
|
||||||
api.event.on('tool.camera.focus', (fn) => {
|
/**
|
||||||
|
* Start camera focus tool.
|
||||||
|
* User clicks face to set camera focus point.
|
||||||
|
* @param {Function} fn - Callback to execute with focus point
|
||||||
|
*/
|
||||||
|
function startFocus(fn) {
|
||||||
opName = 'camera focus';
|
opName = 'camera focus';
|
||||||
onDone = fn;
|
onDone = fn;
|
||||||
startIt();
|
startIt();
|
||||||
});
|
}
|
||||||
|
|
||||||
api.event.on('tool.mesh.face-up', () => {
|
/**
|
||||||
|
* Start face selection tool.
|
||||||
|
* User clicks face to select its normal vector.
|
||||||
|
*/
|
||||||
|
function startFaceUp() {
|
||||||
opName = 'face select';
|
opName = 'face select';
|
||||||
onDone = onFaceUpSelect;
|
onDone = onFaceUpSelect;
|
||||||
startIt();
|
startIt();
|
||||||
});
|
}
|
||||||
|
|
||||||
api.event.on('tool.mesh.lay-flat', () => {
|
/**
|
||||||
|
* Start lay flat tool.
|
||||||
|
* User clicks face to rotate widget so that face is parallel to build plate.
|
||||||
|
*/
|
||||||
|
function startLayFlat() {
|
||||||
opName = 'lay flat';
|
opName = 'lay flat';
|
||||||
onDone = onLayFlatSelect;
|
onDone = onLayFlatSelect;
|
||||||
startIt();
|
startIt();
|
||||||
});
|
}
|
||||||
|
|
||||||
api.event.on('tool.mesh.lefty', () => {
|
/**
|
||||||
|
* Start left align tool.
|
||||||
|
* User clicks face to align its normal with -X axis.
|
||||||
|
*/
|
||||||
|
function startLeftAlign() {
|
||||||
opName = 'align y axis';
|
opName = 'align y axis';
|
||||||
onDone = onLeftySelect;
|
onDone = onLeftySelect;
|
||||||
startIt();
|
startIt();
|
||||||
});
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scale face indicator based on camera distance.
|
||||||
|
* For camera focus, scales inversely (closer = smaller).
|
||||||
|
* For other tools, scales with distance for consistent visual size.
|
||||||
|
*/
|
||||||
function scale() {
|
function scale() {
|
||||||
let cam = space.internals().camera;
|
let cam = space.internals().camera;
|
||||||
let dist = cam.position.distanceTo(pmesh.position);
|
let dist = cam.position.distanceTo(pmesh.position);
|
||||||
|
|
@ -137,8 +215,6 @@ function scale() {
|
||||||
pmesh.scale.set(scale,scale,scale);
|
pmesh.scale.set(scale,scale,scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
api.event.on("space.view.zoom", scale);
|
|
||||||
|
|
||||||
api.event.on('mouse.hover', (ev) => {
|
api.event.on('mouse.hover', (ev) => {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -186,5 +262,16 @@ api.event.on('mouse.hover.up', (ev) => {
|
||||||
endIt();
|
endIt();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export {
|
||||||
export { onLayFlatSelect, onFaceUpSelect, startIt, endIt, cleanup, scale, onDone };
|
cleanup,
|
||||||
|
endIt,
|
||||||
|
onDone,
|
||||||
|
onFaceUpSelect,
|
||||||
|
onLayFlatSelect,
|
||||||
|
scale,
|
||||||
|
startFaceUp,
|
||||||
|
startFocus,
|
||||||
|
startIt,
|
||||||
|
startLeftAlign,
|
||||||
|
startLayFlat,
|
||||||
|
};
|
||||||
159
src/kiri/app/file-ops.js
Normal file
159
src/kiri/app/file-ops.js
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
import { api } from './api.js';
|
||||||
|
import { base } from '../../geo/base.js';
|
||||||
|
import { space } from '../../moto/space.js';
|
||||||
|
|
||||||
|
const DOC = self.document;
|
||||||
|
|
||||||
|
/** Cached platform color before drag-over highlight */
|
||||||
|
let platformColor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle file drag-over event.
|
||||||
|
* Changes platform color to green to indicate drop target.
|
||||||
|
* Prevents drop when modal dialogs are open.
|
||||||
|
* @param {DragEvent} evt - Browser drag event
|
||||||
|
*/
|
||||||
|
function dragOverHandler(evt) {
|
||||||
|
evt.stopPropagation();
|
||||||
|
evt.preventDefault();
|
||||||
|
|
||||||
|
// prevent drop actions when a dialog is open
|
||||||
|
if (api.modal.visible()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
evt.dataTransfer.dropEffect = 'copy';
|
||||||
|
let oldcolor = space.platform.setColor(0x00ff00);
|
||||||
|
if (oldcolor !== 0x00ff00) platformColor = oldcolor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle drag-leave event by restoring original platform color.
|
||||||
|
*/
|
||||||
|
function dragLeave() {
|
||||||
|
space.platform.setColor(platformColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle file drop event.
|
||||||
|
* Restores platform color and loads dropped files.
|
||||||
|
* Handles multi-file drops with optional grouping:
|
||||||
|
* - Single file: loads immediately
|
||||||
|
* - Multiple files: prompts user whether to group
|
||||||
|
* - api.feature.drop_group can override behavior (true=always group, false=never group)
|
||||||
|
* @param {DragEvent} evt - Browser drop event
|
||||||
|
*/
|
||||||
|
function dropHandler(evt) {
|
||||||
|
evt.stopPropagation();
|
||||||
|
evt.preventDefault();
|
||||||
|
|
||||||
|
// prevent drop actions when a dialog is open
|
||||||
|
if (api.modal.visible()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
space.platform.setColor(platformColor);
|
||||||
|
|
||||||
|
let files = evt.dataTransfer.files;
|
||||||
|
|
||||||
|
switch (api.feature.drop_group) {
|
||||||
|
case true:
|
||||||
|
return api.platform.load_files(files, []);
|
||||||
|
case false:
|
||||||
|
return api.platform.load_files(files, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.length === 1) {
|
||||||
|
api.platform.load_files(files);
|
||||||
|
} else if (files.length > 1) {
|
||||||
|
api.uc.confirm(`group ${files.length} files?`).then(yes => {
|
||||||
|
api.platform.load_files(files, yes ? [] : undefined);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a file from the catalog when clicked.
|
||||||
|
* @param {Event} e - Click event from catalog item
|
||||||
|
*/
|
||||||
|
function loadCatalogFile(e) {
|
||||||
|
api.widgets.load(e.target.getAttribute('load'), function(widget) {
|
||||||
|
api.platform.add(widget);
|
||||||
|
api.dialog.hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the catalog UI with current file list.
|
||||||
|
* Builds interactive list with rename, load, and delete buttons for each file.
|
||||||
|
* Sorts files alphabetically (case-insensitive).
|
||||||
|
* @param {object} files - Dictionary of filename -> {vertices, updated} file metadata
|
||||||
|
*/
|
||||||
|
function updateCatalog(files) {
|
||||||
|
let table = api.ui.catalog.list,
|
||||||
|
list = [];
|
||||||
|
table.innerHTML = '';
|
||||||
|
for (let name in files) {
|
||||||
|
list.push({n:name, ln:name.toLowerCase(), v:files[name].vertices, t:files[name].updated});
|
||||||
|
}
|
||||||
|
list.sort(function(a,b) {
|
||||||
|
return a.ln < b.ln ? -1 : 1;
|
||||||
|
});
|
||||||
|
for (let i=0; i<list.length; i++) {
|
||||||
|
let row = DOC.createElement('div'),
|
||||||
|
renm = DOC.createElement('button'),
|
||||||
|
load = DOC.createElement('button'),
|
||||||
|
size = DOC.createElement('button'),
|
||||||
|
del = DOC.createElement('button'),
|
||||||
|
file = list[i],
|
||||||
|
name = file.n,
|
||||||
|
date = new Date(file.t),
|
||||||
|
split = name.split('.'),
|
||||||
|
short = split[0],
|
||||||
|
ext = split[1] ? `.${split[1]}` : '';
|
||||||
|
|
||||||
|
renm.setAttribute('class', 'rename');
|
||||||
|
renm.setAttribute('title', 'rename file');
|
||||||
|
renm.innerHTML = '<i class="far fa-edit"></i>';
|
||||||
|
renm.onclick = () => {
|
||||||
|
api.uc.prompt(`rename file`, short).then(newname => {
|
||||||
|
if (newname && newname !== short) {
|
||||||
|
api.catalog.rename(name, `${newname}${ext}`, then => {
|
||||||
|
api.modal.show('files');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
load.setAttribute('load', name);
|
||||||
|
load.setAttribute('title', `file: ${name}\nvertices: ${file.v}\ndate: ${date}`);
|
||||||
|
load.onclick = loadCatalogFile;
|
||||||
|
load.appendChild(DOC.createTextNode(short));
|
||||||
|
|
||||||
|
del.setAttribute('del', name);
|
||||||
|
del.setAttribute('title', "remove '"+name+"'");
|
||||||
|
del.onclick = () => { api.catalog.deleteFile(name) };
|
||||||
|
del.innerHTML = '<i class="far fa-trash-alt"></i>';
|
||||||
|
|
||||||
|
size.setAttribute("disabled", true);
|
||||||
|
size.setAttribute("class", "label");
|
||||||
|
size.appendChild(DOC.createTextNode(base.util.comma(file.v)));
|
||||||
|
|
||||||
|
row.setAttribute("class", "f-row a-center");
|
||||||
|
row.appendChild(renm);
|
||||||
|
row.appendChild(load);
|
||||||
|
row.appendChild(size);
|
||||||
|
row.appendChild(del);
|
||||||
|
table.appendChild(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fileOps = {
|
||||||
|
dragOverHandler,
|
||||||
|
dragLeave,
|
||||||
|
dropHandler,
|
||||||
|
loadCatalogFile,
|
||||||
|
updateCatalog
|
||||||
|
};
|
||||||
|
|
@ -1,6 +1,15 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages file storage in IndexedDB for 3D model vertices.
|
||||||
|
* Supports file operations (put, get, delete, rename), listener notifications,
|
||||||
|
* and deferred loading for externally-stored files.
|
||||||
|
*/
|
||||||
class Files {
|
class Files {
|
||||||
|
/**
|
||||||
|
* Create a new Files store
|
||||||
|
* @param {object} indexdb - IndexedDB wrapper instance
|
||||||
|
*/
|
||||||
constructor(indexdb) {
|
constructor(indexdb) {
|
||||||
let store = this;
|
let store = this;
|
||||||
this.db = indexdb;
|
this.db = indexdb;
|
||||||
|
|
@ -47,10 +56,21 @@ class Files {
|
||||||
this.listeners.remove(listener);
|
this.listeners.remove(listener);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set handler for deferred file loading.
|
||||||
|
* Deferred files are not stored locally but fetched on-demand.
|
||||||
|
* @param {function} handler - Function(mark, name, callback) to load deferred file
|
||||||
|
*/
|
||||||
setDeferredHandler(handler) {
|
setDeferredHandler(handler) {
|
||||||
this.deferredHandler = handler;
|
this.deferredHandler = handler;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a deferred file placeholder.
|
||||||
|
* File data is not stored locally; the mark is used by the deferred handler to fetch it later.
|
||||||
|
* @param {string} name - Filename
|
||||||
|
* @param {*} mark - Identifier for deferred handler to fetch file (e.g., URL, storage key)
|
||||||
|
*/
|
||||||
putDeferred(name, mark) {
|
putDeferred(name, mark) {
|
||||||
// triggers refresh callback
|
// triggers refresh callback
|
||||||
this.files[name] = {
|
this.files[name] = {
|
||||||
|
|
@ -91,6 +111,13 @@ class Files {
|
||||||
.catch(ondone);
|
.catch(ondone);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rename a file in the catalog.
|
||||||
|
* Copies vertex data to new key, updates file list, and removes old key.
|
||||||
|
* @param {string} name - Current filename
|
||||||
|
* @param {string} newname - New filename
|
||||||
|
* @param {function} callback - Function({error}) called when complete
|
||||||
|
*/
|
||||||
rename(name, newname, callback) {
|
rename(name, newname, callback) {
|
||||||
if (!this.files[name]) return callback({error: 'no such file'});
|
if (!this.files[name]) return callback({error: 'no such file'});
|
||||||
if (!newname || newname == name) return callback({error: 'invalid new name'});
|
if (!newname || newname == name) return callback({error: 'invalid new name'});
|
||||||
|
|
@ -146,6 +173,12 @@ class Files {
|
||||||
if (callback) callback(false);
|
if (callback) callback(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete files matching a filter function.
|
||||||
|
* @param {function} fn - Filter function(key) returning true to delete
|
||||||
|
* @param {*} [from] - Optional start key for range query
|
||||||
|
* @param {*} [to] - Optional end key for range query
|
||||||
|
*/
|
||||||
deleteFilter(fn, from, to) {
|
deleteFilter(fn, from, to) {
|
||||||
this.db.keys(keys => {
|
this.db.keys(keys => {
|
||||||
for (let key of keys) {
|
for (let key of keys) {
|
||||||
|
|
@ -157,17 +190,30 @@ class Files {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save file list to IndexedDB and notify listeners.
|
||||||
|
* @param {Files} store - Files instance
|
||||||
|
*/
|
||||||
function saveFileList(store) {
|
function saveFileList(store) {
|
||||||
store.db.put('files', store.files);
|
store.db.put('files', store.files);
|
||||||
notifyFileListeners(store);
|
notifyFileListeners(store);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify all registered listeners of file list changes.
|
||||||
|
* @param {Files} store - Files instance
|
||||||
|
*/
|
||||||
function notifyFileListeners(store) {
|
function notifyFileListeners(store) {
|
||||||
for (let i=0; i<store.listeners.length; i++) {
|
for (let i=0; i<store.listeners.length; i++) {
|
||||||
store.listeners[i](store.files);
|
store.listeners[i](store.files);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory function to create a new Files instance.
|
||||||
|
* @param {object} indexdb - IndexedDB wrapper instance
|
||||||
|
* @returns {Files} New Files store
|
||||||
|
*/
|
||||||
export const openFiles = function(indexdb) {
|
export const openFiles = function(indexdb) {
|
||||||
return new Files(indexdb);
|
return new Files(indexdb);
|
||||||
};
|
};
|
||||||
|
|
@ -1,12 +1,38 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frame Message API for iframe integration.
|
||||||
|
*
|
||||||
|
* Enables parent windows to control embedded Kiri:Moto instances via postMessage.
|
||||||
|
* Controlled by api.feature.frame flag.
|
||||||
|
*
|
||||||
|
* Supported message types:
|
||||||
|
* - mode: Set operating mode (FDM, CAM, SLA, LASER, etc.)
|
||||||
|
* - view: Set view mode (ARRANGE, SLICE, PREVIEW, etc.)
|
||||||
|
* - function: Call api.function methods (slice, print, export, etc.)
|
||||||
|
* - event: Subscribe to events
|
||||||
|
* - emit: Emit events
|
||||||
|
* - get: Query state (mode, device, process, widgets)
|
||||||
|
* - set: Update state
|
||||||
|
* - features: Update feature flags
|
||||||
|
* - device/process/controller: Update settings
|
||||||
|
* - parse: Parse and load file data (STL, OBJ, 3MF, SVG)
|
||||||
|
* - load: Load URL
|
||||||
|
* - clear: Clear platform
|
||||||
|
* - alert: Show alert
|
||||||
|
* - progress: Update progress bar
|
||||||
|
*/
|
||||||
|
|
||||||
import { api } from './api.js';
|
import { api } from './api.js';
|
||||||
import { load } from '../../load/file.js';
|
import { load } from '../../load/file.js';
|
||||||
import { newWidget } from './widget.js';
|
import { newWidget } from './widget.js';
|
||||||
import { VIEWS } from './consts.js';
|
import { VIEWS } from './consts.js';
|
||||||
import { widgets } from './widgets.js';
|
import { widgets } from './widgets.js';
|
||||||
|
|
||||||
// add frame message api listener
|
/**
|
||||||
|
* Frame message API listener.
|
||||||
|
* Handles postMessage communication between parent window and embedded Kiri:Moto.
|
||||||
|
*/
|
||||||
window.addEventListener('message', msg => {
|
window.addEventListener('message', msg => {
|
||||||
const { conf, event, feature, platform, settings, show } = api;
|
const { conf, event, feature, platform, settings, show } = api;
|
||||||
|
|
||||||
|
|
@ -89,6 +115,9 @@ window.addEventListener('message', msg => {
|
||||||
let bin = data.parse;
|
let bin = data.parse;
|
||||||
let widget;
|
let widget;
|
||||||
switch ((data.type || 'stl').toLowerCase()) {
|
switch ((data.type || 'stl').toLowerCase()) {
|
||||||
|
case 'gcode':
|
||||||
|
api.function.parse(bin, 'gcode');
|
||||||
|
break;
|
||||||
case 'stl':
|
case 'stl':
|
||||||
if (!bin.buffer) bin = new Float32Array(bin).buffer;
|
if (!bin.buffer) bin = new Float32Array(bin).buffer;
|
||||||
new load.STL().parse(bin, vertices => {
|
new load.STL().parse(bin, vertices => {
|
||||||
|
|
@ -1,14 +1,43 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
import { api } from './api.js';
|
import { api } from './api.js';
|
||||||
import { client } from './client.js';
|
import { client } from './workers.js';
|
||||||
import { codec } from './codec.js';
|
import { codec } from '../core/codec.js';
|
||||||
import { space } from '../../moto/space.js';
|
import { space } from '../../moto/space.js';
|
||||||
import { COLOR, PMODES } from './consts.js';
|
import { PMODES } from './consts.js';
|
||||||
|
import { colorSchemeRegistry } from './color/schemes.js';
|
||||||
import { exportFile } from './export.js';
|
import { exportFile } from './export.js';
|
||||||
|
|
||||||
let complete = {};
|
/**
|
||||||
|
* Get current color scheme
|
||||||
|
*/
|
||||||
|
function getColorScheme() {
|
||||||
|
const mode = api.mode.get_id();
|
||||||
|
const theme = api.space.is_dark() ? 'dark' : 'light';
|
||||||
|
return colorSchemeRegistry.getScheme(mode, theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks completion state of operations to prevent redundant work.
|
||||||
|
* Properties: slice, preview, export
|
||||||
|
*/
|
||||||
|
let complete = {};
|
||||||
|
let order;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare and execute slicing for all widgets on the platform.
|
||||||
|
* Main slicing function that:
|
||||||
|
* - Takes screenshots for export/preview
|
||||||
|
* - Handles belt mode layout
|
||||||
|
* - Slices each widget sequentially via worker
|
||||||
|
* - Tracks progress across all widgets
|
||||||
|
* - Renders sliced layers to stacks
|
||||||
|
* - Emits slice.begin, slice, slice.end, slice.error events
|
||||||
|
*
|
||||||
|
* @param {function} [callback] - Called when slicing completes
|
||||||
|
* @param {number} [scale=1] - Progress bar scale factor (for chaining operations)
|
||||||
|
* @param {number} [offset=0] - Progress bar offset (for chaining operations)
|
||||||
|
*/
|
||||||
function prepareSlices(callback, scale = 1, offset = 0) {
|
function prepareSlices(callback, scale = 1, offset = 0) {
|
||||||
const { conf, event, feature, hide, mode, view, platform, show, stacks } = api;
|
const { conf, event, feature, hide, mode, view, platform, show, stacks } = api;
|
||||||
|
|
||||||
|
|
@ -28,6 +57,10 @@ function prepareSlices(callback, scale = 1, offset = 0) {
|
||||||
callback = preparePreview;
|
callback = preparePreview;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
order = api.selection.count() ?
|
||||||
|
Object.assign({}, ...api.selection.widgets().map((w,i) => ({[w.id]:i}))) :
|
||||||
|
undefined;
|
||||||
|
|
||||||
const widgets = api.widgets.all();
|
const widgets = api.widgets.all();
|
||||||
const settings = conf.get();
|
const settings = conf.get();
|
||||||
const { device, process, controller } = settings;
|
const { device, process, controller } = settings;
|
||||||
|
|
@ -73,7 +106,8 @@ function prepareSlices(callback, scale = 1, offset = 0) {
|
||||||
|
|
||||||
let totalProgress;
|
let totalProgress;
|
||||||
|
|
||||||
api.widgets.setOpacity(COLOR.slicing_opacity);
|
const scheme = getColorScheme();
|
||||||
|
api.widgets.setOpacity(scheme.views.SLICE.slicing_opacity);
|
||||||
|
|
||||||
let segtimes = {},
|
let segtimes = {},
|
||||||
segNumber = 0,
|
segNumber = 0,
|
||||||
|
|
@ -87,7 +121,8 @@ function prepareSlices(callback, scale = 1, offset = 0) {
|
||||||
for (let widget of toSlice) {
|
for (let widget of toSlice) {
|
||||||
widget.belt = null;
|
widget.belt = null;
|
||||||
widget.stats.progress = 0;
|
widget.stats.progress = 0;
|
||||||
widget.setColor(COLOR.slicing);
|
const scheme = getColorScheme();
|
||||||
|
widget.setColor(0xffaaaa); // slicing color (from old COLOR.slicing)
|
||||||
let { extruder } = widget.anno;
|
let { extruder } = widget.anno;
|
||||||
if (extruder >= 0) {
|
if (extruder >= 0) {
|
||||||
extruders[extruder] = extruder;
|
extruders[extruder] = extruder;
|
||||||
|
|
@ -114,13 +149,17 @@ function prepareSlices(callback, scale = 1, offset = 0) {
|
||||||
client.rotate(settings);
|
client.rotate(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sliceSetup() {
|
||||||
|
client.slicePre(settings, sliceNext);
|
||||||
|
}
|
||||||
|
|
||||||
function sliceNext() {
|
function sliceNext() {
|
||||||
if (toSlice.length) {
|
if (toSlice.length) {
|
||||||
// while more widgets to slice
|
// while more widgets to slice
|
||||||
sliceWidget(toSlice.shift())
|
sliceWidget(toSlice.shift())
|
||||||
} else {
|
} else {
|
||||||
// once all slicing done, run sliceAll() once for all widgets
|
// once all slicing done, run slicePost() once for all widgets
|
||||||
client.sliceAll(settings, sliceDone);
|
client.slicePost(settings, sliceDone);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,8 +173,8 @@ function prepareSlices(callback, scale = 1, offset = 0) {
|
||||||
let mark = Date.now();
|
let mark = Date.now();
|
||||||
if (lastMsg) {
|
if (lastMsg) {
|
||||||
let key = slicing.length > 1 ?
|
let key = slicing.length > 1 ?
|
||||||
`${widget.id}_${segNumber++}_${lastMsg}` :
|
`${widget.id} ${segNumber++} ${lastMsg}` :
|
||||||
`${segNumber++}_${lastMsg}`
|
`${segNumber++} ${lastMsg}`
|
||||||
segtimes[key] = mark - startTime;
|
segtimes[key] = mark - startTime;
|
||||||
}
|
}
|
||||||
lastMsg = msg;
|
lastMsg = msg;
|
||||||
|
|
@ -159,8 +198,8 @@ function prepareSlices(callback, scale = 1, offset = 0) {
|
||||||
// update segment time
|
// update segment time
|
||||||
if (lastMsg) {
|
if (lastMsg) {
|
||||||
let key = slicing.length > 1 ?
|
let key = slicing.length > 1 ?
|
||||||
`${widget.id}_${segNumber++}_${lastMsg}` :
|
`${widget.id} ${segNumber++} ${lastMsg}` :
|
||||||
`${segNumber++}_${lastMsg}`
|
`${segNumber++} ${lastMsg}`
|
||||||
segtimes[`${key}`] = mark - startTime;
|
segtimes[`${key}`] = mark - startTime;
|
||||||
}
|
}
|
||||||
event.emit('slice', settings.mode);
|
event.emit('slice', settings.mode);
|
||||||
|
|
@ -241,8 +280,8 @@ function prepareSlices(callback, scale = 1, offset = 0) {
|
||||||
for (let widget of slicing) {
|
for (let widget of slicing) {
|
||||||
// on done
|
// on done
|
||||||
let key = slicing.length > 1 ?
|
let key = slicing.length > 1 ?
|
||||||
`${widget.id}_${segNumber++}_draw` :
|
`${widget.id}_${segNumber++} draw` :
|
||||||
`${segNumber++}_draw`
|
`${segNumber++} draw`
|
||||||
segtimes[`${key}`] = widget.render(widget.stack);
|
segtimes[`${key}`] = widget.render(widget.stack);
|
||||||
// rotate stack for belt beds
|
// rotate stack for belt beds
|
||||||
if (widget.belt) {
|
if (widget.belt) {
|
||||||
|
|
@ -250,9 +289,10 @@ function prepareSlices(callback, scale = 1, offset = 0) {
|
||||||
}
|
}
|
||||||
if (scale === 1) {
|
if (scale === 1) {
|
||||||
// clear wireframe
|
// clear wireframe
|
||||||
widget.setWireframe(false, COLOR.wireframe, COLOR.wireframe_opacity);
|
const scheme = getColorScheme();
|
||||||
widget.setOpacity(camOrLaser ? COLOR.cam_sliced_opacity : COLOR.sliced_opacity);
|
widget._setWireframe(false, scheme.wireframe.color, scheme.wireframe.opacity);
|
||||||
widget.setColor(COLOR.deselected);
|
widget.setOpacity(camOrLaser ? scheme.views.SLICE.sliced_opacity ?? 0.2 : scheme.views.PREVIEW?.sliced_opacity ?? 0.0);
|
||||||
|
widget.setColor(scheme.widget.deselected);
|
||||||
api.hide.alert(alert);
|
api.hide.alert(alert);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -273,6 +313,7 @@ function prepareSlices(callback, scale = 1, offset = 0) {
|
||||||
// print stats
|
// print stats
|
||||||
segtimes.total = Date.now() - mark;
|
segtimes.total = Date.now() - mark;
|
||||||
console.log(segtimes);
|
console.log(segtimes);
|
||||||
|
api.visuals.update_stats(segtimes);
|
||||||
if (callback && typeof callback === 'function') {
|
if (callback && typeof callback === 'function') {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
|
|
@ -285,10 +326,22 @@ function prepareSlices(callback, scale = 1, offset = 0) {
|
||||||
space.refresh();
|
space.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
// kick off slicing chain
|
// do any global setup / state management
|
||||||
sliceNext();
|
// then kick off slicing chain
|
||||||
|
sliceSetup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare preview/print visualization.
|
||||||
|
* Generates toolpaths and renders them as 3D lines.
|
||||||
|
* Handles multiple preview modes (speed, filament, layer) via feature.pmode.
|
||||||
|
* Auto-runs slicing first if not already complete.
|
||||||
|
* Emits preview.begin, print, preview.end, preview.error events.
|
||||||
|
*
|
||||||
|
* @param {function} [callback] - Called when preview completes
|
||||||
|
* @param {number} [scale=1] - Progress bar scale factor (for chaining operations)
|
||||||
|
* @param {number} [offset=0] - Progress bar offset (for chaining operations)
|
||||||
|
*/
|
||||||
function preparePreview(callback, scale = 1, offset = 0) {
|
function preparePreview(callback, scale = 1, offset = 0) {
|
||||||
const { conf, event, feature, hide, mode, view, platform, show, stacks } = api;
|
const { conf, event, feature, hide, mode, view, platform, show, stacks } = api;
|
||||||
const widgets = api.widgets.all();
|
const widgets = api.widgets.all();
|
||||||
|
|
@ -325,10 +378,12 @@ function preparePreview(callback, scale = 1, offset = 0) {
|
||||||
event.emit('preview.begin', pMode);
|
event.emit('preview.begin', pMode);
|
||||||
|
|
||||||
if (isCam) {
|
if (isCam) {
|
||||||
api.widgets.setOpacity(isDark ? COLOR.cam_preview_opacity_dark : COLOR.cam_preview_opacity);
|
const scheme = getColorScheme();
|
||||||
api.widgets.setColor(isDark ? COLOR.cam_preview_dark : COLOR.cam_preview);
|
api.widgets.setOpacity(scheme.views.PREVIEW.preview_opacity);
|
||||||
|
api.widgets.setColor(scheme.views.PREVIEW.preview);
|
||||||
} else if (offset === 0) {
|
} else if (offset === 0) {
|
||||||
api.widgets.setOpacity(COLOR.preview_opacity);
|
const scheme = getColorScheme();
|
||||||
|
api.widgets.setOpacity(scheme.views.PREVIEW.preview_opacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mark = Date.now(),
|
let mark = Date.now(),
|
||||||
|
|
@ -341,6 +396,7 @@ function preparePreview(callback, scale = 1, offset = 0) {
|
||||||
// pass preview mode to worker
|
// pass preview mode to worker
|
||||||
settings.pmode = feature.pmode;
|
settings.pmode = feature.pmode;
|
||||||
settings.render = true;
|
settings.render = true;
|
||||||
|
settings.order = order;
|
||||||
|
|
||||||
client.prepare(settings, (progress, message, layer) => {
|
client.prepare(settings, (progress, message, layer) => {
|
||||||
if (layer) {
|
if (layer) {
|
||||||
|
|
@ -349,7 +405,7 @@ function preparePreview(callback, scale = 1, offset = 0) {
|
||||||
if (message && message !== lastMsg) {
|
if (message && message !== lastMsg) {
|
||||||
const mark = Date.now();
|
const mark = Date.now();
|
||||||
if (lastMsg) {
|
if (lastMsg) {
|
||||||
segtimes[`${segNumber++}_${lastMsg}`] = mark - startTime;
|
segtimes[`${segNumber++} ${lastMsg}`] = mark - startTime;
|
||||||
}
|
}
|
||||||
lastMsg = message;
|
lastMsg = message;
|
||||||
startTime = mark;
|
startTime = mark;
|
||||||
|
|
@ -367,7 +423,7 @@ function preparePreview(callback, scale = 1, offset = 0) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastMsg) {
|
if (lastMsg) {
|
||||||
segtimes[`${segNumber++}_${lastMsg}`] = Date.now() - startTime;
|
segtimes[`${segNumber++} ${lastMsg}`] = Date.now() - startTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
show.progress(0);
|
show.progress(0);
|
||||||
|
|
@ -393,12 +449,13 @@ function preparePreview(callback, scale = 1, offset = 0) {
|
||||||
stack.obj.rotate(widgets[0].belt);
|
stack.obj.rotate(widgets[0].belt);
|
||||||
}
|
}
|
||||||
api.hide.alert(alert);
|
api.hide.alert(alert);
|
||||||
segtimes[`${segNumber}_draw`] = Date.now() - startTime;
|
segtimes[`${segNumber} draw`] = Date.now() - startTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
// print stats
|
// print stats
|
||||||
segtimes.total = Date.now() - mark;
|
segtimes.total = Date.now() - mark;
|
||||||
console.log(segtimes);
|
console.log(segtimes);
|
||||||
|
api.visuals.update_stats(segtimes);
|
||||||
|
|
||||||
event.emit('print', pMode);
|
event.emit('print', pMode);
|
||||||
event.emit('preview.end', pMode);
|
event.emit('preview.end', pMode);
|
||||||
|
|
@ -427,6 +484,10 @@ function preparePreview(callback, scale = 1, offset = 0) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare animation (requires SharedArrayBuffer support).
|
||||||
|
* Checks for browser support and emits function.animate event.
|
||||||
|
*/
|
||||||
function prepareAnimation() {
|
function prepareAnimation() {
|
||||||
if (!window.SharedArrayBuffer) {
|
if (!window.SharedArrayBuffer) {
|
||||||
api.alerts.show("The security context of this");
|
api.alerts.show("The security context of this");
|
||||||
|
|
@ -437,6 +498,12 @@ function prepareAnimation() {
|
||||||
api.event.emit("function.animate", {mode: api.conf.get().mode});
|
api.event.emit("function.animate", {mode: api.conf.get().mode});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare and trigger export.
|
||||||
|
* Auto-runs preview first if not already complete.
|
||||||
|
* Delegates to exportFile() for mode-specific export.
|
||||||
|
* @param {...*} args - Arguments passed through to exportFile()
|
||||||
|
*/
|
||||||
function prepareExport() {
|
function prepareExport() {
|
||||||
const settings = api.conf.get();
|
const settings = api.conf.get();
|
||||||
const argsave = arguments;
|
const argsave = arguments;
|
||||||
|
|
@ -449,12 +516,23 @@ function prepareExport() {
|
||||||
exportFile(...argsave);
|
exportFile(...argsave);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel running worker operation by restarting the worker.
|
||||||
|
*/
|
||||||
function cancelWorker() {
|
function cancelWorker() {
|
||||||
if (client.isBusy()) {
|
if (client.isBusy()) {
|
||||||
client.restart();
|
client.restart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse and visualize gcode or other toolpath code.
|
||||||
|
* Sends code to worker for parsing, renders result as 3D preview.
|
||||||
|
* Emits code.load and code.loaded events.
|
||||||
|
*
|
||||||
|
* @param {string} code - Gcode or toolpath text
|
||||||
|
* @param {string} type - Code type identifier (gcode, etc.)
|
||||||
|
*/
|
||||||
function parseCode(code, type) {
|
function parseCode(code, type) {
|
||||||
const { conf, event, show, stacks, widgets, view } = api;
|
const { conf, event, show, stacks, widgets, view } = api;
|
||||||
const settings = conf.get();
|
const settings = conf.get();
|
||||||
|
|
@ -479,6 +557,10 @@ function parseCode(code, type) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear operation completion tracking.
|
||||||
|
* Resets complete state so operations can run again.
|
||||||
|
*/
|
||||||
function clear_progress() {
|
function clear_progress() {
|
||||||
complete = {};
|
complete = {};
|
||||||
}
|
}
|
||||||
24
src/kiri/app/groups.js
Normal file
24
src/kiri/app/groups.js
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
import { selection } from './selected.js';
|
||||||
|
import { Widget } from './widget.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge selected widgets into a group.
|
||||||
|
* Grouped widgets move together as a unit.
|
||||||
|
*/
|
||||||
|
function groupMerge() {
|
||||||
|
Widget.Groups.merge(selection.widgets(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split grouped widgets back into individual widgets.
|
||||||
|
*/
|
||||||
|
function groupSplit() {
|
||||||
|
Widget.Groups.split(selection.widgets(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const group = {
|
||||||
|
merge: groupMerge,
|
||||||
|
split: groupSplit,
|
||||||
|
};
|
||||||
36
src/kiri/app/help.js
Normal file
36
src/kiri/app/help.js
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
import { $ } from '../../moto/webui.js';
|
||||||
|
import { api } from './api.js';
|
||||||
|
import { modal } from './modal.js';
|
||||||
|
import { version } from '../../moto/license.js';
|
||||||
|
|
||||||
|
const WIN = self.window;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show local help dialog.
|
||||||
|
*/
|
||||||
|
function showHelp() {
|
||||||
|
showHelpFile(`local`,() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show help dialog or open external docs.
|
||||||
|
* @param {string} local - If truthy, shows local help modal; otherwise opens docs.grid.space
|
||||||
|
* @param {function} then - Callback after help shown
|
||||||
|
*/
|
||||||
|
function showHelpFile(local,then) {
|
||||||
|
if (!local) {
|
||||||
|
WIN.open("//docs.grid.space/", "_help");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const LANG = api.language.current;
|
||||||
|
$('kiri-version').innerHTML = `${LANG.version} ${version}`;
|
||||||
|
modal.show('help');
|
||||||
|
api.event.emit('help.show', local);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const help = {
|
||||||
|
show: showHelp,
|
||||||
|
file: showHelpFile
|
||||||
|
};
|
||||||
103
src/kiri/app/image.js
Normal file
103
src/kiri/app/image.js
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
import { $ } from '../../moto/webui.js';
|
||||||
|
import { api } from './api.js';
|
||||||
|
import { newWidget } from './widget.js';
|
||||||
|
import { platform } from './platform.js';
|
||||||
|
import { settings } from './conf/manager.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show image import dialog with conversion options.
|
||||||
|
* Prompts for blur, inversion, base size, and border settings.
|
||||||
|
* Large images (>2.5MB) show a warning before proceeding.
|
||||||
|
* @param {ArrayBuffer} image - PNG image data
|
||||||
|
* @param {string} name - Filename
|
||||||
|
* @param {boolean} [force] - Skip size warning if true
|
||||||
|
*/
|
||||||
|
function loadImageDialog(image, name, force) {
|
||||||
|
if (!force && image.byteLength > 2500000) {
|
||||||
|
return api.uc.confirm("Large images may fail to import<br>Consider resizing under 1000 x 1000<br>Proceed with import?").then(ok => {
|
||||||
|
if (ok) {
|
||||||
|
loadImageDialog(image, name, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const opt = {pre: [
|
||||||
|
"<div class='f-col a-center'>",
|
||||||
|
" <h3>Image Conversion</h3>",
|
||||||
|
" <p class='t-just' style='width:300px;line-height:1.5em'>",
|
||||||
|
" This will create a 3D model from a 2D PNG image. Photos must",
|
||||||
|
" be blurred to be usable. Values from 0=off to 50=high are suggested.",
|
||||||
|
" Higher values incur more processing time.",
|
||||||
|
" </p>",
|
||||||
|
" <div class='f-row t-right'><table>",
|
||||||
|
" <tr><th>blur value</th><td><input id='png-blur' value='0' size='3'></td>",
|
||||||
|
" <th> invert image</th><td><input id='png-inv' type='checkbox'></td></tr>",
|
||||||
|
" <tr><th>base size</th><td><input id='png-base' value='0' size='3'></td>",
|
||||||
|
" <th> invert alpha</th><td><input id='alpha-inv' type='checkbox'></td></tr>",
|
||||||
|
" <tr><th>border size</th><td><input id='png-border' value='0' size='3'></td>",
|
||||||
|
" <th></th><td></td></tr>",
|
||||||
|
" </table></div>",
|
||||||
|
"</div>"
|
||||||
|
]};
|
||||||
|
api.uc.confirm(undefined, {convert:true, cancel:false}, undefined, opt).then((ok) => {
|
||||||
|
if (ok) {
|
||||||
|
loadImage(image, {
|
||||||
|
file: name,
|
||||||
|
blur: parseInt($('png-blur').value) || 0,
|
||||||
|
base: parseInt($('png-base').value) || 0,
|
||||||
|
border: parseInt($('png-border').value) || 0,
|
||||||
|
inv_image: $('png-inv').checked,
|
||||||
|
inv_alpha: $('alpha-inv').checked
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert PNG image to 3D mesh using worker.
|
||||||
|
* Creates height map from pixel brightness and generates vertices.
|
||||||
|
* @param {ArrayBuffer} image - PNG image data
|
||||||
|
* @param {object} [opt={}] - Options: file, blur, base, border, inv_image, inv_alpha
|
||||||
|
*/
|
||||||
|
function loadImage(image, opt = {}) {
|
||||||
|
const info = Object.assign({settings: settings.get(), png:image}, opt);
|
||||||
|
api.client.image2mesh(info, progress => {
|
||||||
|
api.show.progress(progress, "converting");
|
||||||
|
}, vertices => {
|
||||||
|
api.show.progress(0);
|
||||||
|
const widget = newWidget().loadVertices(vertices);
|
||||||
|
widget.meta.file = opt.file;
|
||||||
|
platform.add(widget);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert any image format to PNG before loading.
|
||||||
|
* Uses canvas to convert image blob to PNG data URL.
|
||||||
|
* @param {ArrayBuffer} res - Image data in any format
|
||||||
|
* @param {string} name - Filename
|
||||||
|
*/
|
||||||
|
function loadImageConvert(res, name) {
|
||||||
|
let url = URL.createObjectURL(new Blob([res]));
|
||||||
|
|
||||||
|
$('mod-any').innerHTML = `<img id="xsrc" src="${url}"><canvas id="xdst"></canvas>`;
|
||||||
|
|
||||||
|
let img = $('xsrc');
|
||||||
|
let can = $('xdst');
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
can.width = img.width;
|
||||||
|
can.height = img.height;
|
||||||
|
let ctx = can.getContext('2d');
|
||||||
|
ctx.drawImage(img, 0, 0);
|
||||||
|
fetch(can.toDataURL()).then(r => r.arrayBuffer()).then(data => {
|
||||||
|
loadImageDialog(data, name);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const image = {
|
||||||
|
dialog: loadImageDialog,
|
||||||
|
convert: loadImageConvert
|
||||||
|
};
|
||||||
134
src/kiri/app/init/bind.js
Normal file
134
src/kiri/app/init/bind.js
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
import { $ } from '../../../moto/webui.js';
|
||||||
|
|
||||||
|
export function bind() {
|
||||||
|
return {
|
||||||
|
alert: {
|
||||||
|
dialog: $('alert-area'),
|
||||||
|
text: $('alert-text')
|
||||||
|
},
|
||||||
|
func: {
|
||||||
|
slice: $('act-slice'),
|
||||||
|
preview: $('act-preview'),
|
||||||
|
animate: $('act-animate'),
|
||||||
|
export: $('act-export')
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
slice: $('label-slice'),
|
||||||
|
preview: $('label-preview'),
|
||||||
|
animate: $('label-animate'),
|
||||||
|
export: $('label-export'),
|
||||||
|
},
|
||||||
|
acct: {
|
||||||
|
help: $('app-help'),
|
||||||
|
don8: $('app-don8'),
|
||||||
|
mesh: $('app-mesh'),
|
||||||
|
export: $('app-export')
|
||||||
|
},
|
||||||
|
dev: {
|
||||||
|
header: $('dev-header'),
|
||||||
|
search: $('dev-search'),
|
||||||
|
filter: $('dev-filter')
|
||||||
|
},
|
||||||
|
mesh: {
|
||||||
|
name: $('mesh-name'),
|
||||||
|
points: $('mesh-points'),
|
||||||
|
faces: $('mesh-faces'),
|
||||||
|
},
|
||||||
|
|
||||||
|
stats: {
|
||||||
|
fps: $('fps'),
|
||||||
|
rms: $('rms'),
|
||||||
|
div: $('stats'),
|
||||||
|
rnfo: $('rnfo'),
|
||||||
|
},
|
||||||
|
|
||||||
|
load: $('load-file'),
|
||||||
|
speeds: $('speeds'),
|
||||||
|
speedbar: $('speedbar'),
|
||||||
|
|
||||||
|
context: $('context-menu'),
|
||||||
|
|
||||||
|
// ltsetup: $('lt-setup'),
|
||||||
|
// ltfile: $('lt-file'),
|
||||||
|
// ltview: $('lt-view'),
|
||||||
|
// ltact: $('lt-start'),
|
||||||
|
// edit: $('lt-tools'),
|
||||||
|
nozzle: $('menu-nozzle'),
|
||||||
|
|
||||||
|
modal: $('modal'),
|
||||||
|
modalBox: $('modal-box'),
|
||||||
|
modals: {
|
||||||
|
help: $('mod-help'),
|
||||||
|
setup: $('mod-setup'),
|
||||||
|
prefs: $('mod-prefs'),
|
||||||
|
files: $('mod-files'),
|
||||||
|
saves: $('mod-saves'),
|
||||||
|
tools: $('mod-tools'),
|
||||||
|
xany: $('mod-x-any'),
|
||||||
|
xsla: $('mod-x-sla'),
|
||||||
|
xlaser: $('mod-x-laser'),
|
||||||
|
don8: $('mod-don8'),
|
||||||
|
any: $('mod-any'),
|
||||||
|
},
|
||||||
|
|
||||||
|
catalog: {
|
||||||
|
body: $('catalogBody'),
|
||||||
|
list: $('catalogList'),
|
||||||
|
},
|
||||||
|
|
||||||
|
devices: {
|
||||||
|
div: $('devices'),
|
||||||
|
add: $('device-add'),
|
||||||
|
delete: $('device-del'),
|
||||||
|
export: $('device-exp'),
|
||||||
|
rename: $('device-ren'),
|
||||||
|
save: $('device-save'),
|
||||||
|
},
|
||||||
|
|
||||||
|
setMenu: $('set-menu'),
|
||||||
|
settings: $('settings'),
|
||||||
|
settingsBody: $('settingsBody'),
|
||||||
|
settingsList: $('settingsList'),
|
||||||
|
settingsName: $('settingsName'),
|
||||||
|
settingsSave: $('settingsSave'),
|
||||||
|
|
||||||
|
slider: {
|
||||||
|
div: $('slider'),
|
||||||
|
hi: $('slider-hi'),
|
||||||
|
hold: $('slider-hold'),
|
||||||
|
lo: $('slider-lo'),
|
||||||
|
max: $('slider-max'),
|
||||||
|
mid: $('slider-mid'),
|
||||||
|
min: $('slider-zero'),
|
||||||
|
range: $('slider-center'),
|
||||||
|
},
|
||||||
|
|
||||||
|
loading: $('progress').style,
|
||||||
|
progress: $('progbar').style,
|
||||||
|
prostatus: $('progtxt'),
|
||||||
|
selection: $('selection'),
|
||||||
|
|
||||||
|
size: {
|
||||||
|
X: $('size_x'),
|
||||||
|
Y: $('size_y'),
|
||||||
|
Z: $('size_z'),
|
||||||
|
},
|
||||||
|
scale: {
|
||||||
|
X: $('scale_x'),
|
||||||
|
Y: $('scale_y'),
|
||||||
|
Z: $('scale_z'),
|
||||||
|
},
|
||||||
|
lock: {
|
||||||
|
X: $('lock_x'),
|
||||||
|
Y: $('lock_y'),
|
||||||
|
Z: $('lock_z'),
|
||||||
|
},
|
||||||
|
|
||||||
|
stock: $('stock'),
|
||||||
|
stockWidth: $('stock-width'),
|
||||||
|
stockDepth: $('stock-width'),
|
||||||
|
stockHeight: $('stock-width'),
|
||||||
|
};
|
||||||
|
}
|
||||||
546
src/kiri/app/init/input.js
Normal file
546
src/kiri/app/init/input.js
Normal file
|
|
@ -0,0 +1,546 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
import { $ } from '../../../moto/webui.js';
|
||||||
|
import { api } from '../api.js';
|
||||||
|
import { bind } from './bind.js';
|
||||||
|
import { version } from '../../../moto/license.js';
|
||||||
|
import { fileOps } from '../file-ops.js';
|
||||||
|
import { init as initCAM } from '../../mode/cam/app/init-ui.js';
|
||||||
|
import { init as initDRAG } from '../../mode/drag/init-ui.js';
|
||||||
|
import { init as initFDM } from '../../mode/fdm/app/init-ui.js';
|
||||||
|
import { init as initLaser } from '../../mode/laser/init-ui.js';
|
||||||
|
import { init as initSLA } from '../../mode/sla/app/init-ui.js';
|
||||||
|
import { init as initWEDM } from '../../mode/wedm/init-ui.js';
|
||||||
|
import { init as initWJET } from '../../mode/wjet/init-ui.js';
|
||||||
|
import { interact } from '../mouse.js';
|
||||||
|
import { keyboard } from '../keyboard.js';
|
||||||
|
import { local as sdb } from '../../../data/local.js';
|
||||||
|
import { menu as menuCAM } from '../../mode/cam/app/init-menu.js';
|
||||||
|
import { menu as menuFDM } from '../../mode/fdm/app/init-menu.js';
|
||||||
|
import { menu as menuLaser } from '../../mode/laser/init-menu.js';
|
||||||
|
import { menu as menuSLA } from '../../mode/sla/app/init-menu.js';
|
||||||
|
import { modal } from '../modal.js';
|
||||||
|
import { preferences } from '../preferences.js';
|
||||||
|
import { settings as set_ctrl } from '../conf/manager.js';
|
||||||
|
import { settingsOps } from '../conf/settings.js';
|
||||||
|
import { slider } from '../slider.js';
|
||||||
|
import { space } from '../../../moto/space.js';
|
||||||
|
import { LAST, MODES, SEED, VIEWS } from '../consts.js';
|
||||||
|
|
||||||
|
import STACKS from '../stacks.js';
|
||||||
|
import '../frame.js';
|
||||||
|
|
||||||
|
let { SETUP } = api.const,
|
||||||
|
{ CAM, SLA, FDM, LASER, DRAG, WJET, WEDM } = MODES,
|
||||||
|
{ catalog, platform, selection, stats } = api,
|
||||||
|
{ input_binding, input_position, input_rotate } = selection,
|
||||||
|
DOC = self.document,
|
||||||
|
WIN = self.window,
|
||||||
|
LANG = api.language.current,
|
||||||
|
TWOD = [ LASER, DRAG, WJET, WEDM ],
|
||||||
|
TWONED = [ LASER, DRAG, WJET ],
|
||||||
|
THREED = [ FDM, CAM, SLA ],
|
||||||
|
GCODE = [ FDM, CAM, ...TWOD ],
|
||||||
|
CAM_LZR = [ CAM, ...TWOD ],
|
||||||
|
FDM_LZN = [ FDM, ...TWONED ],
|
||||||
|
NO_WEDM = [ FDM, CAM, SLA, LASER, DRAG, WJET ],
|
||||||
|
FDM_CAM = [ FDM, CAM ],
|
||||||
|
inline = true,
|
||||||
|
driven = true,
|
||||||
|
trigger = true,
|
||||||
|
units = true,
|
||||||
|
ui = api.ui,
|
||||||
|
uc = api.uc;
|
||||||
|
|
||||||
|
function settings() {
|
||||||
|
return api.conf.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTool(ev) {
|
||||||
|
api.tool.update(ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkSeed(then) {
|
||||||
|
// skip sample object load in onshape (or any script postload)
|
||||||
|
if (!sdb[SEED]) {
|
||||||
|
sdb[SEED] = new Date().getTime();
|
||||||
|
if (!SETUP.s && api.feature.seed) {
|
||||||
|
if (SETUP.debug) {
|
||||||
|
return then();
|
||||||
|
}
|
||||||
|
platform.load_stl("/obj/cube.stl", function(vert) {
|
||||||
|
catalog.putFile("sample cube.stl", vert);
|
||||||
|
platform.update_bounds();
|
||||||
|
space.view.home();
|
||||||
|
setTimeout(() => { api.space.save(true) },500);
|
||||||
|
then();
|
||||||
|
api.help.show();
|
||||||
|
sdb[LAST] = version;
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (sdb[LAST] !== version) {
|
||||||
|
sdb[LAST] = version;
|
||||||
|
api.help.show();
|
||||||
|
}
|
||||||
|
then();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// upon restore, seed presets
|
||||||
|
api.event.emit('preset', api.conf.dbo());
|
||||||
|
|
||||||
|
// api.event.on("set.threaded", bool => setThreaded(bool));
|
||||||
|
|
||||||
|
export function onBooleanClick(el) {
|
||||||
|
// copy some ui elements to target settings
|
||||||
|
let settings = api.conf.get();
|
||||||
|
settings.device.bedBelt = ui.deviceBelt.checked && api.mode.is_fdm();
|
||||||
|
settings.device.bedRound = ui.deviceRound.checked && api.mode.is_fdm();
|
||||||
|
settings.device.originCenter = ui.deviceOrigin.checked || ui.deviceRound.checked;
|
||||||
|
settings.device.fwRetract = ui.fwRetract.checked;
|
||||||
|
// refresh vars and other ui elements
|
||||||
|
uc.refresh();
|
||||||
|
if (el === ui.camStockIndexed) {
|
||||||
|
api.view.set_arrange();
|
||||||
|
}
|
||||||
|
api.conf.update();
|
||||||
|
DOC.activeElement.blur();
|
||||||
|
api.event.emit("boolean.click");
|
||||||
|
api.devices.update_laser_state();
|
||||||
|
}
|
||||||
|
|
||||||
|
api.event.on('click.boolean', onBooleanClick);
|
||||||
|
|
||||||
|
function onButtonClick(ev) {
|
||||||
|
let target = ev.target;
|
||||||
|
while (target && target.tagName !== 'BUTTON') {
|
||||||
|
target = target.parentNode;
|
||||||
|
}
|
||||||
|
api.event.emit("button.click", target);
|
||||||
|
}
|
||||||
|
|
||||||
|
api.event.on('click.button', onButtonClick);
|
||||||
|
|
||||||
|
// function keys(o) {
|
||||||
|
// let key, list = [];
|
||||||
|
// for (key in o) { if (o.hasOwnProperty(key)) list.push(key) }
|
||||||
|
// return list.sort();
|
||||||
|
// }
|
||||||
|
|
||||||
|
function isBelt() {
|
||||||
|
return api.device.isBelt();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNotBelt() {
|
||||||
|
return !isBelt();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onResize() {
|
||||||
|
if (WIN.innerHeight < 800) {
|
||||||
|
ui.modalBox.classList.add('mh85');
|
||||||
|
} else {
|
||||||
|
ui.modalBox.classList.remove('mh85');
|
||||||
|
}
|
||||||
|
api.view.update_slider();
|
||||||
|
}
|
||||||
|
|
||||||
|
// MAIN UI INITIALIZATION FUNCTION
|
||||||
|
export function init_input() {
|
||||||
|
let { event, conf, view, show } = api,
|
||||||
|
{ newBlank, newButton, newBoolean, newGroup, newInput } = uc,
|
||||||
|
{ newSelect, newRow, newGCode, newDiv, toInt, toFloat } = uc;
|
||||||
|
|
||||||
|
event.emit('init.one');
|
||||||
|
|
||||||
|
// restore kiri-init vars
|
||||||
|
let inits = parseInt(sdb.getItem('kiri-init') || stats.get('init') || 0) + 1;
|
||||||
|
|
||||||
|
// update version and init count
|
||||||
|
sdb.setItem('kiri-init', inits);
|
||||||
|
stats.set('init', inits);
|
||||||
|
stats.set('kiri', version);
|
||||||
|
|
||||||
|
// restore settings from last saved session
|
||||||
|
conf.restore();
|
||||||
|
|
||||||
|
let container = $('container'),
|
||||||
|
gcode = $('dev-gcode'),
|
||||||
|
tracker = $('tracker'),
|
||||||
|
controller = settings().controller;
|
||||||
|
|
||||||
|
// window resize handlers
|
||||||
|
WIN.addEventListener("resize", onResize);
|
||||||
|
event.on('resize', onResize);
|
||||||
|
|
||||||
|
// configure moto.space
|
||||||
|
space.sky.showGrid(false);
|
||||||
|
space.sky.setColor(controller.dark ? 0 : 0xffffff);
|
||||||
|
space.setAntiAlias(controller.antiAlias);
|
||||||
|
space.init(container, (delta) => {
|
||||||
|
const { lo, hi, max } = slider.getRange();
|
||||||
|
if (max === 0 || !delta) return;
|
||||||
|
if (controller.reverseZoom) delta = -delta;
|
||||||
|
let same = hi === lo;
|
||||||
|
let track = lo > 0;
|
||||||
|
let newHi = hi;
|
||||||
|
let newLo = lo;
|
||||||
|
if (delta > 0) {
|
||||||
|
newHi = Math.max(same ? 0 : lo, hi - 1);
|
||||||
|
if (track) {
|
||||||
|
newLo = Math.max(0, lo - 1);
|
||||||
|
}
|
||||||
|
} else if (delta < 0) {
|
||||||
|
newHi = Math.min(max, hi + 1);
|
||||||
|
if (track) {
|
||||||
|
newLo = Math.min(newHi, lo + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (same) {
|
||||||
|
newLo = newHi;
|
||||||
|
}
|
||||||
|
slider.setRange(newLo, newHi);
|
||||||
|
view.update_slider();
|
||||||
|
show.slices();
|
||||||
|
}, controller.ortho);
|
||||||
|
space.platform.onMove(conf.save);
|
||||||
|
space.platform.setRound(true);
|
||||||
|
space.useDefaultKeys(api.feature.on_key === undefined || api.feature.on_key_defaults);
|
||||||
|
preferences.updateDrawer();
|
||||||
|
|
||||||
|
// api augmentation with local functions
|
||||||
|
api.device.export = settingsOps.export_device;
|
||||||
|
|
||||||
|
Object.assign(ui, {
|
||||||
|
tracker: tracker,
|
||||||
|
container: container,
|
||||||
|
|
||||||
|
// static html --> ui bindings
|
||||||
|
...bind(),
|
||||||
|
|
||||||
|
/** Device Browser / Editor */
|
||||||
|
|
||||||
|
_____: newDiv({ class: "f-col t-body t-inset", addto: $('dev-config'), set:true }),
|
||||||
|
device: newGroup(LANG.dv_gr_dev, null, {group:"ddev", inline, class:"noshow"}),
|
||||||
|
|
||||||
|
_____: newGroup("workspace", null, {group:"dext", inline}),
|
||||||
|
bedWidth: newInput('X (width)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:settingsOps.update_device}),
|
||||||
|
bedDepth: newInput('Y (depth)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:settingsOps.update_device}),
|
||||||
|
maxHeight: newInput('Z (height)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:settingsOps.update_device}),
|
||||||
|
resolutionX: newInput(LANG.dv_rezx_s, {title:LANG.dv_rezx_l, convert:toInt, size:6, modes:SLA}),
|
||||||
|
resolutionY: newInput(LANG.dv_rezy_s, {title:LANG.dv_rezy_l, convert:toInt, size:6, modes:SLA}),
|
||||||
|
_____: newDiv({ class: "f-col t-body t-inset", addto: $('dev-config'), set:true, modes:NO_WEDM }),
|
||||||
|
_____: newGroup("firmware", null, {group:"dext", inline, modes:NO_WEDM}),
|
||||||
|
fwRetract: newBoolean(LANG.dv_retr_s, onBooleanClick, {title:LANG.dv_retr_l, modes:FDM, trigger}),
|
||||||
|
deviceOrigin: newBoolean(LANG.dv_orgc_s, onBooleanClick, {title:LANG.dv_orgc_l, modes:FDM_LZN, show:() => !ui.deviceRound.checked}),
|
||||||
|
deviceRound: newBoolean(LANG.dv_bedc_s, onBooleanClick, {title:LANG.dv_bedc_l, modes:FDM, trigger, show:isNotBelt}),
|
||||||
|
deviceBelt: newBoolean(LANG.dv_belt_s, onBooleanClick, {title:LANG.dv_belt_l, modes:FDM, trigger, show:() => !ui.deviceRound.checked}),
|
||||||
|
separator: newBlank({class:"pop-sep", modes:FDM, driven}),
|
||||||
|
spindleMax: newInput(LANG.dv_spmx_s, {title:LANG.dv_spmx_l, convert:toInt, size:5, modes:CAM, trigger}),
|
||||||
|
deviceZMax: newInput(LANG.dv_zmax_s, {title:LANG.dv_zmax_l, convert:toInt, size:5, modes:FDM}),
|
||||||
|
gcodeTime: newInput(LANG.dv_time_s, {title:LANG.dv_time_l, convert:toFloat, size:5, modes:FDM}),
|
||||||
|
_____: newDiv({ class: "f-col t-body t-inset", addto: $('dev-config'), set:true, modes:FDM }),
|
||||||
|
extruder: newGroup(LANG.dv_gr_ext, null, {group:"dext", inline}),
|
||||||
|
extFilament: newInput(LANG.dv_fila_s, {title:LANG.dv_fila_l, convert:toFloat, modes:FDM}),
|
||||||
|
extNozzle: newInput(LANG.dv_nozl_s, {title:LANG.dv_nozl_l, convert:toFloat, modes:FDM}),
|
||||||
|
extOffsetX: newInput(LANG.dv_exox_s, {title:LANG.dv_exox_l, convert:toFloat, modes:FDM}),
|
||||||
|
extOffsetY: newInput(LANG.dv_exoy_s, {title:LANG.dv_exoy_l, convert:toFloat, modes:FDM}),
|
||||||
|
extPad: newBlank({class:"grow", modes:FDM}),
|
||||||
|
separator: newBlank({class:"pop-sep", modes:FDM, driven}),
|
||||||
|
extActions: newRow([
|
||||||
|
ui.extPrev = newButton(undefined, undefined, {icon:'<i class="fas fa-less-than"></i>'}),
|
||||||
|
ui.extAdd = newButton(undefined, undefined, {icon:'<i class="fas fa-plus"></i>'}),
|
||||||
|
ui.extDel = newButton(undefined, undefined, {icon:'<i class="fas fa-minus"></i>'}),
|
||||||
|
ui.extNext = newButton(undefined, undefined, {icon:'<i class="fas fa-greater-than"></i>'})
|
||||||
|
], {class:"dev-buttons ext-buttons var-row", modes:FDM}),
|
||||||
|
_____: newDiv({ class: "f-col t-body t-inset", addto: $('dev-config'), set:true, modes:CAM_LZR }),
|
||||||
|
_____: newGroup(LANG.dv_gr_out, null, {group:"dgco", inline}),
|
||||||
|
gcodeStrip: newBoolean(LANG.dv_strc_s, onBooleanClick, {title:LANG.dv_strc_l, modes:CAM}),
|
||||||
|
gcodeSpace: newBoolean(LANG.dv_tksp_s, onBooleanClick, {title:LANG.dv_tksp_l, modes:CAM_LZR}),
|
||||||
|
laserMaxPower: newInput(LANG.ou_maxp_s, {title:LANG.ou_maxp_l, modes:LASER, size:7, text:true}),
|
||||||
|
useLaser: newBoolean(LANG.dv_lazr_s, onBooleanClick, {title:LANG.dv_lazr_l, modes:CAM}),
|
||||||
|
useIndexed: newBoolean(LANG.dv_4tha_s, onBooleanClick, {title:LANG.dv_4tha_l, modes:CAM}),
|
||||||
|
gcodeFExt: newInput(LANG.dv_fext_s, {title:LANG.dv_fext_l, modes:CAM_LZR, size:7, text:true}),
|
||||||
|
gcodeEd: newGroup(LANG.dv_gr_gco, $('dg'), {group:"dgcp", inline, modes:GCODE}),
|
||||||
|
gcodeMacros: newRow([
|
||||||
|
(ui.gcodePre = newGCode(LANG.dv_head_s, {title:LANG.dv_head_l, modes:GCODE, area:gcode})).button,
|
||||||
|
(ui.gcodePost = newGCode(LANG.dv_foot_s, {title:LANG.dv_foot_l, modes:GCODE, area:gcode})).button,
|
||||||
|
(ui.gcodeLayer = newGCode(LANG.dv_layr_s, {title:LANG.dv_layr_l, modes:FDM, area:gcode})).button,
|
||||||
|
(ui.gcodeTrack = newGCode(LANG.dv_prog_s, {title:LANG.dv_prog_l, modes:FDM, area:gcode})).button,
|
||||||
|
(ui.gcodeFan = newGCode(LANG.dv_fanp_s, {title:LANG.dv_fanp_l, modes:FDM, area:gcode})).button,
|
||||||
|
(ui.gcodeFeature = newGCode(LANG.dv_feat_s, {title:LANG.dv_feat_l, modes:FDM, area:gcode})).button,
|
||||||
|
(ui.gcodeLaserOn = newGCode(LANG.dv_lzon_s, {title:LANG.dv_lzon_l, modes:LASER, area:gcode})).button,
|
||||||
|
(ui.gcodeLaserOff = newGCode(LANG.dv_lzof_s, {title:LANG.dv_lzof_l, modes:LASER, area:gcode})).button,
|
||||||
|
(ui.gcodeWaterOn = newGCode(LANG.dv_waon_s, {title:LANG.dv_waon_l, modes:WJET, area:gcode})).button,
|
||||||
|
(ui.gcodeWaterOff = newGCode(LANG.dv_waof_s, {title:LANG.dv_waof_l, modes:WJET, area:gcode})).button,
|
||||||
|
(ui.gcodeKnifeDn = newGCode(LANG.dv_dkon_s, {title:LANG.dv_dkon_l, modes:DRAG, area:gcode})).button,
|
||||||
|
(ui.gcodeKnifeUp = newGCode(LANG.dv_dkof_s, {title:LANG.dv_dkof_l, modes:DRAG, area:gcode})).button,
|
||||||
|
(ui.gcodeChange = newGCode(LANG.dv_tool_s, {title:LANG.dv_tool_l, modes:FDM_CAM, area:gcode})).button,
|
||||||
|
(ui.gcodeDwell = newGCode(LANG.dv_dwll_s, {title:LANG.dv_dwll_l, modes:CAM, area:gcode})).button,
|
||||||
|
(ui.gcodeSpindle = newGCode(LANG.dv_sspd_s, {title:LANG.dv_sspd_l, modes:CAM, area:gcode, show:() => ui.spindleMax.value > 0})).button,
|
||||||
|
(ui.gcodeResetA = newGCode(LANG.dv_resa_s, {title:LANG.dv_resa_l, modes:CAM, area:gcode, show:() => ui.useIndexed.checked})).button
|
||||||
|
], {class:"ext-buttons f-row gcode-macros"}),
|
||||||
|
|
||||||
|
/** Preferences Menu */
|
||||||
|
|
||||||
|
_____: newGroup(LANG.op_menu, $('prefs-gen1'), {inline}),
|
||||||
|
antiAlias: newBoolean(LANG.op_anta_s, preferences.booleanSave, {title:LANG.op_anta_l}),
|
||||||
|
reverseZoom: newBoolean(LANG.op_invr_s, preferences.booleanSave, {title:LANG.op_invr_l}),
|
||||||
|
ortho: newBoolean(LANG.op_orth_s, preferences.booleanSave, {title:LANG.op_orth_l}),
|
||||||
|
dark: newBoolean(LANG.op_dark_s, preferences.booleanSave, {title:LANG.op_dark_l}),
|
||||||
|
drawer: newBoolean('slide out', preferences.booleanSave, {title:'slide out settings drawer'}),
|
||||||
|
scrolls: newBoolean('scrollbars', preferences.booleanSave, {title:'show scrollbars'}),
|
||||||
|
devel: newBoolean(LANG.op_devl_s, preferences.booleanSave, {title:LANG.op_devl_l}),
|
||||||
|
_____: newGroup(LANG.op_disp, $('prefs-gen2'), {inline}),
|
||||||
|
showOrigin: newBoolean(LANG.op_shor_s, preferences.booleanSave, {title:LANG.op_shor_l}),
|
||||||
|
showRulers: newBoolean(LANG.op_shru_s, preferences.booleanSave, {title:LANG.op_shru_l}),
|
||||||
|
showSpeeds: newBoolean(LANG.op_sped_s, preferences.speedSave, {title:LANG.op_sped_l}),
|
||||||
|
shiny: newBoolean(LANG.op_shny_s, preferences.booleanSave, {title:LANG.op_shny_l, modes:FDM}),
|
||||||
|
lineType: newSelect(LANG.op_line_s, {title: LANG.op_line_l, action: preferences.lineTypeSave, modes:FDM}, "linetype"),
|
||||||
|
manifold: newBoolean(LANG.op_mani_s, preferences.booleanSave, {title: LANG.op_mani_l, modes:CAM}, "manifold"),
|
||||||
|
animesh: newSelect(LANG.op_anim_s, {title: LANG.op_anim_l, action: preferences.aniMeshSave, modes:CAM}, "animesh"),
|
||||||
|
units: newSelect(LANG.op_unit_s, {title: LANG.op_unit_l, action: preferences.unitsSave, modes:CAM}, "units"),
|
||||||
|
edgeangle: newInput(LANG.op_spoa_s, {title:LANG.op_spoa_l, convert:toFloat, size:3}),
|
||||||
|
_____: newGroup(LANG.lo_menu, $('prefs-lay'), {inline}),
|
||||||
|
autoSave: newBoolean(LANG.op_save_s, preferences.booleanSave, {title:LANG.op_save_l}),
|
||||||
|
autoLayout: newBoolean(LANG.op_auto_s, preferences.booleanSave, {title:LANG.op_auto_l}),
|
||||||
|
freeLayout: newBoolean(LANG.op_free_s, preferences.booleanSave, {title:LANG.op_free_l}),
|
||||||
|
spaceRandoX: newBoolean(LANG.op_spcx_s, preferences.booleanSave, {title:LANG.op_spcx_l, show:isBelt}),
|
||||||
|
spaceLayout: newInput(LANG.op_spcr_s, {title:LANG.op_spcr_l, convert:toFloat, size:3, units}),
|
||||||
|
_____: newGroup(LANG.xp_menu, $('prefs-xpo'), {inline: true}),
|
||||||
|
exportPreview: newBoolean(LANG.op_excp_s, preferences.booleanSave, {title:LANG.op_excp_l}),
|
||||||
|
exportOcto: newBoolean(LANG.op_exop_s, preferences.booleanSave, {title:LANG.op_exop_l}),
|
||||||
|
exportThumb: newBoolean(LANG.op_exth_s, preferences.booleanSave, {title:LANG.op_exth_l, modes:FDM}),
|
||||||
|
_____: newGroup(LANG.pt_menu, $('prefs-prt'), {inline}),
|
||||||
|
detail: newSelect(LANG.pt_qual_s, {title: LANG.pt_qual_l, action: preferences.detailSave}, "detail"),
|
||||||
|
healMesh: newBoolean(LANG.pt_heal_s, preferences.booleanSave, {title: LANG.pt_heal_l}),
|
||||||
|
// threaded: newBoolean(LANG.pt_thrd_s, preferences.booleanSave, {title: LANG.pt_thrd_l, modes:THREED}),
|
||||||
|
assembly: newBoolean(LANG.pt_assy_s, preferences.booleanSave, {title: LANG.pt_assy_l, modes:THREED}),
|
||||||
|
webGPU: newBoolean(LANG.pt_wgpu_s, preferences.booleanSave, {title: LANG.pt_wgpu_l, modes:THREED}),
|
||||||
|
|
||||||
|
prefadd: uc.checkpoint($('prefs-add')),
|
||||||
|
|
||||||
|
/** FDM Settings */
|
||||||
|
...menuFDM(),
|
||||||
|
|
||||||
|
/** CAM Settings */
|
||||||
|
...menuCAM(),
|
||||||
|
|
||||||
|
/** LASER/DRAG/WJET/WEDM cut tool Settings */
|
||||||
|
...menuLaser(),
|
||||||
|
|
||||||
|
/** SLA SETTINGS */
|
||||||
|
...menuSLA(),
|
||||||
|
|
||||||
|
layers: uc.setGroup($("layers")),
|
||||||
|
});
|
||||||
|
|
||||||
|
// run client mode initializations right after UI is bound
|
||||||
|
// MUST run after main ui menu inits since they rely on passed state
|
||||||
|
// that createPopOps disrupts (should be refactored)
|
||||||
|
initCAM();
|
||||||
|
initDRAG();
|
||||||
|
initFDM();
|
||||||
|
initLaser();
|
||||||
|
initSLA();
|
||||||
|
initWEDM();
|
||||||
|
initWJET();
|
||||||
|
|
||||||
|
// override old style settings two-button menu
|
||||||
|
ui.settingsSave.onclick = () => {
|
||||||
|
settingsOps.settings_save(undefined, ui.settingsName.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// initialize and expose modal to API
|
||||||
|
api.modal = modal.init({
|
||||||
|
ui: {
|
||||||
|
modal: ui.modal,
|
||||||
|
modals: ui.modals
|
||||||
|
},
|
||||||
|
onShow: (which) => {
|
||||||
|
api.event.emit('modal.show', which);
|
||||||
|
},
|
||||||
|
onHide: () => {
|
||||||
|
api.event.emit('modal.hide');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// initialize and expose slider to API
|
||||||
|
api.slider = slider.init({
|
||||||
|
ui,
|
||||||
|
tracker: tracker,
|
||||||
|
mobile: space.info.mob,
|
||||||
|
onLayerChange: (hi, lo) => {
|
||||||
|
// Visualization is handled by the code that calls setRange()
|
||||||
|
// Don't call api.show.layer here as it creates circular dependency
|
||||||
|
},
|
||||||
|
onStackUpdate: (lo, hi) => {
|
||||||
|
STACKS.setRange(lo, hi);
|
||||||
|
},
|
||||||
|
onSceneUpdate: () => {
|
||||||
|
space.scene.active();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// initialize and expose keyboard to API
|
||||||
|
api.keyboard = keyboard.init({
|
||||||
|
api,
|
||||||
|
platform,
|
||||||
|
selection,
|
||||||
|
slider,
|
||||||
|
space,
|
||||||
|
catalog,
|
||||||
|
sdb,
|
||||||
|
uc,
|
||||||
|
setCtrl: set_ctrl,
|
||||||
|
VIEWS,
|
||||||
|
DOC,
|
||||||
|
WIN,
|
||||||
|
rotateInputSelection: input_rotate,
|
||||||
|
settingsLoad: settingsOps.settings_load
|
||||||
|
});
|
||||||
|
|
||||||
|
// add mobile class to body if needed
|
||||||
|
if (space.info.mob) {
|
||||||
|
DOC.body.classList.add('mobile');
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.load.onchange = (event) => {
|
||||||
|
api.platform.load_files(event.target.files);
|
||||||
|
ui.load.value = ''; // reset so you can re-import the same filee
|
||||||
|
};
|
||||||
|
|
||||||
|
// store layer preferences
|
||||||
|
api.event.on('stack.show', (label) => {
|
||||||
|
let mode = api.mode.get();
|
||||||
|
let view = api.view.get();
|
||||||
|
api.conf.get().labels[`${mode}-${view}-${label}`] = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
api.event.on('stack.hide', (label) => {
|
||||||
|
let mode = api.mode.get();
|
||||||
|
let view = api.view.get();
|
||||||
|
api.conf.get().labels[`${mode}-${view}-${label}`] = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// bind menu language choices
|
||||||
|
let lang_map = [
|
||||||
|
[ 'lset-en', 'en-us' ],
|
||||||
|
[ 'lset-da', 'da-dk' ],
|
||||||
|
[ 'lset-de', 'de-de' ],
|
||||||
|
[ 'lset-fr', 'fr-fr' ],
|
||||||
|
[ 'lset-pl', 'pl-pl' ],
|
||||||
|
[ 'lset-pt', 'pt-pt' ],
|
||||||
|
[ 'lset-es', 'es-es' ],
|
||||||
|
[ 'lset-zh', 'zh' ],
|
||||||
|
]
|
||||||
|
for (let [ btn, lang ] of lang_map) {
|
||||||
|
$(btn).onclick = () => {
|
||||||
|
sdb.setItem('kiri-lang', lang);
|
||||||
|
api.space.reload();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
space.event.addHandlers(self, [
|
||||||
|
'dragover', fileOps.dragOverHandler,
|
||||||
|
'dragleave', fileOps.dragLeave,
|
||||||
|
'drop', fileOps.dropHandler
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Setup selection transformation bindings
|
||||||
|
input_binding(ui);
|
||||||
|
|
||||||
|
$('lab-axis').onclick = () => {
|
||||||
|
ui.lock.X.checked =
|
||||||
|
ui.lock.Y.checked =
|
||||||
|
ui.lock.Z.checked = !(
|
||||||
|
ui.lock.X.checked ||
|
||||||
|
ui.lock.Y.checked ||
|
||||||
|
ui.lock.Z.checked
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
$('scale-reset').onclick = $('lab-scale').onclick = () => {
|
||||||
|
selection.scale(1 / ui.scale.X.was, 1 / ui.scale.Y.was, 1 / ui.scale.Z.was);
|
||||||
|
ui.scale.X.value = ui.scale.Y.value = ui.scale.Z.value =
|
||||||
|
ui.scale.X.was = ui.scale.Y.was = ui.scale.Z.was = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
$('app-xpnd').onclick = () => {
|
||||||
|
try {
|
||||||
|
DOC.body.requestFullscreen();
|
||||||
|
} catch (e) {
|
||||||
|
event.emit('resize');
|
||||||
|
space.event.onResize();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!DOC.body.requestFullscreen) {
|
||||||
|
$('app-xpnd').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
uc.onBlur([
|
||||||
|
ui.toolName,
|
||||||
|
ui.toolNum,
|
||||||
|
ui.toolFluteDiam,
|
||||||
|
ui.toolFluteLen,
|
||||||
|
ui.toolShaftDiam,
|
||||||
|
ui.toolShaftLen,
|
||||||
|
ui.toolTaperTip,
|
||||||
|
], updateTool);
|
||||||
|
|
||||||
|
ui.toolMetric.onclick = updateTool;
|
||||||
|
ui.toolType.onchange = updateTool;
|
||||||
|
// default show gcode pre
|
||||||
|
ui.gcodePre.button.click();
|
||||||
|
|
||||||
|
api.platform.update_size();
|
||||||
|
|
||||||
|
// initialize interaction controller
|
||||||
|
interact.init({
|
||||||
|
api,
|
||||||
|
space,
|
||||||
|
platform,
|
||||||
|
selection,
|
||||||
|
ui,
|
||||||
|
settings,
|
||||||
|
VIEWS,
|
||||||
|
onHover: (data) => {
|
||||||
|
api.event.emit('mouse.hover', data);
|
||||||
|
},
|
||||||
|
onHoverDown: (data) => {
|
||||||
|
api.event.emit('mouse.hover.down', data);
|
||||||
|
},
|
||||||
|
onHoverUp: (data) => {
|
||||||
|
api.event.emit('mouse.hover.up', data);
|
||||||
|
},
|
||||||
|
onDragDone: (offset) => {
|
||||||
|
api.event.emit('mouse.drag.done', offset);
|
||||||
|
},
|
||||||
|
onSelectionDrag: (delta) => {
|
||||||
|
api.event.emit('selection.drag', delta);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// expose interact to API
|
||||||
|
api.interact = interact;
|
||||||
|
|
||||||
|
// prevent safari from exiting full screen mode
|
||||||
|
DOC.onkeydown = function (evt) {
|
||||||
|
if (evt.code == 27) evt.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
// block standard browser context menu
|
||||||
|
DOC.oncontextmenu = (event) => {
|
||||||
|
let et = event.target;
|
||||||
|
if (et.tagName === 'CANVAS' || et.id === 'context-menu' || et.classList.contains('draggable')) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
api.space.restore(() => checkSeed(resolve));
|
||||||
|
});
|
||||||
|
};
|
||||||
69
src/kiri/app/init/lang.js
Normal file
69
src/kiri/app/init/lang.js
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
import { api } from '../api.js';
|
||||||
|
import { local as sdb } from '../../../data/local.js';
|
||||||
|
|
||||||
|
const { SETUP } = api.const;
|
||||||
|
const DOC = self.document;
|
||||||
|
const stats = api.stats;
|
||||||
|
|
||||||
|
// update static html elements with language overrides
|
||||||
|
function lang_rewrite() {
|
||||||
|
const LANG = api.language.current;
|
||||||
|
// lk attribute causes inner text to be replaced with lang value
|
||||||
|
for (let el of [...DOC.querySelectorAll("[lk]")]) {
|
||||||
|
let key = el.getAttribute('lk');
|
||||||
|
let val = LANG[key];
|
||||||
|
if (val) {
|
||||||
|
el.innerText = val;
|
||||||
|
} else {
|
||||||
|
console.log({missing_ln: key});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// lkt attribute causes a title attribute to be set from lang value
|
||||||
|
for (let el of [...DOC.querySelectorAll("[lkt]")]) {
|
||||||
|
let key = el.getAttribute('lkt');
|
||||||
|
let val = LANG[key];
|
||||||
|
if (val) {
|
||||||
|
el.setAttribute("title", val);
|
||||||
|
} else {
|
||||||
|
console.log({missing_ln: key});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// init lang must happen before all other init functions
|
||||||
|
export function init_lang() {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
// if a language needs to load, the script is injected and loaded
|
||||||
|
// first. once this loads, or doesn't, the initialization begins
|
||||||
|
let lang = SETUP.ln ? SETUP.ln[0] : sdb.getItem('kiri-lang') || api.language.get();
|
||||||
|
api.event.emit('init.lang', lang);
|
||||||
|
// inject language script if not english
|
||||||
|
if (lang && lang !== 'en' && lang !== 'en-us') {
|
||||||
|
let map = api.language.map(lang);
|
||||||
|
let scr = DOC.createElement('script');
|
||||||
|
// scr.setAttribute('defer',true);
|
||||||
|
scr.setAttribute('src',`/kiri/lang/${map}.js`);
|
||||||
|
(DOC.body || DOC.head).appendChild(scr);
|
||||||
|
stats.set('ll',lang);
|
||||||
|
scr.onload = function() {
|
||||||
|
api.language.set(map);
|
||||||
|
lang_rewrite();
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
scr.onerror = function(err) {
|
||||||
|
console.log({language_load_error: err, lang})
|
||||||
|
api.language.set();
|
||||||
|
lang_rewrite();
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// set to browser default which will be overridden
|
||||||
|
// by any future script loads (above)
|
||||||
|
api.language.set();
|
||||||
|
lang_rewrite();
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
324
src/kiri/app/init/sync.js
Normal file
324
src/kiri/app/init/sync.js
Normal file
|
|
@ -0,0 +1,324 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
import { $ } from '../../../moto/webui.js';
|
||||||
|
import { api } from '../api.js';
|
||||||
|
import { beta, version } from '../../../moto/license.js';
|
||||||
|
import { fileOps } from '../file-ops.js';
|
||||||
|
import { local as sdb } from '../../../data/local.js';
|
||||||
|
import { preferences } from '../preferences.js';
|
||||||
|
import { settings as set_ctrl } from '../conf/manager.js';
|
||||||
|
import { settingsOps } from '../conf/settings.js';
|
||||||
|
import { space } from '../../../moto/space.js';
|
||||||
|
import { VIEWS } from '../consts.js';
|
||||||
|
import * as view_tools from '../face-tool.js';
|
||||||
|
|
||||||
|
const { SETUP, LOCAL } = api.const;
|
||||||
|
const { catalog, client, platform, selection, stats, ui } = api;
|
||||||
|
|
||||||
|
const DOC = self.document;
|
||||||
|
const WIN = self.window;
|
||||||
|
const LANG = api.language.current;
|
||||||
|
const STARTMODE = SETUP.sm && SETUP.sm.length === 1 ? SETUP.sm[0] : null;
|
||||||
|
|
||||||
|
// SECOND STAGE INIT AFTER UI RESTORED
|
||||||
|
export async function init_sync() {
|
||||||
|
const proto = location.protocol;
|
||||||
|
|
||||||
|
api.event.emit('init.two');
|
||||||
|
|
||||||
|
// load script extensions
|
||||||
|
if (SETUP.s) SETUP.s.forEach(function(lib) {
|
||||||
|
let scr = DOC.createElement('script');
|
||||||
|
scr.setAttribute('async', true);
|
||||||
|
scr.setAttribute('defer', true);
|
||||||
|
scr.setAttribute('src',`/code/${lib}.js`);
|
||||||
|
DOC.body.appendChild(scr);
|
||||||
|
stats.add('load_'+lib);
|
||||||
|
api.event.emit('load.lib', lib);
|
||||||
|
});
|
||||||
|
|
||||||
|
// override stored settings
|
||||||
|
if (SETUP.v) SETUP.v.forEach(function(kv) {
|
||||||
|
kv = kv.split('=');
|
||||||
|
sdb.setItem(kv[0],kv[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// import octoprint settings
|
||||||
|
if (SETUP.ophost) {
|
||||||
|
let ohost = api.const.OCTO = {
|
||||||
|
host: SETUP.ophost[0],
|
||||||
|
apik: SETUP.opkey ? SETUP.opkey[0] : ''
|
||||||
|
};
|
||||||
|
sdb['octo-host'] = ohost.host;
|
||||||
|
sdb['octo-apik'] = ohost.apik;
|
||||||
|
console.log({octoprint:ohost});
|
||||||
|
}
|
||||||
|
|
||||||
|
// One-time migration: Initialize model.opacity from existing ghosting state
|
||||||
|
if (api.local.getFloat('model.opacity') === null) {
|
||||||
|
const wireframeOpacity = api.local.getFloat('model.wireframe.opacity');
|
||||||
|
const wireframe = api.local.getBoolean('model.wireframe', false);
|
||||||
|
|
||||||
|
// If ghosting was enabled (wireframe off, opacity < 1), preserve it
|
||||||
|
if (!wireframe && wireframeOpacity !== null && wireframeOpacity < 1) {
|
||||||
|
api.local.set('model.opacity', wireframeOpacity);
|
||||||
|
} else {
|
||||||
|
api.local.set('model.opacity', 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// load workspace from url
|
||||||
|
if (SETUP.wrk) {
|
||||||
|
set_ctrl.import_url(`${proto}//${SETUP.wrk[0]}`, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// load an object from url
|
||||||
|
if (SETUP.load) {
|
||||||
|
console.log({load:SETUP});
|
||||||
|
api.platform.load_url(`${proto}//${SETUP.load[0]}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// bind this to UI so main can call it on settings import
|
||||||
|
ui.sync = ui_sync;
|
||||||
|
ui_sync();
|
||||||
|
|
||||||
|
// clear alerts as they build up
|
||||||
|
setInterval(api.event.alerts, 1000);
|
||||||
|
|
||||||
|
// add hide-alerts-on-alert-click
|
||||||
|
ui.alert.dialog.onclick = function() {
|
||||||
|
api.event.alerts(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!SETUP.s) console.log(`kiri | init main | ${version}`);
|
||||||
|
|
||||||
|
// send init-done event
|
||||||
|
api.event.emit('init-done', stats);
|
||||||
|
|
||||||
|
// show gdpr if it's never been seen and we're not iframed
|
||||||
|
const isLocal = LOCAL || WIN.location.host.split(':')[0] === 'localhost';
|
||||||
|
if (!sdb.gdpr && WIN.self === WIN.top && !SETUP.debug && !isLocal) {
|
||||||
|
$('gdpr').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
// warn of degraded functionality when SharedArrayBuffers are missing
|
||||||
|
if (api.feature.work_alerts && !window.SharedArrayBuffer) {
|
||||||
|
api.alerts.show("The security context of this", 10);
|
||||||
|
api.alerts.show("Window blocks important functionality.", 10);
|
||||||
|
api.alerts.show("Try a Chromium-base Browser instead", 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add keyboard focus handler (must use for iframes)
|
||||||
|
WIN.addEventListener('load', function () {
|
||||||
|
WIN.focus();
|
||||||
|
DOC.body.addEventListener('click', function() {
|
||||||
|
WIN.focus();
|
||||||
|
},false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup navigation button bindings
|
||||||
|
setup_keybd_nav();
|
||||||
|
|
||||||
|
// show topline separator when iframed
|
||||||
|
try { if (WIN.self !== WIN.top) $('top-sep').style.display = 'flex' } catch (e) { console.log(e) }
|
||||||
|
|
||||||
|
// warn users they are running a beta release
|
||||||
|
if (beta && beta > 0 && sdb.kiri_beta != beta) {
|
||||||
|
api.show.alert('<b style="color:red">caution:</b> beta code ahead', 10);
|
||||||
|
sdb.kiri_beta = beta;
|
||||||
|
}
|
||||||
|
|
||||||
|
// warn users they are using a development server
|
||||||
|
let devwarn = sdb.kiri_dev;
|
||||||
|
if (location.host === 'dev.grid.space' && devwarn !== version) {
|
||||||
|
api.alerts.show('this is a development server', 10);
|
||||||
|
api.alerts.show('use <a href="https://grid.space/kiri">grid.space</a> for production', 10);
|
||||||
|
sdb.kiri_dev = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
// hide url params but preserve version root (when present)
|
||||||
|
let wlp = WIN.location.pathname;
|
||||||
|
let kio = wlp.indexOf('/kiri/');
|
||||||
|
if (kio >= 0) {
|
||||||
|
history.replaceState({}, '', wlp.substring(0,kio + 6));
|
||||||
|
}
|
||||||
|
|
||||||
|
// lift curtain
|
||||||
|
$('curtain').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function ui_sync() {
|
||||||
|
const current = api.conf.get();
|
||||||
|
const control = current.controller;
|
||||||
|
|
||||||
|
if (!control.devel) {
|
||||||
|
// TODO: hide thin type 3 during development
|
||||||
|
api.const.LISTS.thin.length = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
platform.deselect();
|
||||||
|
catalog.addFileListener(fileOps.updateCatalog);
|
||||||
|
space.view.setZoom(control.reverseZoom, control.zoomSpeed);
|
||||||
|
space.platform.setGridZOff(undefined);
|
||||||
|
space.platform.setZOff(0.05);
|
||||||
|
space.view.setProjection(control.ortho ? 'orthographic' : 'perspective');
|
||||||
|
|
||||||
|
// restore UI state from settings
|
||||||
|
ui.antiAlias.checked = control.antiAlias;
|
||||||
|
ui.assembly.checked = control.assembly;
|
||||||
|
ui.autoLayout.checked = control.autoLayout;
|
||||||
|
ui.autoSave.checked = control.autoSave;
|
||||||
|
ui.devel.checked = control.devel;
|
||||||
|
ui.freeLayout.checked = control.freeLayout;
|
||||||
|
ui.healMesh.checked = control.healMesh;
|
||||||
|
ui.manifold.checked = control.manifold;
|
||||||
|
ui.ortho.checked = control.ortho;
|
||||||
|
ui.reverseZoom.checked = control.reverseZoom;
|
||||||
|
ui.showOrigin.checked = control.showOrigin;
|
||||||
|
ui.showRulers.checked = control.showRulers;
|
||||||
|
ui.showSpeeds.checked = control.showSpeeds;
|
||||||
|
ui.spaceRandoX.checked = control.spaceRandoX;
|
||||||
|
// ui.threaded.checked = setThreaded(control.threaded);
|
||||||
|
ui.webGPU.checked = control.webGPU;
|
||||||
|
|
||||||
|
preferences.setThreaded(true);
|
||||||
|
preferences.lineTypeSave();
|
||||||
|
preferences.detailSave();
|
||||||
|
api.visuals.update_stats();
|
||||||
|
|
||||||
|
// optional set-and-lock mode (hides mode menu)
|
||||||
|
let SETMODE = SETUP.mode ? SETUP.mode[0] : null;
|
||||||
|
|
||||||
|
// optional set-and-lock device (hides device menu)
|
||||||
|
let DEVNAME = SETUP.dev ? SETUP.dev[0] : null;
|
||||||
|
|
||||||
|
// setup default mode and enable mode locking, if set
|
||||||
|
api.mode.set(SETMODE || STARTMODE || current.mode, SETMODE);
|
||||||
|
|
||||||
|
// fill device list
|
||||||
|
api.devices.refresh();
|
||||||
|
|
||||||
|
// update ui fields from settings
|
||||||
|
api.conf.update_fields();
|
||||||
|
|
||||||
|
// default to ARRANGE view mode
|
||||||
|
api.view.set(VIEWS.ARRANGE);
|
||||||
|
|
||||||
|
// add ability to override (todo: restore?)
|
||||||
|
// api.show.controls(api.feature.controls);
|
||||||
|
|
||||||
|
// update everything dependent on the platform size
|
||||||
|
platform.update_size();
|
||||||
|
|
||||||
|
// load wasm if indicated
|
||||||
|
client.wasm(control.assembly === true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup_keybd_nav() {
|
||||||
|
// bind interface action elements
|
||||||
|
ui.acct.help.onclick = (ev) => { ev.stopPropagation(); api.help.show() };
|
||||||
|
ui.acct.don8.onclick = (ev) => { ev.stopPropagation(); api.modal.show('don8') };
|
||||||
|
ui.acct.mesh.onclick = (ev) => { ev.stopPropagation(); WIN.location = "/mesh" };
|
||||||
|
ui.acct.export.onclick = (ev) => { ev.stopPropagation(); settingsOps.export_profile() };
|
||||||
|
ui.acct.export.title = LANG.acct_xpo;
|
||||||
|
ui.func.slice.onclick = (ev) => { ev.stopPropagation(); api.function.slice() };
|
||||||
|
ui.func.preview.onclick = (ev) => { ev.stopPropagation(); api.function.print() };
|
||||||
|
ui.func.animate.onclick = (ev) => { ev.stopPropagation(); api.function.animate() };
|
||||||
|
ui.func.export.onclick = (ev) => { ev.stopPropagation(); api.function.export() };
|
||||||
|
// prevent modal input from propagating to parents
|
||||||
|
ui.modalBox.onclick = (ev) => { ev.stopPropagation() };
|
||||||
|
|
||||||
|
$('export-support-a').onclick = (ev) => { ev.stopPropagation(); api.modal.show('don8') };
|
||||||
|
$('mode-device').onclick = api.show.devices;
|
||||||
|
$('mode-profile').onclick = settingsOps.settings_load;
|
||||||
|
$('mode-fdm').onclick = () => api.mode.set('FDM');
|
||||||
|
$('mode-cam').onclick = () => api.mode.set('CAM');
|
||||||
|
$('mode-sla').onclick = () => api.mode.set('SLA');
|
||||||
|
$('mode-laser').onclick = () => api.mode.set('LASER');
|
||||||
|
$('mode-drag').onclick = () => api.mode.set('DRAG');
|
||||||
|
$('mode-wjet').onclick = () => api.mode.set('WJET');
|
||||||
|
$('mode-wedm').onclick = () => api.mode.set('WEDM');
|
||||||
|
$('set-device').onclick = (ev) => { ev.stopPropagation(); api.show.devices() };
|
||||||
|
$('set-profs').onclick = (ev) => { ev.stopPropagation(); api.conf.show() };
|
||||||
|
$('set-tools').onclick = (ev) => { ev.stopPropagation(); api.show.tools() };
|
||||||
|
$('set-prefs').onclick = (ev) => { ev.stopPropagation(); api.modal.show('prefs') };
|
||||||
|
$('file-new').onclick = (ev) => { ev.stopPropagation(); settingsOps.new_workspace() };
|
||||||
|
$('file-recent').onclick = () => { api.modal.show('files') };
|
||||||
|
$('file-import').onclick = (ev) => { api.event.import(ev); };
|
||||||
|
$('view-arrange').onclick = api.platform.layout;
|
||||||
|
$('view-top').onclick = space.view.top;
|
||||||
|
$('view-home').onclick = space.view.home;
|
||||||
|
$('view-front').onclick = space.view.front;
|
||||||
|
$('view-back').onclick = space.view.back;
|
||||||
|
$('view-left').onclick = space.view.left;
|
||||||
|
$('view-right').onclick = space.view.right;
|
||||||
|
$('unrotate').onclick = () => {
|
||||||
|
api.widgets.for(w => w.unrotate());
|
||||||
|
selection.update_info();
|
||||||
|
};
|
||||||
|
|
||||||
|
// attach button handlers to support targets
|
||||||
|
for (let btn of ["don8pt","don8gh","don8pp"]) {
|
||||||
|
$(btn).onclick = (ev) => {
|
||||||
|
window.open(ev.target.children[0].href);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// rotation buttons
|
||||||
|
let d = (Math.PI / 180);
|
||||||
|
$('rot_x_lt').onclick = () => { selection.rotate(-d * $('rot_x').value,0,0) };
|
||||||
|
$('rot_x_gt').onclick = () => { selection.rotate( d * $('rot_x').value,0,0) };
|
||||||
|
$('rot_y_lt').onclick = () => { selection.rotate(0,-d * $('rot_y').value,0) };
|
||||||
|
$('rot_y_gt').onclick = () => { selection.rotate(0, d * $('rot_y').value,0) };
|
||||||
|
$('rot_z_lt').onclick = () => { selection.rotate(0,0, d * $('rot_z').value) };
|
||||||
|
$('rot_z_gt').onclick = () => { selection.rotate(0,0,-d * $('rot_z').value) };
|
||||||
|
|
||||||
|
// rendering options
|
||||||
|
$('render-edges').onclick = () => { api.view.set_edges({ toggle: true }); api.conf.save() };
|
||||||
|
$('render-ghost').onclick = () => {
|
||||||
|
const opacity = api.view.is_arrange() ? 0.4 : 0.25;
|
||||||
|
api.view.set_wireframe(false);
|
||||||
|
api.visuals.set_opacity(opacity);
|
||||||
|
// Also save to old key for backwards compatibility
|
||||||
|
api.local.set('model.wireframe.opacity', opacity);
|
||||||
|
api.conf.save();
|
||||||
|
};
|
||||||
|
$('render-wire').onclick = () => {
|
||||||
|
api.view.set_wireframe(true, 0, api.space.is_dark() ? 0.25 : 0.5);
|
||||||
|
api.visuals.set_opacity(1.0);
|
||||||
|
api.conf.save();
|
||||||
|
};
|
||||||
|
$('render-solid').onclick = () => {
|
||||||
|
api.view.set_wireframe(false);
|
||||||
|
api.visuals.set_opacity(1.0);
|
||||||
|
// Also save to old key for backwards compatibility
|
||||||
|
api.local.set('model.wireframe.opacity', 1.0);
|
||||||
|
api.conf.save();
|
||||||
|
};
|
||||||
|
$('mesh-export-stl').onclick = () => { settingsOps.export_objects('stl') };
|
||||||
|
$('mesh-export-obj').onclick = () => { settingsOps.export_objects('obj') };
|
||||||
|
$('mesh-merge').onclick = selection.merge;
|
||||||
|
$('mesh-split').onclick = selection.isolateBodies;
|
||||||
|
$('context-duplicate').onclick = selection.duplicate;
|
||||||
|
$('context-mirror').onclick = selection.mirror;
|
||||||
|
$('context-layflat').onclick = view_tools.startLayFlat;
|
||||||
|
$('context-lefty').onclick = view_tools.startLeftAlign;
|
||||||
|
$('context-setfocus').onclick = () => {
|
||||||
|
view_tools.startFocus(ev => api.space.set_focus(undefined, ev.object.point));
|
||||||
|
};
|
||||||
|
$('context-contents').onclick = api.const.SPACE.view.fit;
|
||||||
|
$('view-fit').onclick = api.const.SPACE.view.fit;
|
||||||
|
$('wassup').onmouseover = () => { $('suppopp').classList.remove('hide') };
|
||||||
|
|
||||||
|
// enable modal hiding
|
||||||
|
$('mod-x').onclick = api.modal.hide;
|
||||||
|
|
||||||
|
// dismiss gdpr alert
|
||||||
|
$('gotit').onclick = () => {
|
||||||
|
$('gdpr').style.display = 'none';
|
||||||
|
sdb.gdpr = Date.now();
|
||||||
|
};
|
||||||
|
|
||||||
|
// add app name hover info
|
||||||
|
$('app-info').innerText = version;
|
||||||
|
}
|
||||||
|
|
@ -1,31 +1,62 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI component factory system.
|
||||||
|
* Creates form elements, dialogs, and manages UI state/visibility.
|
||||||
|
* Supports mode-specific visibility, unit conversion, and hierarchical grouping.
|
||||||
|
*/
|
||||||
|
|
||||||
import { $ } from '../../moto/webui.js';
|
import { $ } from '../../moto/webui.js';
|
||||||
import { api } from './api.js';
|
import { api } from './api.js';
|
||||||
|
|
||||||
let DOC = self.document,
|
let DOC = self.document,
|
||||||
|
/** Callback for input changes */
|
||||||
inputAction = null,
|
inputAction = null,
|
||||||
|
/** Previous addTo value for nesting */
|
||||||
lastAddTo = null,
|
lastAddTo = null,
|
||||||
|
/** Current group array */
|
||||||
lastGroup = null,
|
lastGroup = null,
|
||||||
|
/** Last created div container */
|
||||||
lastDiv = null,
|
lastDiv = null,
|
||||||
|
/** Current container to add elements to */
|
||||||
addTo = null,
|
addTo = null,
|
||||||
|
/** Alternative binding target for input actions */
|
||||||
bindTo = null,
|
bindTo = null,
|
||||||
|
/** Map of group name to array of elements */
|
||||||
groups = {},
|
groups = {},
|
||||||
|
/** Sticky state prevents auto-hide on blur */
|
||||||
groupSticky = false,
|
groupSticky = false,
|
||||||
|
/** Current group name */
|
||||||
groupName = undefined,
|
groupName = undefined,
|
||||||
heads = {}, // hideable group heads (clickable label)
|
/** Collapsible group headers (clickable label) */
|
||||||
hidden = {}, // hidden groups (by name)
|
heads = {},
|
||||||
|
/** Hidden groups by name */
|
||||||
|
hidden = {},
|
||||||
|
/** Elements with mode visibility rules */
|
||||||
hasModes = [],
|
hasModes = [],
|
||||||
|
/** Elements with unit conversion setters */
|
||||||
setters = [],
|
setters = [],
|
||||||
|
/** Last mode set for visibility filtering */
|
||||||
lastMode = null,
|
lastMode = null,
|
||||||
|
/** Last expert mode state */
|
||||||
lastExpert = true,
|
lastExpert = true,
|
||||||
|
/** Prefix for element IDs */
|
||||||
prefix = "tab",
|
prefix = "tab",
|
||||||
|
/** Unit scale multiplier for conversions */
|
||||||
units = 1,
|
units = 1,
|
||||||
|
/** Last changed input element */
|
||||||
lastChange = null,
|
lastChange = null,
|
||||||
|
/** Last clicked button */
|
||||||
lastBtn = null,
|
lastBtn = null,
|
||||||
|
/** Last clicked text element */
|
||||||
lastTxt = null,
|
lastTxt = null,
|
||||||
|
/** Last shown popup */
|
||||||
lastPop = null;
|
lastPop = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI component factory and utilities.
|
||||||
|
* Provides chainable builder pattern and component creation functions.
|
||||||
|
*/
|
||||||
export const UI = {
|
export const UI = {
|
||||||
prefix: function(pre) { prefix = pre; return UI },
|
prefix: function(pre) { prefix = pre; return UI },
|
||||||
inputAction: function(fn) { inputAction = fn; return UI },
|
inputAction: function(fn) { inputAction = fn; return UI },
|
||||||
|
|
@ -84,6 +115,10 @@ export const UI = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set hidden group map and refresh visibility.
|
||||||
|
* @param {object} map - Map of group name to hidden boolean
|
||||||
|
*/
|
||||||
function setHidden(map) {
|
function setHidden(map) {
|
||||||
hidden = map;
|
hidden = map;
|
||||||
refresh();
|
refresh();
|
||||||
|
|
@ -92,6 +127,11 @@ function setHidden(map) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach blur event listener to element(s).
|
||||||
|
* @param {HTMLElement|Array<HTMLElement>} obj - Element or array of elements
|
||||||
|
* @param {function} fn - Blur handler function
|
||||||
|
*/
|
||||||
function onBlur(obj, fn) {
|
function onBlur(obj, fn) {
|
||||||
if (Array.isArray(obj)) {
|
if (Array.isArray(obj)) {
|
||||||
for (let o of obj) onBlur(o, fn);
|
for (let o of obj) onBlur(o, fn);
|
||||||
|
|
@ -100,14 +140,34 @@ function onBlur(obj, fn) {
|
||||||
obj.addEventListener('blur', fn);
|
obj.addEventListener('blur', fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show alert dialog with OK button.
|
||||||
|
* @param {string} message - Alert message
|
||||||
|
* @returns {Promise<boolean>} Resolves when OK clicked
|
||||||
|
*/
|
||||||
function alert(message) {
|
function alert(message) {
|
||||||
return confirm(message, {ok:true});
|
return confirm(message, {ok:true});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show prompt dialog with text input.
|
||||||
|
* @param {string} message - Prompt message
|
||||||
|
* @param {string} value - Default input value
|
||||||
|
* @returns {Promise<string>} Resolves with entered text or undefined if cancelled
|
||||||
|
*/
|
||||||
function prompt(message, value) {
|
function prompt(message, value) {
|
||||||
return confirm(message, {ok:true, cancel:undefined}, value);
|
return confirm(message, {ok:true, cancel:undefined}, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show modal dialog with custom buttons and optional input.
|
||||||
|
* Blocks keyboard events while open.
|
||||||
|
* @param {string} message - Dialog message
|
||||||
|
* @param {object} buttons - Button labels mapped to return values (e.g., {yes: true, no: false})
|
||||||
|
* @param {string|Array<string>} [input] - Optional input field. Array creates textarea with lines.
|
||||||
|
* @param {object} [opt={}] - Options: {pre, post} for additional HTML content
|
||||||
|
* @returns {Promise} Resolves with button value or input value if provided
|
||||||
|
*/
|
||||||
function confirm(message, buttons, input, opt = {}) {
|
function confirm(message, buttons, input, opt = {}) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let { feature } = api;
|
let { feature } = api;
|
||||||
|
|
@ -148,9 +208,11 @@ function confirm(message, buttons, input, opt = {}) {
|
||||||
html.appendAll(opt.post);
|
html.appendAll(opt.post);
|
||||||
}
|
}
|
||||||
dialog.innerHTML = html.join('');
|
dialog.innerHTML = html.join('');
|
||||||
|
dialog.onclose = () => {
|
||||||
|
feature.on_key = onkey_save;
|
||||||
|
};
|
||||||
function done(value) {
|
function done(value) {
|
||||||
dialog.close();
|
dialog.close();
|
||||||
feature.on_key = onkey_save;
|
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
setTimeout(() => { resolve(value) }, 150);
|
setTimeout(() => { resolve(value) }, 150);
|
||||||
}
|
}
|
||||||
|
|
@ -307,6 +369,12 @@ function addCollapsableElement(parent, options = {}) {
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a value bounding function.
|
||||||
|
* @param {number} low - Minimum value
|
||||||
|
* @param {number} high - Maximum value
|
||||||
|
* @returns {function} Function that clamps value to [low, high] range
|
||||||
|
*/
|
||||||
function bound(low,high) {
|
function bound(low,high) {
|
||||||
return function(v) {
|
return function(v) {
|
||||||
if (isNaN(v)) return low;
|
if (isNaN(v)) return low;
|
||||||
|
|
@ -314,6 +382,11 @@ function bound(low,high) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert input value to integer.
|
||||||
|
* Bound to input element as `this`. Applies bounds and unit conversion if configured.
|
||||||
|
* @returns {number} Integer value
|
||||||
|
*/
|
||||||
function toInt() {
|
function toInt() {
|
||||||
let nv = this.value !== '' ? parseInt(this.value) : null;
|
let nv = this.value !== '' ? parseInt(this.value) : null;
|
||||||
if (isNaN(nv)) nv = 0;
|
if (isNaN(nv)) nv = 0;
|
||||||
|
|
@ -325,6 +398,11 @@ function toInt() {
|
||||||
return nv;
|
return nv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert input value to float.
|
||||||
|
* Bound to input element as `this`. Applies bounds and unit conversion if configured.
|
||||||
|
* @returns {number} Float value
|
||||||
|
*/
|
||||||
function toFloat() {
|
function toFloat() {
|
||||||
let nv = this.value !== '' ? parseFloat(this.value) : null;
|
let nv = this.value !== '' ? parseFloat(this.value) : null;
|
||||||
if (nv !== null && this.bound) nv = this.bound(nv);
|
if (nv !== null && this.bound) nv = this.bound(nv);
|
||||||
|
|
@ -335,6 +413,11 @@ function toFloat() {
|
||||||
return nv;
|
return nv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert comma-separated input to float array.
|
||||||
|
* Bound to input element as `this`. Applies bounds to each value.
|
||||||
|
* @returns {Array<number>} Float array
|
||||||
|
*/
|
||||||
function toFloatArray() {
|
function toFloatArray() {
|
||||||
let nv = this.value !== '' ? this.value.split(',').map(v => parseFloat(v)) : null;
|
let nv = this.value !== '' ? this.value.split(',').map(v => parseFloat(v)) : null;
|
||||||
console.log({ toFloatArray: nv });
|
console.log({ toFloatArray: nv });
|
||||||
|
|
@ -343,6 +426,11 @@ function toFloatArray() {
|
||||||
return nv;
|
return nv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert input value to degrees float (0-360).
|
||||||
|
* Bound to input element as `this`. Normalizes to 0-359.99 range.
|
||||||
|
* @returns {number} Degrees float
|
||||||
|
*/
|
||||||
function toDegsFloat(){
|
function toDegsFloat(){
|
||||||
let nv = this.value !== '' ? parseFloat(this.value) : null;
|
let nv = this.value !== '' ? parseFloat(this.value) : null;
|
||||||
if (nv !== null && this.bound) nv = this.bound(nv);
|
if (nv !== null && this.bound) nv = this.bound(nv);
|
||||||
|
|
@ -375,6 +463,12 @@ function newLabel(text, opt = {}) {
|
||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a read-only value display element.
|
||||||
|
* @param {number} [size=6] - Input size attribute
|
||||||
|
* @param {object} [opt={}] - Options: {class}
|
||||||
|
* @returns {HTMLInputElement} Read-only input element
|
||||||
|
*/
|
||||||
function newValue(size = 6, opt = {}) {
|
function newValue(size = 6, opt = {}) {
|
||||||
let value = DOC.createElement('input');
|
let value = DOC.createElement('input');
|
||||||
value.setAttribute("size", size);
|
value.setAttribute("size", size);
|
||||||
|
|
@ -452,6 +546,13 @@ function newDiv(opt = {}) {
|
||||||
return div;
|
return div;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a collapsible details/summary element.
|
||||||
|
* Changes addTo context to the details element until endExpand() called.
|
||||||
|
* @param {string} label - Summary label text
|
||||||
|
* @param {object} [opt={}] - Options: {class, open, modes, show, etc.}
|
||||||
|
* @returns {HTMLElement} Details element with collapse() method
|
||||||
|
*/
|
||||||
function newExpand(label, opt = {}) {
|
function newExpand(label, opt = {}) {
|
||||||
let div = DOC.createElement('details');
|
let div = DOC.createElement('details');
|
||||||
div.setAttribute('class', opt.class || 'f-col');
|
div.setAttribute('class', opt.class || 'f-col');
|
||||||
|
|
@ -467,6 +568,11 @@ function newExpand(label, opt = {}) {
|
||||||
div.removeAttribute('open');
|
div.removeAttribute('open');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (opt.hover) {
|
||||||
|
div.addEventListener('mouseenter', event => opt.hover('enter', event));
|
||||||
|
div.addEventListener('mouseleave', event => opt.hover('leave', event));
|
||||||
|
}
|
||||||
|
|
||||||
lastAddTo = addTo;
|
lastAddTo = addTo;
|
||||||
addTo.appendChild(div);
|
addTo.appendChild(div);
|
||||||
addTo = div;
|
addTo = div;
|
||||||
|
|
@ -474,6 +580,10 @@ function newExpand(label, opt = {}) {
|
||||||
return div;
|
return div;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* End expand context, restoring previous addTo.
|
||||||
|
* @returns {HTMLElement} Restored addTo element
|
||||||
|
*/
|
||||||
function endExpand() {
|
function endExpand() {
|
||||||
addTo = lastAddTo;
|
addTo = lastAddTo;
|
||||||
return addTo;
|
return addTo;
|
||||||
|
|
@ -612,6 +722,15 @@ function newText(label, options) {
|
||||||
return txt;
|
return txt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a labeled input field with automatic validation and unit conversion.
|
||||||
|
* Supports text input or textarea (if height > 1).
|
||||||
|
* Filters non-numeric keys unless opt.text=true.
|
||||||
|
* Triggers action on blur.
|
||||||
|
* @param {string} label - Input label text
|
||||||
|
* @param {object} [opt={}] - Options: {size, height, action, convert, bound, units, round, text, comma, disabled, title, id, trigger, hide}
|
||||||
|
* @returns {HTMLInputElement|HTMLTextAreaElement} Input element with setVisible() method
|
||||||
|
*/
|
||||||
function newInput(label, opt = {}) {
|
function newInput(label, opt = {}) {
|
||||||
let row = newDiv(opt),
|
let row = newDiv(opt),
|
||||||
hide = opt.hide,
|
hide = opt.hide,
|
||||||
|
|
@ -621,6 +740,14 @@ function newInput(label, opt = {}) {
|
||||||
ip = height > 1 ? DOC.createElement('textarea') : DOC.createElement('input');
|
ip = height > 1 ? DOC.createElement('textarea') : DOC.createElement('input');
|
||||||
|
|
||||||
row.appendChild(newLabel(label));
|
row.appendChild(newLabel(label));
|
||||||
|
if (opt.selector) {
|
||||||
|
let sel = DOC.createElement('button');
|
||||||
|
sel.classList.add('z-select');
|
||||||
|
sel.addEventListener('click', event => opt.selector('click', { input: ip, event, opt }));
|
||||||
|
sel.addEventListener('mouseenter', event => opt.selector('enter', { input: ip, event, opt }));
|
||||||
|
sel.addEventListener('mouseleave', event => opt.selector('leave', { input: ip, event, opt }));
|
||||||
|
row.appendChild(sel);
|
||||||
|
}
|
||||||
row.appendChild(ip);
|
row.appendChild(ip);
|
||||||
row.setAttribute("class", opt.class || "var-row");
|
row.setAttribute("class", opt.class || "var-row");
|
||||||
if (height > 1) {
|
if (height > 1) {
|
||||||
|
|
@ -680,6 +807,9 @@ function newInput(label, opt = {}) {
|
||||||
if (opt.trigger) {
|
if (opt.trigger) {
|
||||||
refresh(opt.trigger === 1 || opt.trigger === true);
|
refresh(opt.trigger === 1 || opt.trigger === true);
|
||||||
}
|
}
|
||||||
|
if (opt.post) {
|
||||||
|
opt.post();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
if (opt.units) {
|
if (opt.units) {
|
||||||
addUnits(ip, opt.round || 3);
|
addUnits(ip, opt.round || 3);
|
||||||
|
|
@ -706,6 +836,12 @@ function addUnits(input, round) {
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a labeled range slider.
|
||||||
|
* @param {string} label - Slider label text
|
||||||
|
* @param {object} options - Options: {min, max, hide, title, action, modes, etc.}
|
||||||
|
* @returns {HTMLInputElement} Range input with setVisible() method
|
||||||
|
*/
|
||||||
function newRange(label, options) {
|
function newRange(label, options) {
|
||||||
let row = newDiv(options),
|
let row = newDiv(options),
|
||||||
ip = DOC.createElement('input'),
|
ip = DOC.createElement('input'),
|
||||||
|
|
@ -732,6 +868,14 @@ function newRange(label, options) {
|
||||||
return ip;
|
return ip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a labeled select dropdown.
|
||||||
|
* Triggers action on change.
|
||||||
|
* @param {string} label - Select label text
|
||||||
|
* @param {object} [options={}] - Options: {hide, id, convert, disabled, title, action, trigger, post, modes, etc.}
|
||||||
|
* @param {Array|string} source - Option source array or source attribute value
|
||||||
|
* @returns {HTMLSelectElement} Select element with setVisible() method
|
||||||
|
*/
|
||||||
function newSelect(label, options = {}, source) {
|
function newSelect(label, options = {}, source) {
|
||||||
let row = newDiv(options),
|
let row = newDiv(options),
|
||||||
ip = DOC.createElement('select'),
|
ip = DOC.createElement('select'),
|
||||||
|
|
@ -774,6 +918,14 @@ function newSelect(label, options = {}, source) {
|
||||||
return ip;
|
return ip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a labeled checkbox.
|
||||||
|
* Triggers action on click.
|
||||||
|
* @param {string} label - Checkbox label text
|
||||||
|
* @param {function} [action=bindTo] - Click handler function
|
||||||
|
* @param {object} [opt={}] - Options: {hide, disabled, title, trigger, modes, etc.}
|
||||||
|
* @returns {HTMLInputElement} Checkbox element with setVisible() method
|
||||||
|
*/
|
||||||
function newBoolean(label, action = bindTo, opt = {}) {
|
function newBoolean(label, action = bindTo, opt = {}) {
|
||||||
let row = newDiv(opt),
|
let row = newDiv(opt),
|
||||||
ip = DOC.createElement('input'),
|
ip = DOC.createElement('input'),
|
||||||
|
|
@ -828,7 +980,15 @@ function newBlank(options) {
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
// unlike other elements, does not auto-add to a row
|
/**
|
||||||
|
* Create a button element.
|
||||||
|
* Unlike other elements, does not auto-add to a row.
|
||||||
|
* Action can be a function or event name string.
|
||||||
|
* @param {string} label - Button label text
|
||||||
|
* @param {function|string} action - Click handler or event name to emit
|
||||||
|
* @param {object} [opt={}] - Options: {class, icon, title, id, modes, etc.}
|
||||||
|
* @returns {HTMLButtonElement} Button element with mode visibility
|
||||||
|
*/
|
||||||
function newButton(label, action, opt = {}) {
|
function newButton(label, action, opt = {}) {
|
||||||
let b = DOC.createElement('button');
|
let b = DOC.createElement('button');
|
||||||
|
|
||||||
|
|
@ -866,9 +1026,19 @@ function newButton(label, action, opt = {}) {
|
||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a row container with child elements.
|
||||||
|
* @param {Array<HTMLElement>} children - Child elements to append
|
||||||
|
* @param {object} options - Options: {class, noadd, modes, etc.}
|
||||||
|
* @returns {HTMLElement} Row element
|
||||||
|
*/
|
||||||
function newRow(children, options) {
|
function newRow(children, options) {
|
||||||
let row = addCollapsableElement((options && options.noadd) ? null : addTo);
|
let row = addCollapsableElement((options && options.noadd) ? null : addTo);
|
||||||
if (children) children.forEach(function (c) { row.appendChild(c) });
|
if (children) {
|
||||||
|
children.forEach((c) => {
|
||||||
|
row.appendChild(c);
|
||||||
|
});
|
||||||
|
};
|
||||||
addModeControls(row, options);
|
addModeControls(row, options);
|
||||||
if (options && options.class) {
|
if (options && options.class) {
|
||||||
options.class.split(' ').forEach(ce => {
|
options.class.split(' ').forEach(ce => {
|
||||||
450
src/kiri/app/keyboard.js
Normal file
450
src/kiri/app/keyboard.js
Normal file
|
|
@ -0,0 +1,450 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keyboard Controller (Singleton)
|
||||||
|
* Manages keyboard event handling and shortcuts
|
||||||
|
*/
|
||||||
|
|
||||||
|
class KeyboardControl {
|
||||||
|
#api;
|
||||||
|
#platform;
|
||||||
|
#selection;
|
||||||
|
#slider;
|
||||||
|
#space;
|
||||||
|
#catalog;
|
||||||
|
#sdb;
|
||||||
|
#uc;
|
||||||
|
#setCtrl;
|
||||||
|
#VIEWS;
|
||||||
|
#DOC;
|
||||||
|
#WIN;
|
||||||
|
#rotateInputSelection;
|
||||||
|
#settingsLoad;
|
||||||
|
#initialized = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the keyboard controller with dependencies
|
||||||
|
* @param {Object} config
|
||||||
|
* @param {Object} config.api - API object
|
||||||
|
* @param {Object} config.platform - Platform object
|
||||||
|
* @param {Object} config.selection - Selection object
|
||||||
|
* @param {Object} config.slider - Slider controller
|
||||||
|
* @param {Object} config.space - Space object
|
||||||
|
* @param {Object} config.catalog - Catalog object
|
||||||
|
* @param {Object} config.sdb - Local storage
|
||||||
|
* @param {Object} config.uc - User confirmation utility
|
||||||
|
* @param {Object} config.setCtrl - Settings controller
|
||||||
|
* @param {Object} config.VIEWS - View constants
|
||||||
|
* @param {Document} config.DOC - Document object
|
||||||
|
* @param {Window} config.WIN - Window object
|
||||||
|
* @param {Function} config.rotateInputSelection - Rotate input callback
|
||||||
|
* @param {Function} config.settingsLoad - Settings load callback
|
||||||
|
*/
|
||||||
|
init({ api, platform, selection, slider, space, catalog, sdb, uc, setCtrl, VIEWS, DOC, WIN, rotateInputSelection, settingsLoad }) {
|
||||||
|
if (this.#initialized) {
|
||||||
|
throw new Error('Keyboard already initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#api = api;
|
||||||
|
this.#platform = platform;
|
||||||
|
this.#selection = selection;
|
||||||
|
this.#slider = slider;
|
||||||
|
this.#space = space;
|
||||||
|
this.#catalog = catalog;
|
||||||
|
this.#sdb = sdb;
|
||||||
|
this.#uc = uc;
|
||||||
|
this.#setCtrl = setCtrl;
|
||||||
|
this.#VIEWS = VIEWS;
|
||||||
|
this.#DOC = DOC;
|
||||||
|
this.#WIN = WIN;
|
||||||
|
this.#rotateInputSelection = rotateInputSelection;
|
||||||
|
this.#settingsLoad = settingsLoad;
|
||||||
|
|
||||||
|
this.#bindEvents();
|
||||||
|
this.#initialized = true;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an input element has focus
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
inputHasFocus() {
|
||||||
|
let active = this.#DOC.activeElement;
|
||||||
|
return active && (active.nodeName === "INPUT" || active.nodeName === "TEXTAREA");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get character code at position 0
|
||||||
|
* @param {string} c - Character
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
#cca(c) {
|
||||||
|
return c.charCodeAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind keyboard event handlers to space.event
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
#bindEvents() {
|
||||||
|
this.#space.event.addHandlers(self, [
|
||||||
|
'keyup', this.#handleKeyUp.bind(this),
|
||||||
|
'keydown', this.#handleKeyDown.bind(this),
|
||||||
|
'keypress', this.#handleKeyPress.bind(this)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle keyup events.
|
||||||
|
* Primarily handles Escape key for dismissing modals/dialogs/selections.
|
||||||
|
* @private
|
||||||
|
* @param {KeyboardEvent} evt - Keyboard event
|
||||||
|
* @returns {boolean} False to prevent default
|
||||||
|
*/
|
||||||
|
#handleKeyUp(evt) {
|
||||||
|
// Allow feature hooks to intercept
|
||||||
|
if (this.#api.feature.on_key) {
|
||||||
|
if (this.#api.feature.on_key({up:evt})) return;
|
||||||
|
}
|
||||||
|
for (let handler of this.#api.feature.on_key2) {
|
||||||
|
if (handler({up:evt})) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (evt.keyCode) {
|
||||||
|
// escape
|
||||||
|
case 27:
|
||||||
|
// blur text input focus
|
||||||
|
this.#DOC.activeElement.blur();
|
||||||
|
// dismiss modals
|
||||||
|
this.#api.modal.hide();
|
||||||
|
// deselect widgets
|
||||||
|
this.#platform.deselect();
|
||||||
|
// hide all dialogs
|
||||||
|
this.#api.dialog.hide();
|
||||||
|
// cancel slicing
|
||||||
|
this.#api.function.cancel();
|
||||||
|
// trigger escape handlers (used by FDM mode for support editing)
|
||||||
|
this.#onEscape();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle keydown events.
|
||||||
|
* Handles arrow keys for movement/rotation, delete key, and Cmd/Ctrl shortcuts.
|
||||||
|
* Arrow keys: rotate (default) or move (with Alt). Shift = smaller rotation.
|
||||||
|
* Meta + Up/Down: navigate layers.
|
||||||
|
* @private
|
||||||
|
* @param {KeyboardEvent} evt - Keyboard event
|
||||||
|
*/
|
||||||
|
#handleKeyDown(evt) {
|
||||||
|
if (this.#api.modal.visible()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow feature hooks to intercept
|
||||||
|
if (this.#api.feature.on_key) {
|
||||||
|
if (this.#api.feature.on_key({down:evt})) return;
|
||||||
|
}
|
||||||
|
for (let handler of this.#api.feature.on_key2) {
|
||||||
|
if (handler({down:evt})) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let move = evt.altKey ? 5 : 0,
|
||||||
|
deg = move ? 0 : -Math.PI / (evt.shiftKey ? 36 : 2);
|
||||||
|
|
||||||
|
switch (evt.keyCode) {
|
||||||
|
case 8: // apple: delete/backspace
|
||||||
|
case 46: // others: delete
|
||||||
|
if (this.inputHasFocus()) return false;
|
||||||
|
this.#platform.delete(this.#selection.meshes());
|
||||||
|
evt.preventDefault();
|
||||||
|
break;
|
||||||
|
case 37: // left arrow
|
||||||
|
if (this.inputHasFocus()) return false;
|
||||||
|
if (deg) this.#selection.rotate(0, 0, -deg);
|
||||||
|
if (move > 0) this.#selection.move(-move, 0, 0);
|
||||||
|
evt.preventDefault();
|
||||||
|
break;
|
||||||
|
case 39: // right arrow
|
||||||
|
if (this.inputHasFocus()) return false;
|
||||||
|
if (deg) this.#selection.rotate(0, 0, deg);
|
||||||
|
if (move > 0) this.#selection.move(move, 0, 0);
|
||||||
|
evt.preventDefault();
|
||||||
|
break;
|
||||||
|
case 38: // up arrow
|
||||||
|
if (this.inputHasFocus()) return false;
|
||||||
|
if (evt.metaKey) {
|
||||||
|
const { hi } = this.#slider.getRange();
|
||||||
|
return this.#api.show.layer(hi + 1);
|
||||||
|
}
|
||||||
|
if (deg) this.#selection.rotate(deg, 0, 0);
|
||||||
|
if (move > 0) this.#selection.move(0, move, 0);
|
||||||
|
evt.preventDefault();
|
||||||
|
break;
|
||||||
|
case 40: // down arrow
|
||||||
|
if (this.inputHasFocus()) return false;
|
||||||
|
if (evt.metaKey) {
|
||||||
|
const { hi } = this.#slider.getRange();
|
||||||
|
return this.#api.show.layer(hi - 1);
|
||||||
|
}
|
||||||
|
if (deg) this.#selection.rotate(-deg, 0, 0);
|
||||||
|
if (move > 0) this.#selection.move(0, -move, 0);
|
||||||
|
evt.preventDefault();
|
||||||
|
break;
|
||||||
|
case 65: // 'a' for select all
|
||||||
|
if (evt.metaKey || evt.ctrlKey) {
|
||||||
|
if (this.inputHasFocus()) return false;
|
||||||
|
evt.preventDefault();
|
||||||
|
this.#platform.deselect();
|
||||||
|
this.#platform.select_all();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 83: // 's' for save workspace
|
||||||
|
if (evt.ctrlKey) {
|
||||||
|
evt.preventDefault();
|
||||||
|
this.#api.conf.save();
|
||||||
|
console.log("settings saved");
|
||||||
|
} else
|
||||||
|
if (evt.metaKey) {
|
||||||
|
evt.preventDefault();
|
||||||
|
this.#api.space.save();
|
||||||
|
this.#setCtrl.sync.put();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 76: // 'l' for restore workspace
|
||||||
|
if (evt.metaKey) {
|
||||||
|
evt.preventDefault();
|
||||||
|
this.#api.space.restore();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle keypress events.
|
||||||
|
* Main keyboard shortcuts for application commands:
|
||||||
|
* - Numbers 0-9: show slice layers (0=all, 1-9=10-90%)
|
||||||
|
* - s/S: slice, p/P: prepare, x/X: export
|
||||||
|
* - a: arrange/layout, v: toggle single slice view
|
||||||
|
* - d: duplicate, m: mirror
|
||||||
|
* - i: import, r: recent files
|
||||||
|
* - e: devices, o: tools, l: load settings
|
||||||
|
* - Z: clear all settings, C: refresh catalog
|
||||||
|
* - Ctrl+g: group, Ctrl+u: ungroup
|
||||||
|
* @private
|
||||||
|
* @param {KeyboardEvent} evt - Keyboard event
|
||||||
|
* @returns {boolean} False to prevent default
|
||||||
|
*/
|
||||||
|
#handleKeyPress(evt) {
|
||||||
|
let handled = true;
|
||||||
|
if (this.#api.modal.visible() || this.inputHasFocus()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow feature hooks to intercept
|
||||||
|
if (this.#api.feature.on_key) {
|
||||||
|
if (this.#api.feature.on_key({key:evt})) return;
|
||||||
|
}
|
||||||
|
for (let handler of this.#api.feature.on_key2) {
|
||||||
|
if (handler({key:evt})) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle widget grouping (disabled: broken, conflicts with hash settings)
|
||||||
|
if (evt.ctrlKey) {
|
||||||
|
switch (evt.key) {
|
||||||
|
// case 'g': return this.#api.group.merge();
|
||||||
|
// case 'u': return this.#api.group.split();
|
||||||
|
case 'w': return $('render-edges').onclick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let api = this.#api;
|
||||||
|
let isWireframe = api.local.getBoolean('model.wireframe');
|
||||||
|
let isGhost = !isWireframe && (api.local.getFloat('model.opacity') ?? 1.0) < 1;
|
||||||
|
|
||||||
|
switch (evt.keyCode) {
|
||||||
|
case this.#cca('`'): this.#api.show.slices(0); break;
|
||||||
|
case this.#cca('0'): {
|
||||||
|
const { max } = this.#slider.getRange();
|
||||||
|
this.#api.show.slices(max);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case this.#cca('1'): {
|
||||||
|
const { max } = this.#slider.getRange();
|
||||||
|
this.#api.show.slices(max/10);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case this.#cca('2'): {
|
||||||
|
const { max } = this.#slider.getRange();
|
||||||
|
this.#api.show.slices(max*2/10);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case this.#cca('3'): {
|
||||||
|
const { max } = this.#slider.getRange();
|
||||||
|
this.#api.show.slices(max*3/10);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case this.#cca('4'): {
|
||||||
|
const { max } = this.#slider.getRange();
|
||||||
|
this.#api.show.slices(max*4/10);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case this.#cca('5'): {
|
||||||
|
const { max } = this.#slider.getRange();
|
||||||
|
this.#api.show.slices(max*5/10);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case this.#cca('6'): {
|
||||||
|
const { max } = this.#slider.getRange();
|
||||||
|
this.#api.show.slices(max*6/10);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case this.#cca('7'): {
|
||||||
|
const { max } = this.#slider.getRange();
|
||||||
|
this.#api.show.slices(max*7/10);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case this.#cca('8'): {
|
||||||
|
const { max } = this.#slider.getRange();
|
||||||
|
this.#api.show.slices(max*8/10);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case this.#cca('9'): {
|
||||||
|
const { max } = this.#slider.getRange();
|
||||||
|
this.#api.show.slices(max*9/10);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case this.#cca('?'):
|
||||||
|
this.#api.help.show();
|
||||||
|
break;
|
||||||
|
case this.#cca('Z'): // reset stored state
|
||||||
|
this.#uc.confirm('clear all settings and preferences?').then(yes => {
|
||||||
|
if (yes) {
|
||||||
|
this.#sdb.clear();
|
||||||
|
this.#WIN.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case this.#cca('C'): // refresh catalog
|
||||||
|
this.#catalog.refresh();
|
||||||
|
break;
|
||||||
|
case this.#cca('i'): // file import
|
||||||
|
this.#api.event.import();
|
||||||
|
break;
|
||||||
|
case this.#cca('S'): // slice
|
||||||
|
case this.#cca('s'): // slice
|
||||||
|
this.#api.function.slice();
|
||||||
|
break;
|
||||||
|
case this.#cca('P'): // prepare
|
||||||
|
case this.#cca('p'): // prepare
|
||||||
|
if (this.#api.mode.get() !== 'SLA') {
|
||||||
|
// hidden in SLA mode
|
||||||
|
this.#api.function.prepare();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case this.#cca('X'): // export
|
||||||
|
case this.#cca('x'): // export
|
||||||
|
this.#api.function.export();
|
||||||
|
break;
|
||||||
|
case this.#cca('g'): // CAM animate
|
||||||
|
this.#api.function.animate();
|
||||||
|
break;
|
||||||
|
case this.#cca('O'): // manual rotation
|
||||||
|
this.#rotateInputSelection();
|
||||||
|
break;
|
||||||
|
case this.#cca('r'): // recent files
|
||||||
|
this.#api.modal.show('files');
|
||||||
|
break;
|
||||||
|
case this.#cca('q'): // preferences
|
||||||
|
this.#api.modal.show('prefs');
|
||||||
|
break;
|
||||||
|
case this.#cca('l'): // device
|
||||||
|
this.#settingsLoad();
|
||||||
|
break;
|
||||||
|
case this.#cca('e'): // device
|
||||||
|
this.#api.show.devices();
|
||||||
|
break;
|
||||||
|
case this.#cca('o'): // tools
|
||||||
|
this.#api.show.tools();
|
||||||
|
break;
|
||||||
|
case this.#cca('v'): // toggle single slice view mode
|
||||||
|
if (this.#api.view.get() === this.#VIEWS.ARRANGE) {
|
||||||
|
this.#api.space.set_focus(this.#selection.widgets());
|
||||||
|
}
|
||||||
|
const { lo, hi } = this.#slider.getRange();
|
||||||
|
if (hi === lo) {
|
||||||
|
this.#slider.setRange(0, hi);
|
||||||
|
} else {
|
||||||
|
this.#slider.setRange(hi, hi);
|
||||||
|
}
|
||||||
|
this.#api.show.slices();
|
||||||
|
break;
|
||||||
|
case this.#cca('d'): // duplicate object
|
||||||
|
this.#selection.duplicate();
|
||||||
|
break;
|
||||||
|
case this.#cca('m'): // mirror object
|
||||||
|
this.#selection.mirror();
|
||||||
|
break;
|
||||||
|
case this.#cca('a'):
|
||||||
|
if (this.#api.view.get() === this.#VIEWS.ARRANGE) {
|
||||||
|
// auto arrange items on platform
|
||||||
|
this.#platform.layout();
|
||||||
|
if (!this.#api.conf.get().controller.spaceRandoX) {
|
||||||
|
this.#api.space.set_focus(this.#selection.widgets());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// go to arrange view
|
||||||
|
this.#api.view.set(this.#VIEWS.ARRANGE);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case this.#cca('w'): // set ghost
|
||||||
|
if (isGhost && !isWireframe) {
|
||||||
|
$('render-solid').onclick();
|
||||||
|
} else {
|
||||||
|
$('render-ghost').onclick();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case this.#cca('W'): // set wireframe
|
||||||
|
if (isWireframe) {
|
||||||
|
$('render-solid').onclick();
|
||||||
|
} else {
|
||||||
|
$('render-wire').onclick();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.#onUnhandledKey(evt);
|
||||||
|
handled = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (handled) {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when escape key is pressed
|
||||||
|
* Replaces api.event.emit('key.esc')
|
||||||
|
*/
|
||||||
|
#onEscape() {
|
||||||
|
// Notify listeners (currently used by FDM mode for support editing)
|
||||||
|
this.#api.event.emit("key.esc");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an unhandled key is pressed
|
||||||
|
* Replaces api.event.emit('keypress', evt)
|
||||||
|
*/
|
||||||
|
#onUnhandledKey(evt) {
|
||||||
|
// Notify listeners for custom key handling
|
||||||
|
this.#api.event.emit('keypress', evt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export singleton instance
|
||||||
|
export const keyboard = new KeyboardControl();
|
||||||
|
|
@ -1,10 +1,23 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global language/translation system.
|
||||||
|
* Stores language maps and tracks the current language selection.
|
||||||
|
*/
|
||||||
const LANG = self.lang = { current: {} };
|
const LANG = self.lang = { current: {} };
|
||||||
|
|
||||||
|
/** Default language code */
|
||||||
const KDFL = 'en-us';
|
const KDFL = 'en-us';
|
||||||
|
|
||||||
|
/** Currently selected language code from browser */
|
||||||
let lset = navigator.language.toLocaleLowerCase();
|
let lset = navigator.language.toLocaleLowerCase();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map language codes to supported language identifiers.
|
||||||
|
* Normalizes short codes (en, fr, de) to full language codes (en-us, fr-fr, de-de).
|
||||||
|
* @param {string} key - Language code (e.g., 'en', 'fr', 'de-de')
|
||||||
|
* @returns {string} Normalized language code or default 'en-us'
|
||||||
|
*/
|
||||||
LANG.map = function(key) {
|
LANG.map = function(key) {
|
||||||
if (!key) {
|
if (!key) {
|
||||||
return KDFL;
|
return KDFL;
|
||||||
|
|
@ -23,10 +36,21 @@ LANG.map = function(key) {
|
||||||
return KDFL;
|
return KDFL;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current browser language setting.
|
||||||
|
* @returns {string} Browser language code
|
||||||
|
*/
|
||||||
LANG.get = function() {
|
LANG.get = function() {
|
||||||
return lset;
|
return lset;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set language by trying a list of language codes in order.
|
||||||
|
* Falls back to browser language, then English if no arguments provided.
|
||||||
|
* Populates LANG.current with translations, filling missing keys from English.
|
||||||
|
* @param {...string} keys - Language codes to try in order
|
||||||
|
* @returns {string|undefined} Selected language code or undefined if none found
|
||||||
|
*/
|
||||||
LANG.set = function() {
|
LANG.set = function() {
|
||||||
let map, key, keys = [...arguments];
|
let map, key, keys = [...arguments];
|
||||||
// provide default if none given
|
// provide default if none given
|
||||||
|
|
@ -3,11 +3,19 @@
|
||||||
import { newPolygon, Polygon } from '../../geo/polygon.js';
|
import { newPolygon, Polygon } from '../../geo/polygon.js';
|
||||||
import { polygons as POLY } from '../../geo/polygons.js';
|
import { polygons as POLY } from '../../geo/polygons.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layer management system for 3D visualization.
|
||||||
|
* Organizes geometry (lines, polygons, faces, paths) by layer with color/opacity.
|
||||||
|
* Supports multiple rendering styles: basic lines, webgl lines, filled areas, 3D extrusion paths.
|
||||||
|
*/
|
||||||
export class Layers {
|
export class Layers {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize or reset layers, profiles, and statistics.
|
||||||
|
*/
|
||||||
init() {
|
init() {
|
||||||
this.layers = {};
|
this.layers = {};
|
||||||
this.profiles = {};
|
this.profiles = {};
|
||||||
|
|
@ -20,21 +28,47 @@ export class Layers {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get layer data by layer index.
|
||||||
|
* @param {number} layer - Layer index
|
||||||
|
* @returns {object} Layer data or undefined
|
||||||
|
*/
|
||||||
getLayer(layer) {
|
getLayer(layer) {
|
||||||
return this.layers[layer];
|
return this.layers[layer];
|
||||||
}
|
}
|
||||||
|
|
||||||
// in radians
|
/**
|
||||||
|
* Set rotation for subsequent layers.
|
||||||
|
* @param {number} [x=0] - X rotation in radians
|
||||||
|
* @param {number} [y=0] - Y rotation in radians
|
||||||
|
* @param {number} [z=0] - Z rotation in radians
|
||||||
|
* @returns {Layers} This instance for chaining
|
||||||
|
*/
|
||||||
setRotation(x = 0, y = 0, z = 0) {
|
setRotation(x = 0, y = 0, z = 0) {
|
||||||
this.rotation = { x, y, z };
|
this.rotation = { x, y, z };
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set position offset for subsequent layers.
|
||||||
|
* @param {number} [x=0] - X position
|
||||||
|
* @param {number} [y=0] - Y position
|
||||||
|
* @param {number} [z=0] - Z position
|
||||||
|
* @returns {Layers} This instance for chaining
|
||||||
|
*/
|
||||||
setPosition(x = 0, y = 0, z = 0) {
|
setPosition(x = 0, y = 0, z = 0) {
|
||||||
this.position = { x, y, z };
|
this.position = { x, y, z };
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set current layer for subsequent geometry additions.
|
||||||
|
* Creates layer if it doesn't exist.
|
||||||
|
* @param {number} layer - Layer index
|
||||||
|
* @param {number|object} colors - Single color number or {line, face, opacity}
|
||||||
|
* @param {boolean} [off] - If true, marks layer as off/hidden
|
||||||
|
* @returns {Layers} This instance for chaining
|
||||||
|
*/
|
||||||
setLayer(layer, colors, off) {
|
setLayer(layer, colors, off) {
|
||||||
let layers = this.layers;
|
let layers = this.layers;
|
||||||
if (typeof(colors) === 'number') {
|
if (typeof(colors) === 'number') {
|
||||||
|
|
@ -65,13 +99,24 @@ export class Layers {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
// add a line segment (two points)
|
/**
|
||||||
|
* Add a single line segment to current layer.
|
||||||
|
* @param {object} p1 - First point {x, y, z}
|
||||||
|
* @param {object} p2 - Second point {x, y, z}
|
||||||
|
* @returns {Layers} This instance for chaining
|
||||||
|
*/
|
||||||
addLine(p1, p2) {
|
addLine(p1, p2) {
|
||||||
this.current.lines.push(p1, p2);
|
this.current.lines.push(p1, p2);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
// add an array of line segments
|
/**
|
||||||
|
* Add multiple line segments to current layer.
|
||||||
|
* If options provided, converts to open polygons via addPolys.
|
||||||
|
* @param {Array} lines - Array of points (alternating pairs for line segments)
|
||||||
|
* @param {object} [options] - If provided, creates open polygons instead
|
||||||
|
* @returns {Layers} This instance for chaining
|
||||||
|
*/
|
||||||
addLines(lines, options) {
|
addLines(lines, options) {
|
||||||
if (options) {
|
if (options) {
|
||||||
// the open option encodes lines as open polygons
|
// the open option encodes lines as open polygons
|
||||||
|
|
@ -92,12 +137,23 @@ export class Layers {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
// an open or closed polygon
|
/**
|
||||||
|
* Add a single polygon to current layer.
|
||||||
|
* @param {Polygon} poly - Polygon to add
|
||||||
|
* @param {object} [options] - Rendering options
|
||||||
|
* @returns {Layers} This instance for chaining
|
||||||
|
*/
|
||||||
addPoly(poly, options) {
|
addPoly(poly, options) {
|
||||||
return this.addPolys([ poly ], options);
|
return this.addPolys([ poly ], options);
|
||||||
}
|
}
|
||||||
|
|
||||||
// a polygon rendered as a webgl line
|
/**
|
||||||
|
* Add polygons rendered as WebGL lines.
|
||||||
|
* Dispatches to addFlats or addPaths based on options.
|
||||||
|
* @param {Array<Polygon>} polys - Array of polygons
|
||||||
|
* @param {object} [options] - Options: {clean, flat, thin, z, color}
|
||||||
|
* @returns {Layers} This instance for chaining
|
||||||
|
*/
|
||||||
addPolys(polys, options) {
|
addPolys(polys, options) {
|
||||||
if (!polys) {
|
if (!polys) {
|
||||||
return this;
|
return this;
|
||||||
|
|
@ -134,8 +190,13 @@ export class Layers {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
// add an enclosed 3D polygon earcut into faces
|
/**
|
||||||
// used for FDM solids, bridges, flats debug, CAM hole generation & SLA slice visualization
|
* Add enclosed 3D polygon earcut into triangular faces.
|
||||||
|
* Used for FDM solids, bridges, flats debug, CAM hole generation, SLA slice visualization.
|
||||||
|
* @param {Polygon|Array<Polygon>} polys - Polygon(s) to tessellate
|
||||||
|
* @param {object} [options] - Options: {outline} to also draw polygon outline
|
||||||
|
* @returns {Layers} This instance for chaining
|
||||||
|
*/
|
||||||
addAreas(polys, options) {
|
addAreas(polys, options) {
|
||||||
const faces = this.current.faces;
|
const faces = this.current.faces;
|
||||||
polys = Array.isArray(polys) ? polys : [ polys ];
|
polys = Array.isArray(polys) ? polys : [ polys ];
|
||||||
|
|
@ -151,7 +212,13 @@ export class Layers {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// add a 2D polyline path (usually FDM extrusion paths)
|
/**
|
||||||
|
* Add 2D polyline paths with width (usually FDM extrusion paths).
|
||||||
|
* Creates flat ribbons with vertex normals for lighting.
|
||||||
|
* @param {Array<Polygon>} polys - Polygons to render as flat ribbons
|
||||||
|
* @param {object} [options] - Options: {offset, outline, color}
|
||||||
|
* @returns {Layers} This instance for chaining
|
||||||
|
*/
|
||||||
addFlats(polys, options) {
|
addFlats(polys, options) {
|
||||||
const opts = options || {};
|
const opts = options || {};
|
||||||
const offset = opts.offset || 1;
|
const offset = opts.offset || 1;
|
||||||
|
|
@ -200,7 +267,14 @@ export class Layers {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
// add 3D polyline path (FDM extrusion paths)
|
/**
|
||||||
|
* Add 3D polyline paths with extrusion (FDM extrusion paths).
|
||||||
|
* Creates 3D tubes with vertex normals for lighting.
|
||||||
|
* Supports incremental merging and per-segment color changes.
|
||||||
|
* @param {Array<Polygon>} polys - Polygons to render as 3D tubes
|
||||||
|
* @param {object} [options] - Options: {height, offset, z, color}
|
||||||
|
* @returns {Layers} This instance for chaining
|
||||||
|
*/
|
||||||
addPaths(polys, options) {
|
addPaths(polys, options) {
|
||||||
const opts = options || {};
|
const opts = options || {};
|
||||||
const height = opts.height || 1;
|
const height = opts.height || 1;
|
||||||
|
|
@ -259,6 +333,12 @@ export class Layers {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flatten polygon array or single polygon for rendering.
|
||||||
|
* Clones and flattens nested polygon structures.
|
||||||
|
* @param {Polygon|Array<Polygon>} polys - Polygon(s) to flatten
|
||||||
|
* @returns {Array<Polygon>} Flattened polygon array
|
||||||
|
*/
|
||||||
function flat(polys) {
|
function flat(polys) {
|
||||||
if (Array.isArray(polys)) {
|
if (Array.isArray(polys)) {
|
||||||
return POLY.flatten(polys.clone(true), [], true);
|
return POLY.flatten(polys.clone(true), [], true);
|
||||||
55
src/kiri/app/local.js
Normal file
55
src/kiri/app/local.js
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
// Lazily access api to avoid circular dependency at module load time
|
||||||
|
let _api;
|
||||||
|
|
||||||
|
function getAPI() {
|
||||||
|
if (!_api) {
|
||||||
|
_api = self.kiri_api;
|
||||||
|
}
|
||||||
|
return _api;
|
||||||
|
}
|
||||||
|
|
||||||
|
function localGet(key) {
|
||||||
|
const api = getAPI();
|
||||||
|
let sloc = api.conf.get().local;
|
||||||
|
return sloc[key] || api.sdb[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
function localSet(key, val) {
|
||||||
|
const api = getAPI();
|
||||||
|
let sloc = api.conf.get().local;
|
||||||
|
sloc[key] = api.sdb[key] = val;
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
function localRemove(key) {
|
||||||
|
const api = getAPI();
|
||||||
|
let sloc = api.conf.get().local;
|
||||||
|
return delete sloc[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
function localGetBoolean(key, def = true) {
|
||||||
|
let val = localGet(key);
|
||||||
|
return val === true || val === 'true' || val === def;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const local = {
|
||||||
|
get: (key) => localGet(key),
|
||||||
|
getItem: (key) => localGet(key),
|
||||||
|
getInt: (key) => parseInt(localGet(key)),
|
||||||
|
getFloat: (key) => {
|
||||||
|
const val = localGet(key);
|
||||||
|
if (val === undefined || val === null) return null;
|
||||||
|
const parsed = parseFloat(val);
|
||||||
|
return isNaN(parsed) ? null : parsed;
|
||||||
|
},
|
||||||
|
getBoolean: localGetBoolean,
|
||||||
|
toggle: (key, val, def) => {
|
||||||
|
return localSet(key, val ?? !localGetBoolean(key, def));
|
||||||
|
},
|
||||||
|
put: (key, val) => localSet(key, val),
|
||||||
|
set: (key, val) => localSet(key, val),
|
||||||
|
setItem: (key, val) => localSet(key, val),
|
||||||
|
removeItem: (key) => localRemove(key)
|
||||||
|
};
|
||||||
119
src/kiri/app/modal.js
Normal file
119
src/kiri/app/modal.js
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modal Manager (Singleton)
|
||||||
|
* Manages modal dialog display and animations
|
||||||
|
*/
|
||||||
|
|
||||||
|
class ModalControl {
|
||||||
|
#ui;
|
||||||
|
#onShow;
|
||||||
|
#onHide;
|
||||||
|
#showing;
|
||||||
|
#initialized = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the modal controller with dependencies
|
||||||
|
* @param {Object} config
|
||||||
|
* @param {Object} config.ui - UI element references
|
||||||
|
* @param {HTMLElement} config.ui.modal - Modal container element
|
||||||
|
* @param {Object} config.ui.modals - Object containing all modal elements by name
|
||||||
|
* @param {Function} config.onShow - Optional callback when modal is shown
|
||||||
|
* @param {Function} config.onHide - Optional callback when modal is hidden
|
||||||
|
*/
|
||||||
|
init({ ui, onShow, onHide }) {
|
||||||
|
if (this.#initialized) {
|
||||||
|
throw new Error('Modal already initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#ui = ui;
|
||||||
|
this.#onShow = onShow;
|
||||||
|
this.#onHide = onHide;
|
||||||
|
this.#initialized = true;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if modal is currently visible
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
visible() {
|
||||||
|
return modal.#ui.modal.style.display === 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a specific modal is currently visible
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
is(name) {
|
||||||
|
return modal.#showing === name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a specific modal with animation
|
||||||
|
* @param {string} which - Name of the modal to show
|
||||||
|
*/
|
||||||
|
show(which) {
|
||||||
|
let mod = modal.#ui.modal,
|
||||||
|
style = mod.style,
|
||||||
|
visible = modal.visible(),
|
||||||
|
info = { pct: 0 };
|
||||||
|
|
||||||
|
// if the dialog needs it, it will re-add it
|
||||||
|
document.body.classList.remove('devel');
|
||||||
|
|
||||||
|
// hide all modals before showing another
|
||||||
|
Object.keys(modal.#ui.modals).forEach(name => {
|
||||||
|
modal.#ui.modals[name].style.display = name === which ? 'flex' : '';
|
||||||
|
});
|
||||||
|
|
||||||
|
modal.#showing = which;
|
||||||
|
|
||||||
|
const ondone = () => {
|
||||||
|
modal.#onShow?.(which);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
return ondone();
|
||||||
|
}
|
||||||
|
|
||||||
|
style.height = '0';
|
||||||
|
style.display = 'flex';
|
||||||
|
|
||||||
|
new TWEEN.Tween(info).
|
||||||
|
easing(TWEEN.Easing.Quadratic.InOut).
|
||||||
|
to({ pct: 100 }, 100).
|
||||||
|
onUpdate(() => { style.height = `${info.pct}%` }).
|
||||||
|
onComplete(ondone).
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide the modal with animation
|
||||||
|
*/
|
||||||
|
hide() {
|
||||||
|
if (!modal.visible()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
modal.#showing = undefined;
|
||||||
|
|
||||||
|
let mod = modal.#ui.modal,
|
||||||
|
style = mod.style,
|
||||||
|
info = { pct: 100 };
|
||||||
|
|
||||||
|
new TWEEN.Tween(info).
|
||||||
|
easing(TWEEN.Easing.Quadratic.InOut).
|
||||||
|
to({ pct: 0 }, 100).
|
||||||
|
onUpdate(() => { style.height = `${info.pct}%` }).
|
||||||
|
onComplete(() => {
|
||||||
|
style.display = '';
|
||||||
|
modal.#onHide?.();
|
||||||
|
}).
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export singleton instance
|
||||||
|
export const modal = new ModalControl();
|
||||||
148
src/kiri/app/mode.js
Normal file
148
src/kiri/app/mode.js
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
import { $ } from '../../moto/webui.js';
|
||||||
|
import { api } from './api.js';
|
||||||
|
import { consts } from './consts.js';
|
||||||
|
import { modal } from './modal.js';
|
||||||
|
import { platform } from './platform.js';
|
||||||
|
import { selection } from './selected.js';
|
||||||
|
import { settings } from './conf/manager.js';
|
||||||
|
|
||||||
|
const { MODES, VIEWS } = consts;
|
||||||
|
const clone = Object.clone;
|
||||||
|
|
||||||
|
/** Current operating mode ID */
|
||||||
|
let MODE = MODES.FDM;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current mode name as string.
|
||||||
|
* @returns {string} Mode name (FDM, CAM, SLA, LASER, etc.)
|
||||||
|
*/
|
||||||
|
function getMode() {
|
||||||
|
return settings.mode();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current mode name in lowercase.
|
||||||
|
* @returns {string} Lowercase mode name (fdm, cam, sla, etc.)
|
||||||
|
*/
|
||||||
|
function getModeLower() {
|
||||||
|
return getMode().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch to a different operating mode.
|
||||||
|
* Convenience wrapper around setMode() that updates platform size after switch.
|
||||||
|
* @param {string} mode - Target mode name (FDM, CAM, SLA, LASER, DRAG, WJET, WEDM)
|
||||||
|
*/
|
||||||
|
function switchMode(mode) {
|
||||||
|
setMode(mode, null, platform.update_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set operating mode with full initialization.
|
||||||
|
* Complex function that:
|
||||||
|
* - Validates and sets mode constant
|
||||||
|
* - Updates UI to show/hide mode-specific controls
|
||||||
|
* - Restores cached device for the mode
|
||||||
|
* - Resets view to ARRANGE
|
||||||
|
* - Saves settings
|
||||||
|
* - Updates platform and selection
|
||||||
|
* - Emits mode.set event
|
||||||
|
*
|
||||||
|
* @param {string} mode - Target mode name (FDM, CAM, SLA, LASER, DRAG, WJET, WEDM)
|
||||||
|
* @param {*} lock - Currently unused parameter
|
||||||
|
* @param {function} [then] - Optional callback after mode set complete
|
||||||
|
*/
|
||||||
|
function setMode(mode, lock, then) {
|
||||||
|
if (!MODES[mode]) {
|
||||||
|
console.log("invalid mode: "+mode);
|
||||||
|
mode = 'FDM';
|
||||||
|
}
|
||||||
|
const current = settings.get();
|
||||||
|
// change mode constants
|
||||||
|
current.mode = mode;
|
||||||
|
MODE = MODES[mode];
|
||||||
|
// gcode edit area for any non-SLA mode
|
||||||
|
api.uc.setVisible($('gcode-edit'), mode !== 'SLA');
|
||||||
|
// highlight selected mode menu item
|
||||||
|
["FDM","CAM","SLA","LASER","DRAG","WJET","WEDM"].forEach(sm => {
|
||||||
|
const cl = $(`mode-${sm.toLowerCase()}`).classList;
|
||||||
|
if (sm === mode) {
|
||||||
|
cl.add('selected');
|
||||||
|
} else {
|
||||||
|
cl.remove('selected');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// restore cached device profile for this mode
|
||||||
|
if (current.cdev[mode]) {
|
||||||
|
current.device = clone(current.cdev[mode]);
|
||||||
|
api.event.emit('device.select', api.device.get());
|
||||||
|
}
|
||||||
|
// hide/show
|
||||||
|
api.uc.setVisible($('set-tools'), mode === 'CAM');
|
||||||
|
// updates right-hand menu by enabling/disabling fields
|
||||||
|
api.view.set(VIEWS.ARRANGE);
|
||||||
|
api.uc.setMode(MODE);
|
||||||
|
// sanitize and persist settings
|
||||||
|
api.conf.load();
|
||||||
|
api.conf.save();
|
||||||
|
// trigger settings event
|
||||||
|
api.event.emit('settings', settings.get());
|
||||||
|
// other housekeeping
|
||||||
|
platform.update_selected();
|
||||||
|
selection.update_bounds(api.widgets.all());
|
||||||
|
api.conf.update_fields();
|
||||||
|
// if device dialog showing, needs to be refreshed
|
||||||
|
if (modal.is('setup')) {
|
||||||
|
api.show.devices();
|
||||||
|
}
|
||||||
|
api.space.restore(null, true);
|
||||||
|
api.event.emit("mode.set", mode);
|
||||||
|
if (then) {
|
||||||
|
then();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the name of the current process profile for this mode.
|
||||||
|
* @returns {string} Process profile name
|
||||||
|
*/
|
||||||
|
function currentProcessName() {
|
||||||
|
return settings.get().cproc[getMode()];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the process configuration object for the current mode and profile.
|
||||||
|
* @returns {object} Process configuration
|
||||||
|
*/
|
||||||
|
function currentProcessCode() {
|
||||||
|
return settings.get().sproc[getMode()][currentProcessName()];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mode = {
|
||||||
|
get_id() { return MODE },
|
||||||
|
get_lower: getModeLower,
|
||||||
|
get: getMode,
|
||||||
|
set: setMode,
|
||||||
|
switch: switchMode,
|
||||||
|
set_expert() {}, // noop
|
||||||
|
is_fdm() { return MODE === MODES.FDM },
|
||||||
|
is_cam() { return MODE === MODES.CAM },
|
||||||
|
is_sla() { return MODE === MODES.SLA },
|
||||||
|
is_drag() { return MODE === MODES.DRAG },
|
||||||
|
is_wedm() { return MODE === MODES.WEDM },
|
||||||
|
is_wjet() { return MODE === MODES.WJET },
|
||||||
|
is_laser() { return MODE === MODES.LASER },
|
||||||
|
is_2d() { return false ||
|
||||||
|
mode.is_drag() ||
|
||||||
|
mode.is_wedm() ||
|
||||||
|
mode.is_wjet() ||
|
||||||
|
mode.is_laser()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const process = {
|
||||||
|
code: currentProcessCode,
|
||||||
|
get: currentProcessName
|
||||||
|
};
|
||||||
218
src/kiri/app/mouse.js
Normal file
218
src/kiri/app/mouse.js
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interaction Controller (Singleton)
|
||||||
|
* Manages mouse hover, click, and drag interactions with 3D objects
|
||||||
|
*/
|
||||||
|
|
||||||
|
class InteractionControl {
|
||||||
|
#api;
|
||||||
|
#space;
|
||||||
|
#platform;
|
||||||
|
#selection;
|
||||||
|
#ui;
|
||||||
|
#settings;
|
||||||
|
#VIEWS;
|
||||||
|
#initialized = false;
|
||||||
|
|
||||||
|
// Callbacks to replace event emissions
|
||||||
|
#onHover;
|
||||||
|
#onHoverDown;
|
||||||
|
#onHoverUp;
|
||||||
|
#onDragDone;
|
||||||
|
#onSelectionDrag;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the interaction controller with dependencies
|
||||||
|
* @param {Object} config
|
||||||
|
* @param {Object} config.api - API object
|
||||||
|
* @param {Object} config.space - Space object (3D scene)
|
||||||
|
* @param {Object} config.platform - Platform object
|
||||||
|
* @param {Object} config.selection - Selection object
|
||||||
|
* @param {Object} config.ui - UI element references
|
||||||
|
* @param {Function} config.settings - Settings getter function
|
||||||
|
* @param {Object} config.VIEWS - View constants
|
||||||
|
* @param {Function} config.onHover - Called when hovering over objects
|
||||||
|
* @param {Function} config.onHoverDown - Called on mouse down over object
|
||||||
|
* @param {Function} config.onHoverUp - Called on mouse up over object
|
||||||
|
* @param {Function} config.onDragDone - Called when drag is complete
|
||||||
|
* @param {Function} config.onSelectionDrag - Called during selection drag
|
||||||
|
*/
|
||||||
|
init({ api, space, platform, selection, ui, settings, VIEWS, onHover, onHoverDown, onHoverUp, onDragDone, onSelectionDrag }) {
|
||||||
|
if (this.#initialized) {
|
||||||
|
throw new Error('Interactions already initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#api = api;
|
||||||
|
this.#space = space;
|
||||||
|
this.#platform = platform;
|
||||||
|
this.#selection = selection;
|
||||||
|
this.#ui = ui;
|
||||||
|
this.#settings = settings;
|
||||||
|
this.#VIEWS = VIEWS;
|
||||||
|
this.#onHover = onHover;
|
||||||
|
this.#onHoverDown = onHoverDown;
|
||||||
|
this.#onHoverUp = onHoverUp;
|
||||||
|
this.#onDragDone = onDragDone;
|
||||||
|
this.#onSelectionDrag = onSelectionDrag;
|
||||||
|
|
||||||
|
this.#bindHoverHandlers();
|
||||||
|
this.#bindMouseHandlers();
|
||||||
|
this.#initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind hover event handlers for mouse and platform interactions.
|
||||||
|
* Listens to "feature.hover" event to enable/disable hover functionality.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
#bindHoverHandlers() {
|
||||||
|
// Set up hover handlers
|
||||||
|
this.#api.event.on("feature.hover", enable => {
|
||||||
|
this.#space.mouse.onHover(enable ? this.#mouseOnHover.bind(this) : undefined);
|
||||||
|
this.#space.platform.onHover(enable ? this.#platformOnHover.bind(this) : undefined);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle mouse hover over widgets.
|
||||||
|
* Returns widget meshes if no intersection, otherwise triggers hover callback.
|
||||||
|
* @private
|
||||||
|
* @param {object} int - Intersection data
|
||||||
|
* @param {Event} event - Mouse event
|
||||||
|
* @param {Array} ints - All intersections
|
||||||
|
*/
|
||||||
|
#mouseOnHover(int, event, ints) {
|
||||||
|
if (!this.#api.feature.hover) return;
|
||||||
|
if (!int) return this.#api.feature.hovers || this.#api.widgets.meshes();
|
||||||
|
this.#onHover?.({int, ints, event, point: int.point, type: 'widget'});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle mouse hover over platform.
|
||||||
|
* Triggers hover callback with platform intersection point.
|
||||||
|
* @private
|
||||||
|
* @param {object} int - Intersection point
|
||||||
|
* @param {Event} event - Mouse event
|
||||||
|
*/
|
||||||
|
#platformOnHover(int, event) {
|
||||||
|
if (!this.#api.feature.hover) return;
|
||||||
|
if (int) this.#onHover?.({point: int, event, type: 'platform'});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind mouse click and drag handlers.
|
||||||
|
* Handles:
|
||||||
|
* - Mouse down: lay-flat (Ctrl/Cmd+click), custom hooks, hover mode
|
||||||
|
* - Mouse up: widget selection/deselection, custom hooks
|
||||||
|
* - Drag: move selected widgets with boundary checking
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
#bindMouseHandlers() {
|
||||||
|
// Mouse down handler
|
||||||
|
this.#space.mouse.downSelect((int, event) => {
|
||||||
|
// Feature hook for custom mouse down handling
|
||||||
|
if (this.#api.feature.on_mouse_down) {
|
||||||
|
return this.#api.feature.on_mouse_down(int, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hover mode handling
|
||||||
|
if (this.#api.feature.hover) {
|
||||||
|
if (int) {
|
||||||
|
this.#onHoverDown?.({int, point: int.point});
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
return this.#selection.meshes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lay flat with meta or ctrl clicking a selected face
|
||||||
|
if (int && (event.ctrlKey || event.metaKey || this.#api.feature.on_face_select)) {
|
||||||
|
let q = new THREE.Quaternion();
|
||||||
|
// find intersecting point, look "up" on Z and rotate to face that
|
||||||
|
q.setFromUnitVectors(int.face.normal, new THREE.Vector3(0,0,-1));
|
||||||
|
this.#selection.rotate(q);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#api.view.get() !== this.#VIEWS.ARRANGE) {
|
||||||
|
// return no selection in modes other than arrange
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
// return selected meshes for further mouse processing
|
||||||
|
return this.#api.feature.hovers || this.#selection.meshes();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mouse up handler
|
||||||
|
this.#space.mouse.upSelect((object, event) => {
|
||||||
|
// Feature hook for custom mouse up handling
|
||||||
|
if (this.#api.feature.on_mouse_up) {
|
||||||
|
if (event && object) {
|
||||||
|
return this.#api.feature.on_mouse_up(object, event);
|
||||||
|
} else {
|
||||||
|
return this.#api.widgets.meshes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hover mode handling
|
||||||
|
if (event && this.#api.feature.hover) {
|
||||||
|
this.#onHoverUp?.({ object, event });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular selection handling
|
||||||
|
if (event && event.target.nodeName === "CANVAS") {
|
||||||
|
if (object && object.object) {
|
||||||
|
if (object.object.widget) {
|
||||||
|
this.#platform.select(object.object.widget, event.shiftKey, false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.#platform.deselect();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return this.#api.feature.hovers || this.#api.widgets.meshes();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Drag handler
|
||||||
|
this.#space.mouse.onDrag((delta, offset, up = false, int) => {
|
||||||
|
if (this.#api.feature.hover) {
|
||||||
|
return this.#api.feature?.on_mouse_drag({ delta, offset, up, int });
|
||||||
|
// return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (up) {
|
||||||
|
this.#onDragDone?.(offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delta && this.#ui.freeLayout.checked) {
|
||||||
|
let set = this.#settings();
|
||||||
|
let dev = set.device;
|
||||||
|
let bound = set.bounds_sel;
|
||||||
|
let width = dev.bedWidth/2;
|
||||||
|
let depth = dev.bedDepth/2;
|
||||||
|
let isout = (
|
||||||
|
bound.min.x <= -width ||
|
||||||
|
bound.min.y <= -depth ||
|
||||||
|
bound.max.x >= width ||
|
||||||
|
bound.max.y >= depth
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isout) {
|
||||||
|
if (bound.min.x + delta.x <= -width) return;
|
||||||
|
if (bound.min.y + delta.y <= -depth) return;
|
||||||
|
if (bound.max.x + delta.x >= width) return;
|
||||||
|
if (bound.max.y + delta.y >= depth) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#selection.move(delta.x, delta.y, 0);
|
||||||
|
this.#onSelectionDrag?.(delta);
|
||||||
|
} else {
|
||||||
|
return this.#selection.meshes().length > 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export singleton instance
|
||||||
|
export const interact = new InteractionControl();
|
||||||
555
src/kiri/app/paint.js
Normal file
555
src/kiri/app/paint.js
Normal file
|
|
@ -0,0 +1,555 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paint overlay shaders for mesh painting functionality.
|
||||||
|
*
|
||||||
|
* Provides two implementations for visualizing painted regions on mesh surfaces:
|
||||||
|
* 1. Simple uniform-based (up to ~256 paint points)
|
||||||
|
* 2. Texture-based (unlimited paint points)
|
||||||
|
*
|
||||||
|
* Paint points are spheres defined by center position and radius.
|
||||||
|
* The shader checks if each fragment falls within any paint sphere and
|
||||||
|
* blends the paint color accordingly.
|
||||||
|
*
|
||||||
|
* SURFACE FILTERING:
|
||||||
|
* By default, painting is constrained to downward-facing surfaces only (z-normal < 0).
|
||||||
|
* This prevents paint "spill" onto adjacent upward-facing surfaces when painting
|
||||||
|
* support regions. For multi-material coloring or other uses, set
|
||||||
|
* options.onlyDownwardFacing = false to paint all surfaces.
|
||||||
|
*
|
||||||
|
* BELT PRINTER MODE:
|
||||||
|
* For belt printers, surfaces need support when facing "down the belt slope" rather
|
||||||
|
* than straight down. Set options.beltMode = true and provide options.beltAngle (in radians)
|
||||||
|
* to paint surfaces that face down the angled belt direction.
|
||||||
|
*
|
||||||
|
* PAINT POINT DATA STRUCTURE:
|
||||||
|
* ---------------------------
|
||||||
|
* Paint points must be an array of objects with this structure:
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* point: { x: number, y: number, z: number }, // World-space center of paint sphere
|
||||||
|
* radius: number // Sphere radius in mm
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* const paintPoints = [
|
||||||
|
* { point: { x: 10.5, y: 20.3, z: 15.0 }, radius: 5.0 },
|
||||||
|
* { point: { x: 12.0, y: 21.0, z: 15.5 }, radius: 5.0 },
|
||||||
|
* ...
|
||||||
|
* ];
|
||||||
|
*
|
||||||
|
* USAGE:
|
||||||
|
* ------
|
||||||
|
* // Initial setup - enable paint overlay (for support painting, downward-facing only):
|
||||||
|
* widget._origMaterial = widget.mesh.material;
|
||||||
|
* widget.mesh.material = widget.mesh.material.clone();
|
||||||
|
* addPaintOverlayAuto(widget.mesh.material, paintPoints, new THREE.Color(0x4488ff));
|
||||||
|
* widget.mesh.material.needsUpdate = true;
|
||||||
|
*
|
||||||
|
* // For multi-material coloring (paint all surfaces):
|
||||||
|
* addPaintOverlayAuto(widget.mesh.material, paintPoints, new THREE.Color(0xff0000), { onlyDownwardFacing: false });
|
||||||
|
*
|
||||||
|
* // Real-time updates while painting (see updatePaintOverlay function below)
|
||||||
|
*
|
||||||
|
* // Cleanup - restore original material:
|
||||||
|
* widget.mesh.material = widget._origMaterial;
|
||||||
|
* delete widget._origMaterial;
|
||||||
|
*
|
||||||
|
* REAL-TIME UPDATES:
|
||||||
|
* ------------------
|
||||||
|
* When actively painting (user dragging mouse), paint points are added continuously.
|
||||||
|
* Use updatePaintOverlay() to efficiently update the shader without recreating material:
|
||||||
|
*
|
||||||
|
* // Add new paint point
|
||||||
|
* paintPoints.push({ point: { x, y, z }, radius: 5 });
|
||||||
|
*
|
||||||
|
* // Update shader to show new point immediately
|
||||||
|
* updatePaintOverlay(widget.mesh.material, paintPoints);
|
||||||
|
*
|
||||||
|
* Note: After onBeforeCompile runs once, the shader program is compiled and cached.
|
||||||
|
* Subsequent updates only need to update the uniform values, which is very fast.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { THREE } from '../../ext/three.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add paint overlay to material using uniform array storage.
|
||||||
|
* Suitable for up to ~256 paint points (GPU uniform limit varies).
|
||||||
|
*
|
||||||
|
* Uses sphere-based distance checks to determine if a fragment is painted.
|
||||||
|
* Paint color is blended at 50% opacity with the underlying material color.
|
||||||
|
*
|
||||||
|
* @param {THREE.Material} material - Material to modify (will be mutated via onBeforeCompile)
|
||||||
|
* @param {Array<{point: {x,y,z}, radius: number}>} paintPoints - Array of paint spheres
|
||||||
|
* @param {THREE.Color} [paintColor] - Color for painted regions (default: light blue)
|
||||||
|
* @param {Object} [options] - Optional configuration
|
||||||
|
* @param {boolean} [options.onlyDownwardFacing=true] - Only paint on downward-facing surfaces (z-normal < 0)
|
||||||
|
* @param {boolean} [options.beltMode=false] - Enable belt printer mode (paints surfaces facing down belt slope)
|
||||||
|
* @param {number} [options.beltAngle=0] - Belt angle in radians (only used if beltMode is true)
|
||||||
|
* @param {number} [options.supportAngle=45] - Support angle in degrees (cone around down direction)
|
||||||
|
* @returns {THREE.Material} The modified material
|
||||||
|
*/
|
||||||
|
function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Color(0x4488ff), options = {}) {
|
||||||
|
// Initialize userData if it doesn't exist
|
||||||
|
if (!material.userData) {
|
||||||
|
material.userData = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: only paint downward-facing surfaces (for support painting)
|
||||||
|
const onlyDownwardFacing = options.onlyDownwardFacing !== undefined ? options.onlyDownwardFacing : true;
|
||||||
|
const beltMode = options.beltMode || false;
|
||||||
|
const beltAngle = options.beltAngle || 0;
|
||||||
|
const supportAngle = options.supportAngle || 45;
|
||||||
|
const supportThreshold = Math.cos((90 - supportAngle) * Math.PI / 180);
|
||||||
|
|
||||||
|
// Store initial paint points reference
|
||||||
|
material.userData.paintPoints = paintPoints;
|
||||||
|
material.userData.paintColor = paintColor;
|
||||||
|
|
||||||
|
material.onBeforeCompile = (shader) => {
|
||||||
|
// Store shader reference for later updates
|
||||||
|
material.userData.shader = shader;
|
||||||
|
|
||||||
|
// Use updated paint points if they were modified before first compile
|
||||||
|
const currentPoints = material.userData.paintPoints || paintPoints;
|
||||||
|
|
||||||
|
// Convert paint points to vec4 array (x, y, z, radius)
|
||||||
|
// Filter out invalid entries
|
||||||
|
const validPoints = currentPoints
|
||||||
|
.filter(p => p && p.point && typeof p.radius === 'number')
|
||||||
|
.map(p => new THREE.Vector4(p.point.x, p.point.y, p.point.z, p.radius));
|
||||||
|
|
||||||
|
// Pad array to exactly 256 elements (shader uniform array size)
|
||||||
|
// THREE.js requires the array length to match the shader declaration
|
||||||
|
const points = new Array(256);
|
||||||
|
for (let i = 0; i < 256; i++) {
|
||||||
|
points[i] = i < validPoints.length ? validPoints[i] : new THREE.Vector4(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add uniforms for paint data
|
||||||
|
shader.uniforms.paintPoints = { value: points };
|
||||||
|
shader.uniforms.paintCount = { value: validPoints.length };
|
||||||
|
shader.uniforms.paintColor = { value: paintColor };
|
||||||
|
shader.uniforms.onlyDownwardFacing = { value: onlyDownwardFacing };
|
||||||
|
shader.uniforms.beltMode = { value: beltMode };
|
||||||
|
shader.uniforms.beltAngle = { value: beltAngle };
|
||||||
|
shader.uniforms.supportThreshold = { value: supportThreshold };
|
||||||
|
|
||||||
|
// Pass model position from vertex shader to fragment shader
|
||||||
|
// Paint points are in widget/model space, so we compare against transformed (model space)
|
||||||
|
shader.vertexShader = shader.vertexShader.replace(
|
||||||
|
`#include <worldpos_vertex>`,
|
||||||
|
`
|
||||||
|
#include <worldpos_vertex>
|
||||||
|
vWorldPosition = vec3(transformed);
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Don't pass normals from vertex shader at all - we'll use fragment shader's normal
|
||||||
|
|
||||||
|
shader.vertexShader = `
|
||||||
|
varying vec3 vWorldPosition;
|
||||||
|
` + shader.vertexShader;
|
||||||
|
|
||||||
|
// Fragment shader: check distance to each paint sphere
|
||||||
|
shader.fragmentShader = `
|
||||||
|
varying vec3 vWorldPosition;
|
||||||
|
uniform vec4 paintPoints[256]; // (x, y, z, radius)
|
||||||
|
uniform int paintCount;
|
||||||
|
uniform vec3 paintColor;
|
||||||
|
uniform bool onlyDownwardFacing;
|
||||||
|
uniform bool beltMode;
|
||||||
|
uniform float beltAngle;
|
||||||
|
uniform float supportThreshold;
|
||||||
|
` + shader.fragmentShader;
|
||||||
|
|
||||||
|
shader.fragmentShader = shader.fragmentShader.replace(
|
||||||
|
`#include <dithering_fragment>`,
|
||||||
|
`
|
||||||
|
#include <dithering_fragment>
|
||||||
|
|
||||||
|
// Compute world-space normal from position derivatives
|
||||||
|
vec3 posDx = dFdx(vWorldPosition);
|
||||||
|
vec3 posDy = dFdy(vWorldPosition);
|
||||||
|
vec3 worldNormal = normalize(cross(posDx, posDy));
|
||||||
|
|
||||||
|
// Only paint downward-facing surfaces if option is enabled
|
||||||
|
bool shouldPaint;
|
||||||
|
if (beltMode) {
|
||||||
|
// Belt mode: mesh is rotated, check if normal points toward world down
|
||||||
|
// World down (0,0,-1) appears as (0, -sin(θ), -cos(θ)) in rotated mesh space
|
||||||
|
// Dot product: normal · (0, -sin(θ), -cos(θ)) = -sin(θ)*ny - cos(θ)*nz
|
||||||
|
float sinAngle = sin(beltAngle);
|
||||||
|
float cosAngle = cos(beltAngle);
|
||||||
|
float rotatedZ = worldNormal.y * sinAngle + worldNormal.z * cosAngle;
|
||||||
|
// Check if within support angle cone from down direction (rotatedZ < -threshold)
|
||||||
|
shouldPaint = !onlyDownwardFacing || rotatedZ < -supportThreshold;
|
||||||
|
} else {
|
||||||
|
// Normal mode: check if normal points downward within support angle cone
|
||||||
|
shouldPaint = !onlyDownwardFacing || worldNormal.z < -supportThreshold;
|
||||||
|
}
|
||||||
|
if (shouldPaint) {
|
||||||
|
// Check if this fragment is within any paint sphere
|
||||||
|
bool painted = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < 256; i++) {
|
||||||
|
if (i >= paintCount) break;
|
||||||
|
|
||||||
|
vec3 center = paintPoints[i].xyz;
|
||||||
|
float radius = paintPoints[i].w;
|
||||||
|
float dist = distance(vWorldPosition, center);
|
||||||
|
|
||||||
|
if (dist <= radius) {
|
||||||
|
painted = true;
|
||||||
|
break; // Early exit once painted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (painted) {
|
||||||
|
// Blend paint color with existing fragment color (increased to 0.7 for visibility)
|
||||||
|
gl_FragColor.rgb = mix(gl_FragColor.rgb, paintColor, 0.7);
|
||||||
|
}
|
||||||
|
} // End normal check
|
||||||
|
|
||||||
|
// DEBUG: Uncomment to tint entire mesh when shader is active (proves shader is running)
|
||||||
|
// if (paintCount > 0) {
|
||||||
|
// gl_FragColor.rgb += vec3(0.3, 0.0, 0.0); // Add red tint to entire mesh
|
||||||
|
// }
|
||||||
|
`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return material;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add paint overlay using texture-based point storage.
|
||||||
|
* Scales to unlimited paint points by encoding them in a DataTexture.
|
||||||
|
*
|
||||||
|
* Each texel in the data texture stores one paint point as RGBA float:
|
||||||
|
* - R channel: x coordinate
|
||||||
|
* - G channel: y coordinate
|
||||||
|
* - B channel: z coordinate
|
||||||
|
* - A channel: radius
|
||||||
|
*
|
||||||
|
* @param {THREE.Material} material - Material to modify (will be mutated via onBeforeCompile)
|
||||||
|
* @param {Array<{point: {x,y,z}, radius: number}>} paintPoints - Array of paint spheres
|
||||||
|
* @param {THREE.Color} [paintColor] - Color for painted regions (default: light blue)
|
||||||
|
* @param {Object} [options] - Optional configuration
|
||||||
|
* @param {boolean} [options.onlyDownwardFacing=true] - Only paint on downward-facing surfaces (z-normal < 0)
|
||||||
|
* @param {boolean} [options.beltMode=false] - Enable belt printer mode (paints surfaces facing down belt slope)
|
||||||
|
* @param {number} [options.beltAngle=0] - Belt angle in radians (only used if beltMode is true)
|
||||||
|
* @param {number} [options.supportAngle=45] - Support angle in degrees (cone around down direction)
|
||||||
|
* @returns {THREE.Material} The modified material
|
||||||
|
*/
|
||||||
|
function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Color(0x4488ff), options = {}) {
|
||||||
|
// Initialize userData if it doesn't exist
|
||||||
|
if (!material.userData) {
|
||||||
|
material.userData = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: only paint downward-facing surfaces (for support painting)
|
||||||
|
const onlyDownwardFacing = options.onlyDownwardFacing !== undefined ? options.onlyDownwardFacing : true;
|
||||||
|
const beltMode = options.beltMode || false;
|
||||||
|
const beltAngle = options.beltAngle || 0;
|
||||||
|
const supportAngle = options.supportAngle || 45;
|
||||||
|
const supportThreshold = Math.cos((90 - supportAngle) * Math.PI / 180);
|
||||||
|
|
||||||
|
// Store initial paint points reference
|
||||||
|
material.userData.paintPoints = paintPoints;
|
||||||
|
material.userData.paintColor = paintColor;
|
||||||
|
|
||||||
|
material.onBeforeCompile = (shader) => {
|
||||||
|
// Store shader reference for later updates
|
||||||
|
material.userData.shader = shader;
|
||||||
|
|
||||||
|
// Use updated paint points if they were modified before first compile
|
||||||
|
const currentPoints = material.userData.paintPoints || paintPoints;
|
||||||
|
|
||||||
|
// Filter out invalid entries
|
||||||
|
const validPoints = currentPoints.filter(p => p && p.point && typeof p.radius === 'number');
|
||||||
|
|
||||||
|
// Create data texture from paint points
|
||||||
|
// Texture is laid out as square grid: width x height >= point count
|
||||||
|
const pointCount = validPoints.length;
|
||||||
|
const texWidth = Math.ceil(Math.sqrt(pointCount));
|
||||||
|
const texHeight = Math.ceil(pointCount / texWidth);
|
||||||
|
const data = new Float32Array(texWidth * texHeight * 4);
|
||||||
|
|
||||||
|
// Pack paint points into texture data
|
||||||
|
// Each point = 4 floats (x, y, z, radius)
|
||||||
|
for (let i = 0; i < validPoints.length; i++) {
|
||||||
|
const p = validPoints[i];
|
||||||
|
data[i * 4 + 0] = p.point.x;
|
||||||
|
data[i * 4 + 1] = p.point.y;
|
||||||
|
data[i * 4 + 2] = p.point.z;
|
||||||
|
data[i * 4 + 3] = p.radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create texture with float data
|
||||||
|
const texture = new THREE.DataTexture(
|
||||||
|
data,
|
||||||
|
texWidth,
|
||||||
|
texHeight,
|
||||||
|
THREE.RGBAFormat,
|
||||||
|
THREE.FloatType
|
||||||
|
);
|
||||||
|
texture.needsUpdate = true;
|
||||||
|
|
||||||
|
// Add uniforms for paint data
|
||||||
|
shader.uniforms.paintTexture = { value: texture };
|
||||||
|
shader.uniforms.paintCount = { value: pointCount };
|
||||||
|
shader.uniforms.paintTexSize = { value: new THREE.Vector2(texWidth, texHeight) };
|
||||||
|
shader.uniforms.paintColor = { value: paintColor };
|
||||||
|
shader.uniforms.onlyDownwardFacing = { value: onlyDownwardFacing };
|
||||||
|
shader.uniforms.beltMode = { value: beltMode };
|
||||||
|
shader.uniforms.beltAngle = { value: beltAngle };
|
||||||
|
shader.uniforms.supportThreshold = { value: supportThreshold };
|
||||||
|
|
||||||
|
// Pass model position from vertex shader to fragment shader
|
||||||
|
// Paint points are in widget/model space, so we compare against transformed (model space)
|
||||||
|
shader.vertexShader = shader.vertexShader.replace(
|
||||||
|
`#include <worldpos_vertex>`,
|
||||||
|
`
|
||||||
|
#include <worldpos_vertex>
|
||||||
|
vWorldPosition = vec3(transformed);
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Don't pass normals from vertex shader at all - we'll use fragment shader's normal
|
||||||
|
|
||||||
|
shader.vertexShader = `
|
||||||
|
varying vec3 vWorldPosition;
|
||||||
|
` + shader.vertexShader;
|
||||||
|
|
||||||
|
// Fragment shader: sample paint points from texture and check distances
|
||||||
|
shader.fragmentShader = `
|
||||||
|
varying vec3 vWorldPosition;
|
||||||
|
uniform sampler2D paintTexture;
|
||||||
|
uniform int paintCount;
|
||||||
|
uniform vec2 paintTexSize;
|
||||||
|
uniform vec3 paintColor;
|
||||||
|
uniform bool onlyDownwardFacing;
|
||||||
|
uniform bool beltMode;
|
||||||
|
uniform float beltAngle;
|
||||||
|
uniform float supportThreshold;
|
||||||
|
` + shader.fragmentShader;
|
||||||
|
|
||||||
|
shader.fragmentShader = shader.fragmentShader.replace(
|
||||||
|
`#include <dithering_fragment>`,
|
||||||
|
`
|
||||||
|
#include <dithering_fragment>
|
||||||
|
|
||||||
|
// Compute world-space normal from position derivatives
|
||||||
|
vec3 posDx = dFdx(vWorldPosition);
|
||||||
|
vec3 posDy = dFdy(vWorldPosition);
|
||||||
|
vec3 worldNormal = normalize(cross(posDx, posDy));
|
||||||
|
|
||||||
|
// Only paint downward-facing surfaces if option is enabled
|
||||||
|
bool shouldPaint;
|
||||||
|
if (beltMode) {
|
||||||
|
// Belt mode: mesh is rotated, check if normal points toward world down
|
||||||
|
// World down (0,0,-1) appears as (0, -sin(θ), -cos(θ)) in rotated mesh space
|
||||||
|
// Dot product: normal · (0, -sin(θ), -cos(θ)) = -sin(θ)*ny - cos(θ)*nz
|
||||||
|
float sinAngle = sin(beltAngle);
|
||||||
|
float cosAngle = cos(beltAngle);
|
||||||
|
float rotatedZ = worldNormal.y * sinAngle + worldNormal.z * cosAngle;
|
||||||
|
// Check if within support angle cone from down direction (rotatedZ < -threshold)
|
||||||
|
shouldPaint = !onlyDownwardFacing || rotatedZ < -supportThreshold;
|
||||||
|
} else {
|
||||||
|
// Normal mode: check if normal points downward within support angle cone
|
||||||
|
shouldPaint = !onlyDownwardFacing || worldNormal.z < -supportThreshold;
|
||||||
|
}
|
||||||
|
if (shouldPaint) {
|
||||||
|
bool painted = false;
|
||||||
|
|
||||||
|
// Iterate through paint points stored in texture
|
||||||
|
// Loop limit must be constant, but we break early when i >= paintCount
|
||||||
|
for (int i = 0; i < 10000; i++) {
|
||||||
|
if (i >= paintCount) break;
|
||||||
|
|
||||||
|
// Calculate texture coordinates for this point index
|
||||||
|
// Points are stored row-major in the texture
|
||||||
|
float u = (float(i % int(paintTexSize.x)) + 0.5) / paintTexSize.x;
|
||||||
|
float v = (float(i / int(paintTexSize.x)) + 0.5) / paintTexSize.y;
|
||||||
|
|
||||||
|
// Sample point data from texture (x, y, z, radius)
|
||||||
|
vec4 pointData = texture2D(paintTexture, vec2(u, v));
|
||||||
|
vec3 center = pointData.xyz;
|
||||||
|
float radius = pointData.w;
|
||||||
|
|
||||||
|
// Check if fragment is within this paint sphere
|
||||||
|
float dist = distance(vWorldPosition, center);
|
||||||
|
|
||||||
|
if (dist <= radius) {
|
||||||
|
painted = true;
|
||||||
|
break; // Early exit once painted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (painted) {
|
||||||
|
// Blend paint color with existing fragment color (increased to 0.7 for visibility)
|
||||||
|
gl_FragColor.rgb = mix(gl_FragColor.rgb, paintColor, 0.7);
|
||||||
|
}
|
||||||
|
} // End normal check
|
||||||
|
|
||||||
|
// DEBUG: Uncomment to tint entire mesh when shader is active (proves shader is running)
|
||||||
|
// if (paintCount > 0) {
|
||||||
|
// gl_FragColor.rgb += vec3(0.3, 0.0, 0.0); // Add red tint to entire mesh
|
||||||
|
// }
|
||||||
|
`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return material;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to choose appropriate paint overlay method based on point count.
|
||||||
|
* Automatically selects simple or scalable version.
|
||||||
|
*
|
||||||
|
* @param {THREE.Material} material - Material to modify
|
||||||
|
* @param {Array<{point: {x,y,z}, radius: number}>} paintPoints - Array of paint spheres
|
||||||
|
* @param {THREE.Color} [paintColor] - Color for painted regions
|
||||||
|
* @param {Object} [options] - Optional configuration
|
||||||
|
* @param {boolean} [options.onlyDownwardFacing=true] - Only paint on downward-facing surfaces (z-normal < 0)
|
||||||
|
* @returns {THREE.Material} The modified material
|
||||||
|
*/
|
||||||
|
function addPaintOverlayAuto(material, paintPoints, paintColor = new THREE.Color(0x4488ff), options = {}) {
|
||||||
|
if (paintPoints.length <= 256) {
|
||||||
|
return addPaintOverlaySimple(material, paintPoints, paintColor, options);
|
||||||
|
} else {
|
||||||
|
return addPaintOverlayTexture(material, paintPoints, paintColor, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update paint overlay with new paint points (for real-time painting).
|
||||||
|
* Call this after adding new points to the paintPoints array.
|
||||||
|
*
|
||||||
|
* This function updates the shader uniforms without recompiling the shader,
|
||||||
|
* making it suitable for real-time updates during mouse drag painting.
|
||||||
|
*
|
||||||
|
* IMPORTANT: The material must have already been initialized with one of the
|
||||||
|
* addPaintOverlay functions before calling this update function.
|
||||||
|
*
|
||||||
|
* NOTE: If the shader hasn't been compiled yet (first render not complete),
|
||||||
|
* this will trigger a material update to force recompilation with new points.
|
||||||
|
*
|
||||||
|
* @param {THREE.Material} material - Material with paint shader already applied
|
||||||
|
* @param {Array<{point: {x,y,z}, radius: number}>} paintPoints - Updated array of paint spheres
|
||||||
|
*/
|
||||||
|
function updatePaintOverlay(material, paintPoints) {
|
||||||
|
// Initialize userData if it doesn't exist
|
||||||
|
if (!material.userData) {
|
||||||
|
material.userData = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store paint points reference in userData for access during shader compilation
|
||||||
|
if (!material.userData.paintPoints) {
|
||||||
|
material.userData.paintPoints = paintPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Access the compiled shader uniforms
|
||||||
|
// After onBeforeCompile runs once, userData.shader contains the compiled program
|
||||||
|
const shader = material.userData?.shader;
|
||||||
|
|
||||||
|
if (!shader || !shader.uniforms) {
|
||||||
|
// Shader hasn't compiled yet (first render not complete)
|
||||||
|
// Store the new points and trigger recompile
|
||||||
|
material.userData.paintPoints = paintPoints;
|
||||||
|
material.needsUpdate = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine which shader type based on available uniforms
|
||||||
|
if (shader.uniforms.paintPoints) {
|
||||||
|
// Simple uniform-based shader
|
||||||
|
updatePaintOverlaySimple(shader, paintPoints);
|
||||||
|
} else if (shader.uniforms.paintTexture) {
|
||||||
|
// Texture-based shader
|
||||||
|
updatePaintOverlayTexture(shader, paintPoints);
|
||||||
|
} else {
|
||||||
|
console.warn('updatePaintOverlay: unrecognized paint shader type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update simple uniform-based paint shader.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function updatePaintOverlaySimple(shader, paintPoints) {
|
||||||
|
const uniforms = shader.uniforms;
|
||||||
|
|
||||||
|
// Convert paint points to vec4 array (x, y, z, radius)
|
||||||
|
// Filter out invalid entries
|
||||||
|
const validPoints = paintPoints
|
||||||
|
.filter(p => p && p.point && typeof p.radius === 'number')
|
||||||
|
.map(p => new THREE.Vector4(p.point.x, p.point.y, p.point.z, p.radius));
|
||||||
|
|
||||||
|
// Pad array to exactly 256 elements (shader uniform array size)
|
||||||
|
const points = new Array(256);
|
||||||
|
for (let i = 0; i < 256; i++) {
|
||||||
|
points[i] = i < validPoints.length ? validPoints[i] : new THREE.Vector4(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update uniform values
|
||||||
|
uniforms.paintPoints.value = points;
|
||||||
|
uniforms.paintCount.value = validPoints.length;
|
||||||
|
|
||||||
|
// Note: No need to set needsUpdate on the material
|
||||||
|
// Uniform updates are automatically detected by THREE.js
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update texture-based paint shader.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function updatePaintOverlayTexture(shader, paintPoints) {
|
||||||
|
const uniforms = shader.uniforms;
|
||||||
|
|
||||||
|
// Filter out invalid entries
|
||||||
|
const validPoints = paintPoints.filter(p => p && p.point && typeof p.radius === 'number');
|
||||||
|
|
||||||
|
// Recreate texture with new paint points
|
||||||
|
const pointCount = validPoints.length;
|
||||||
|
const texWidth = Math.ceil(Math.sqrt(pointCount));
|
||||||
|
const texHeight = Math.ceil(pointCount / texWidth);
|
||||||
|
const data = new Float32Array(texWidth * texHeight * 4);
|
||||||
|
|
||||||
|
// Pack paint points into texture data
|
||||||
|
for (let i = 0; i < validPoints.length; i++) {
|
||||||
|
const p = validPoints[i];
|
||||||
|
data[i * 4 + 0] = p.point.x;
|
||||||
|
data[i * 4 + 1] = p.point.y;
|
||||||
|
data[i * 4 + 2] = p.point.z;
|
||||||
|
data[i * 4 + 3] = p.radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new texture
|
||||||
|
const texture = new THREE.DataTexture(
|
||||||
|
data,
|
||||||
|
texWidth,
|
||||||
|
texHeight,
|
||||||
|
THREE.RGBAFormat,
|
||||||
|
THREE.FloatType
|
||||||
|
);
|
||||||
|
texture.needsUpdate = true;
|
||||||
|
|
||||||
|
// Dispose old texture to prevent memory leak
|
||||||
|
if (uniforms.paintTexture.value) {
|
||||||
|
uniforms.paintTexture.value.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update uniform values
|
||||||
|
uniforms.paintTexture.value = texture;
|
||||||
|
uniforms.paintCount.value = pointCount;
|
||||||
|
uniforms.paintTexSize.value.set(texWidth, texHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
addPaintOverlaySimple,
|
||||||
|
addPaintOverlayTexture,
|
||||||
|
addPaintOverlayAuto,
|
||||||
|
updatePaintOverlay
|
||||||
|
};
|
||||||
|
|
@ -1,32 +1,65 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
import { $, h } from '../../moto/webui.js';
|
import { $, h } from '../../moto/webui.js';
|
||||||
|
import { ajax, js2o } from '../core/utils.js';
|
||||||
import { api } from './api.js';
|
import { api } from './api.js';
|
||||||
import { ajax, js2o } from './utils.js';
|
import { MODES } from './consts.js';
|
||||||
import { base } from '../../geo/base.js';
|
import { colorSchemeRegistry } from './color/schemes.js';
|
||||||
import { load } from '../../load/file.js';
|
import { load as file_load } from '../../load/file.js';
|
||||||
import { space } from '../../moto/space.js';
|
import { newBounds } from '../../geo/bounds.js';
|
||||||
import { Widget, newWidget } from './widget.js';
|
|
||||||
import { Packer } from './pack.js';
|
import { Packer } from './pack.js';
|
||||||
|
import { space } from '../../moto/space.js';
|
||||||
import { COLOR, MODES } from './consts.js';
|
|
||||||
import { THREE } from '../../ext/three.js';
|
import { THREE } from '../../ext/three.js';
|
||||||
|
import { Widget, newWidget } from './widget.js';
|
||||||
|
|
||||||
const V0 = new THREE.Vector3(0,0,0);
|
const V0 = new THREE.Vector3(0,0,0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current color scheme
|
||||||
|
*/
|
||||||
|
function getColorScheme() {
|
||||||
|
const mode = api.mode.get_id();
|
||||||
|
const theme = api.space.is_dark() ? 'dark' : 'light';
|
||||||
|
return colorSchemeRegistry.getScheme(mode, theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bounds update callback */
|
||||||
let setbounds = undefined;
|
let setbounds = undefined;
|
||||||
|
|
||||||
|
/** Flag indicating if grouping operation is in progress */
|
||||||
let grouping = false;
|
let grouping = false;
|
||||||
|
|
||||||
|
/** Maximum Z height of all widgets on platform */
|
||||||
let topZ = 0;
|
let topZ = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current settings object.
|
||||||
|
* @returns {object} Current configuration
|
||||||
|
*/
|
||||||
function current() {
|
function current() {
|
||||||
return api.conf.get();
|
return api.conf.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current mode ID constant.
|
||||||
|
* @returns {number} Mode ID
|
||||||
|
*/
|
||||||
function get_mode() {
|
function get_mode() {
|
||||||
return api.mode.get_id();
|
return api.mode.get_id();
|
||||||
}
|
}
|
||||||
|
|
||||||
function platformUpdateOrigin(update_bounds = true) {
|
/**
|
||||||
|
* Update platform origin position and rulers.
|
||||||
|
* Calculates origin based on mode, device, and process settings:
|
||||||
|
* - FDM: Corner or center based on device.originCenter/bedRound
|
||||||
|
* - CAM: Relative to stock, with optional top origin
|
||||||
|
* - 2D modes (LASER, DRAG, WJET, WEDM): Corner, center, or bounds-based
|
||||||
|
* - Belt: Special Y offset handling
|
||||||
|
* Applies mode-specific origin offsets and updates visual rulers.
|
||||||
|
*
|
||||||
|
* @param {boolean} [update_bounds=true] - Whether to recalculate bounds first
|
||||||
|
*/
|
||||||
|
function update_origin(update_bounds = true) {
|
||||||
if (update_bounds) {
|
if (update_bounds) {
|
||||||
platform.update_bounds();
|
platform.update_bounds();
|
||||||
}
|
}
|
||||||
|
|
@ -106,7 +139,14 @@ function platformUpdateOrigin(update_bounds = true) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function platformUpdateSize(updateDark = true) {
|
/**
|
||||||
|
* Update platform size and visual appearance.
|
||||||
|
* Sets platform dimensions from device settings, updates grid/rulers,
|
||||||
|
* and applies dark mode styling if enabled.
|
||||||
|
*
|
||||||
|
* @param {boolean} [updateDark=true] - Whether to update dark mode styling
|
||||||
|
*/
|
||||||
|
function update_size(updateDark = true) {
|
||||||
const { process, device, controller } = current();
|
const { process, device, controller } = current();
|
||||||
const { showRulers, units } = controller;
|
const { showRulers, units } = controller;
|
||||||
|
|
||||||
|
|
@ -124,17 +164,18 @@ function platformUpdateSize(updateDark = true) {
|
||||||
gridMinor = unitMM ? 5 : 25.4 / 10;
|
gridMinor = unitMM ? 5 : 25.4 / 10;
|
||||||
|
|
||||||
if (updateDark) {
|
if (updateDark) {
|
||||||
|
const scheme = getColorScheme();
|
||||||
if (controller.dark) {
|
if (controller.dark) {
|
||||||
space.platform.set({ light: 0.08 });
|
space.platform.set({ light: 0.08 });
|
||||||
space.platform.setFont({rulerColor:'#888888'});
|
space.platform.setFont({rulerColor:'#888888'});
|
||||||
space.platform.setGrid(gridMajor, gridMinor, 0x666666, 0x333333);
|
space.platform.setGrid(gridMajor, gridMinor, scheme.grid.major, scheme.grid.minor);
|
||||||
space.platform.opacity(0.05);
|
space.platform.opacity(0.05);
|
||||||
space.sky.set({ color: 0, ambient: { intensity: 0.6 } });
|
space.sky.set({ color: 0, ambient: { intensity: 0.6 } });
|
||||||
document.body.classList.add('dark');
|
document.body.classList.add('dark');
|
||||||
} else {
|
} else {
|
||||||
space.platform.set({ light: 0.08 });
|
space.platform.set({ light: 0.08 });
|
||||||
space.platform.setFont({rulerColor:'#333333'});
|
space.platform.setFont({rulerColor:'#333333'});
|
||||||
space.platform.setGrid(gridMajor, gridMinor, 0x999999, 0xcccccc);
|
space.platform.setGrid(gridMajor, gridMinor, scheme.grid.major, scheme.grid.minor);
|
||||||
space.platform.opacity(0.2);
|
space.platform.opacity(0.2);
|
||||||
space.sky.set({ color: 0xffffff, ambient: { intensity: 1.1 } });
|
space.sky.set({ color: 0xffffff, ambient: { intensity: 1.1 } });
|
||||||
document.body.classList.remove('dark');
|
document.body.classList.remove('dark');
|
||||||
|
|
@ -146,6 +187,10 @@ function platformUpdateSize(updateDark = true) {
|
||||||
platform.update_origin();
|
platform.update_origin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update topZ to track maximum Z height of all widgets.
|
||||||
|
* Used for belt mode and other Z-dependent calculations.
|
||||||
|
*/
|
||||||
function platformUpdateMidZ() {
|
function platformUpdateMidZ() {
|
||||||
topZ = 0;
|
topZ = 0;
|
||||||
api.widgets.each(widget => {
|
api.widgets.each(widget => {
|
||||||
|
|
@ -154,7 +199,12 @@ function platformUpdateMidZ() {
|
||||||
space.platform.setMaxZ(topZ);
|
space.platform.setMaxZ(topZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
function platformUpdateTopZ() {
|
/**
|
||||||
|
* Update widget Z positioning based on CAM anchoring settings.
|
||||||
|
* In CAM mode, anchors widgets to top/middle/bottom of stock with offset.
|
||||||
|
* In other modes, resets topZ to 0.
|
||||||
|
*/
|
||||||
|
function update_top_z() {
|
||||||
const { process, stock } = current();
|
const { process, stock } = current();
|
||||||
const MODE = get_mode();
|
const MODE = get_mode();
|
||||||
api.widgets.each(widget => {
|
api.widgets.each(widget => {
|
||||||
|
|
@ -183,6 +233,13 @@ function platformUpdateTopZ() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate and update stock dimensions for CAM mode.
|
||||||
|
* Stock dimensions can be absolute or relative (offset from bounds).
|
||||||
|
* Falls back to offset mode if any stock dimension is 0.
|
||||||
|
* Calculates stock center point for origin calculations.
|
||||||
|
* In non-CAM modes, clears stock object.
|
||||||
|
*/
|
||||||
function platformUpdateStock() {
|
function platformUpdateStock() {
|
||||||
const settings = current();
|
const settings = current();
|
||||||
const { bounds, process, mode } = settings;
|
const { bounds, process, mode } = settings;
|
||||||
|
|
@ -209,12 +266,23 @@ function platformUpdateStock() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function platformSetBounds(bounds) {
|
/**
|
||||||
|
* Set explicit platform bounds and trigger update.
|
||||||
|
* @param {THREE.Box3} bounds - Bounding box to set
|
||||||
|
* @returns {THREE.Box3} Updated bounds
|
||||||
|
*/
|
||||||
|
function set_bounds(bounds) {
|
||||||
setbounds = bounds;
|
setbounds = bounds;
|
||||||
return platformUpdateBounds();
|
return update_bounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
function platformUpdateBounds() {
|
/**
|
||||||
|
* Calculate platform bounds from all widgets or use explicit bounds.
|
||||||
|
* Unions all widget bounding boxes translated by their positions.
|
||||||
|
* Updates stock, top Z, midZ, and origin after calculation.
|
||||||
|
* @returns {THREE.Box3} Calculated or explicit bounds
|
||||||
|
*/
|
||||||
|
function update_bounds() {
|
||||||
const bounds = setbounds || new THREE.Box3();
|
const bounds = setbounds || new THREE.Box3();
|
||||||
if (!setbounds)
|
if (!setbounds)
|
||||||
api.widgets.each(widget => {
|
api.widgets.each(widget => {
|
||||||
|
|
@ -231,17 +299,26 @@ function platformUpdateBounds() {
|
||||||
}
|
}
|
||||||
current().bounds = bounds;
|
current().bounds = bounds;
|
||||||
platformUpdateStock();
|
platformUpdateStock();
|
||||||
platformUpdateTopZ();
|
update_top_z();
|
||||||
platformUpdateMidZ();
|
platformUpdateMidZ();
|
||||||
platformUpdateOrigin(false);
|
update_origin(false);
|
||||||
return bounds;
|
return bounds;
|
||||||
}
|
}
|
||||||
|
|
||||||
function platformSelectedCount() {
|
/**
|
||||||
|
* Get count of selected widgets in arrange view.
|
||||||
|
* @returns {number} Selection count (0 if not in arrange view)
|
||||||
|
*/
|
||||||
|
function selected_count() {
|
||||||
return api.view.is_arrange() ? api.selection.count() : 0;
|
return api.view.is_arrange() ? api.selection.count() : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function platformUpdateSelected() {
|
/**
|
||||||
|
* Update visual appearance of selected widgets.
|
||||||
|
* Sets selected color and highlights extruder buttons for FDM multi-extruder devices.
|
||||||
|
* Saves widget state after selection.
|
||||||
|
*/
|
||||||
|
function update_selected() {
|
||||||
const settings = current();
|
const settings = current();
|
||||||
|
|
||||||
const { device } = settings;
|
const { device } = settings;
|
||||||
|
|
@ -253,21 +330,32 @@ function platformUpdateSelected() {
|
||||||
let b = $(`sel-ext-${i}`);
|
let b = $(`sel-ext-${i}`);
|
||||||
if (b) b.classList.remove('pop-sel');
|
if (b) b.classList.remove('pop-sel');
|
||||||
}
|
}
|
||||||
|
const scheme = getColorScheme();
|
||||||
selection.for_widgets(w => {
|
selection.for_widgets(w => {
|
||||||
w.setColor(COLOR.selected);
|
w.setColor(scheme.widget.selected);
|
||||||
let ext = api.widgets.annotate(w.id).extruder || 0;
|
let ext = api.widgets.annotate(w.id).extruder || 0;
|
||||||
let b = $(`sel-ext-${ext}`);
|
let b = $(`sel-ext-${ext}`);
|
||||||
if (b) b.classList.add('pop-sel');
|
if (b) b.classList.add('pop-sel');
|
||||||
w.saveState();
|
w.saveState();
|
||||||
}, true);
|
}, true);
|
||||||
} else {
|
} else {
|
||||||
|
const scheme = getColorScheme();
|
||||||
selection.for_widgets(w => {
|
selection.for_widgets(w => {
|
||||||
w.setColor(COLOR.selected);
|
w.setColor(scheme.widget.selected);
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function platformSelect(widget, shift, recurse = true) {
|
/**
|
||||||
|
* Select a widget on the platform.
|
||||||
|
* Only works in arrange view. Handles group selection recursively.
|
||||||
|
* With shift key, toggles selection. Without shift, replaces selection.
|
||||||
|
* Emits 'widget.select' event and updates UI.
|
||||||
|
* @param {Widget} widget - Widget to select
|
||||||
|
* @param {boolean} shift - Whether shift key is pressed (multi-select)
|
||||||
|
* @param {boolean} [recurse=true] - Whether to recursively select group members
|
||||||
|
*/
|
||||||
|
function select(widget, shift, recurse = true) {
|
||||||
const { event, selection, view } = api;
|
const { event, selection, view } = api;
|
||||||
|
|
||||||
if (!view.is_arrange()) {
|
if (!view.is_arrange()) {
|
||||||
|
|
@ -277,7 +365,7 @@ function platformSelect(widget, shift, recurse = true) {
|
||||||
// apply select to entire group
|
// apply select to entire group
|
||||||
if (recurse && widget && widget.group.length > 1) {
|
if (recurse && widget && widget.group.length > 1) {
|
||||||
for (let w of widget.group) {
|
for (let w of widget.group) {
|
||||||
platformSelect(w, true, false);
|
select(w, true, false);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -299,7 +387,8 @@ function platformSelect(widget, shift, recurse = true) {
|
||||||
}
|
}
|
||||||
selection.add(widget);
|
selection.add(widget);
|
||||||
event.emit('widget.select', widget);
|
event.emit('widget.select', widget);
|
||||||
widget.setColor(COLOR.selected);
|
const scheme = getColorScheme();
|
||||||
|
widget.setColor(scheme.widget.selected);
|
||||||
$(`ws-${widget.id}`)?.classList.add('selected');
|
$(`ws-${widget.id}`)?.classList.add('selected');
|
||||||
selection.update_info();
|
selection.update_info();
|
||||||
}
|
}
|
||||||
|
|
@ -308,7 +397,15 @@ function platformSelect(widget, shift, recurse = true) {
|
||||||
space.update();
|
space.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
function platformDeselect(widget, recurse = true) {
|
/**
|
||||||
|
* Deselect widget(s) on the platform.
|
||||||
|
* Only works in arrange view. Handles group deselection recursively.
|
||||||
|
* If no widget provided, deselects all widgets.
|
||||||
|
* Emits 'widget.deselect' event and updates UI.
|
||||||
|
* @param {Widget} [widget] - Widget to deselect, or undefined to deselect all
|
||||||
|
* @param {boolean} [recurse=true] - Whether to recursively deselect group members
|
||||||
|
*/
|
||||||
|
function deselect(widget, recurse = true) {
|
||||||
const { selection, view } = api;
|
const { selection, view } = api;
|
||||||
|
|
||||||
if (!view.is_arrange()) {
|
if (!view.is_arrange()) {
|
||||||
|
|
@ -320,7 +417,7 @@ function platformDeselect(widget, recurse = true) {
|
||||||
// apply deselect to entire group
|
// apply deselect to entire group
|
||||||
if (recurse && widget && widget.group.length > 1) {
|
if (recurse && widget && widget.group.length > 1) {
|
||||||
for (let w of widget.group) {
|
for (let w of widget.group) {
|
||||||
platformDeselect(w, false);
|
deselect(w, false);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -337,13 +434,20 @@ function platformDeselect(widget, recurse = true) {
|
||||||
}
|
}
|
||||||
|
|
||||||
$(`ws-${widget.id}`)?.classList.remove('selected');
|
$(`ws-${widget.id}`)?.classList.remove('selected');
|
||||||
widget.setColor(COLOR.deselected);
|
const scheme = getColorScheme();
|
||||||
|
widget.setColor(scheme.widget.deselected);
|
||||||
platform.update_selected();
|
platform.update_selected();
|
||||||
selection.update_info();
|
selection.update_info();
|
||||||
space.update();
|
space.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
function platformLoad(url, onload) {
|
/**
|
||||||
|
* Load mesh from URL (.stl or raw vertex data).
|
||||||
|
* Detects file type and delegates to load_stl or ajax loader.
|
||||||
|
* @param {string} url - URL to load from
|
||||||
|
* @param {Function} [onload] - Callback(vertices, widget) on load complete
|
||||||
|
*/
|
||||||
|
function load(url, onload) {
|
||||||
if (url.toLowerCase().indexOf(".stl") > 0) {
|
if (url.toLowerCase().indexOf(".stl") > 0) {
|
||||||
platform.load_stl(url, onload);
|
platform.load_stl(url, onload);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -357,8 +461,16 @@ function platformLoad(url, onload) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function platformLoadSTL(url, onload, formdata, credentials, headers) {
|
/**
|
||||||
new load.STL().load(url, (vertices, filename) => {
|
* Load STL file from URL and add to platform.
|
||||||
|
* @param {string} url - URL to STL file
|
||||||
|
* @param {Function} [onload] - Callback(vertices, widget) on load complete
|
||||||
|
* @param {FormData} [formdata] - Optional form data for POST request
|
||||||
|
* @param {boolean} [credentials] - Include credentials in request
|
||||||
|
* @param {object} [headers] - Additional HTTP headers
|
||||||
|
*/
|
||||||
|
function load_stl(url, onload, formdata, credentials, headers) {
|
||||||
|
new file_load.STL().load(url, (vertices, filename) => {
|
||||||
if (vertices) {
|
if (vertices) {
|
||||||
let widget = newWidget().loadVertices(vertices);
|
let widget = newWidget().loadVertices(vertices);
|
||||||
widget.meta.file = filename;
|
widget.meta.file = filename;
|
||||||
|
|
@ -370,9 +482,16 @@ function platformLoadSTL(url, onload, formdata, credentials, headers) {
|
||||||
}, formdata, 1 / api.view.unit_scale(), credentials, headers);
|
}, formdata, 1 / api.view.unit_scale(), credentials, headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
function platformLoadURL(url, options = {}) {
|
/**
|
||||||
|
* Load mesh(es) from URL via file loader.
|
||||||
|
* Supports multiple file formats. Groups loading to prevent intermediate layouts.
|
||||||
|
* Emits 'load.url' event with loaded widgets.
|
||||||
|
* @param {string} url - URL to load from
|
||||||
|
* @param {object} [options={}] - Load options including optional group array
|
||||||
|
*/
|
||||||
|
function load_url(url, options = {}) {
|
||||||
platform.group();
|
platform.group();
|
||||||
load.URL.load(url, options).then(objects => {
|
file_load.URL.load(url, options).then(objects => {
|
||||||
let widgets = [];
|
let widgets = [];
|
||||||
for (let object of objects) {
|
for (let object of objects) {
|
||||||
let widget = newWidget(undefined, options.group).loadVertices(object.mesh);
|
let widget = newWidget(undefined, options.group).loadVertices(object.mesh);
|
||||||
|
|
@ -391,12 +510,21 @@ function platformLoadURL(url, options = {}) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function platformGroup() {
|
/**
|
||||||
|
* Begin group loading mode.
|
||||||
|
* Defers layout and group position updates until group_done() is called.
|
||||||
|
*/
|
||||||
|
function group() {
|
||||||
grouping = true;
|
grouping = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// called after all new widgets are loaded to update group positions
|
/**
|
||||||
function platformGroupDone(skipLayout) {
|
* Complete group loading and finalize widget positions.
|
||||||
|
* Called after all widgets in a group are loaded.
|
||||||
|
* Triggers layout if drop_layout feature is enabled.
|
||||||
|
* @param {boolean} [skipLayout] - Whether to skip auto-layout
|
||||||
|
*/
|
||||||
|
function group_done(skipLayout) {
|
||||||
grouping = false;
|
grouping = false;
|
||||||
Widget.Groups.loadDone();
|
Widget.Groups.loadDone();
|
||||||
if (api.feature.drop_layout && !skipLayout) {
|
if (api.feature.drop_layout && !skipLayout) {
|
||||||
|
|
@ -404,10 +532,24 @@ function platformGroupDone(skipLayout) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Deferred widget additions pending batch processing */
|
||||||
let deferred = [];
|
let deferred = [];
|
||||||
|
|
||||||
|
/** Timeout handle for deferred widget batch processing */
|
||||||
let deferTimeout;
|
let deferTimeout;
|
||||||
|
|
||||||
function platformAdd(widget, shift, nolayout, defer) {
|
/**
|
||||||
|
* Add widget to platform and 3D scene.
|
||||||
|
* Can defer addition for batch processing to improve performance.
|
||||||
|
* Initializes widget annotation (extruder assignment).
|
||||||
|
* Updates bounds, triggers save, and positions widget if autoLayout is disabled.
|
||||||
|
* Emits 'widget.add' event.
|
||||||
|
* @param {Widget} widget - Widget to add
|
||||||
|
* @param {boolean} shift - Whether to multi-select (shift key pressed)
|
||||||
|
* @param {boolean} nolayout - Skip layout/positioning
|
||||||
|
* @param {boolean} defer - Batch multiple adds for better performance
|
||||||
|
*/
|
||||||
|
function add(widget, shift, nolayout, defer) {
|
||||||
api.widgets.add(widget);
|
api.widgets.add(widget);
|
||||||
space.world.add(widget.mesh);
|
space.world.add(widget.mesh);
|
||||||
widget.anno = widget.anno || {};
|
widget.anno = widget.anno || {};
|
||||||
|
|
@ -420,13 +562,15 @@ function platformAdd(widget, shift, nolayout, defer) {
|
||||||
platform.select(widget, shift);
|
platform.select(widget, shift);
|
||||||
platform.update_bounds();
|
platform.update_bounds();
|
||||||
api.space.auto_save();
|
api.space.auto_save();
|
||||||
platformChanged();
|
changed();
|
||||||
api.event.emit('widget.add', widget);
|
api.event.emit('widget.add', widget);
|
||||||
|
// Apply saved visual state AFTER selection/color to avoid being overwritten
|
||||||
|
api.visuals.apply_visual_state(widget);
|
||||||
if (nolayout) {
|
if (nolayout) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!grouping) {
|
if (!grouping) {
|
||||||
platformGroupDone(nolayout);
|
group_done(nolayout);
|
||||||
if (!current().controller.autoLayout) {
|
if (!current().controller.autoLayout) {
|
||||||
positionNewWidget(widget);
|
positionNewWidget(widget);
|
||||||
}
|
}
|
||||||
|
|
@ -434,6 +578,12 @@ function platformAdd(widget, shift, nolayout, defer) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process batch of deferred widget additions.
|
||||||
|
* Called after 150ms timeout to batch multiple rapid additions.
|
||||||
|
* Positions widgets if autoLayout is disabled, then completes grouping.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
function platformAddDeferred() {
|
function platformAddDeferred() {
|
||||||
let skiplayout = false;
|
let skiplayout = false;
|
||||||
for (let rec of deferred) {
|
for (let rec of deferred) {
|
||||||
|
|
@ -445,15 +595,27 @@ function platformAddDeferred() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!grouping) {
|
if (!grouping) {
|
||||||
platformGroupDone(skiplayout);
|
group_done(skiplayout);
|
||||||
}
|
}
|
||||||
api.event.emit('widget.add', deferred.map(r => r.widget));
|
api.event.emit('widget.add', deferred.map(r => r.widget));
|
||||||
|
// Apply visual state to all deferred widgets after they're positioned
|
||||||
|
for (let rec of deferred) {
|
||||||
|
api.visuals.apply_visual_state(rec.widget);
|
||||||
|
}
|
||||||
platform.update_bounds();
|
platform.update_bounds();
|
||||||
api.space.auto_save();
|
api.space.auto_save();
|
||||||
platformChanged();
|
changed();
|
||||||
deferred = [];
|
deferred = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find non-colliding position for newly added widget.
|
||||||
|
* Uses spiral search pattern radiating from center (0,0).
|
||||||
|
* Tests 360 positions at each radius (10, 20, 30...200mm).
|
||||||
|
* Skips if this is the first widget on platform.
|
||||||
|
* @param {Widget} widget - Widget to position
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
function positionNewWidget(widget) {
|
function positionNewWidget(widget) {
|
||||||
if (api.widgets.count() <= 1) {
|
if (api.widgets.count() <= 1) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -470,8 +632,8 @@ function positionNewWidget(widget) {
|
||||||
x: dim.x / 2,
|
x: dim.x / 2,
|
||||||
y: dim.y / 2
|
y: dim.y / 2
|
||||||
};
|
};
|
||||||
const bounds = base.newBounds();
|
const bounds = newBounds();
|
||||||
const target = base.newBounds();
|
const target = newBounds();
|
||||||
const DEG2RAD = Math.PI / 180;
|
const DEG2RAD = Math.PI / 180;
|
||||||
const WIDGETS = api.widgets.all();
|
const WIDGETS = api.widgets.all();
|
||||||
// look for best position for new widget that doesn't collide
|
// look for best position for new widget that doesn't collide
|
||||||
|
|
@ -502,6 +664,15 @@ function positionNewWidget(widget) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete widget(s) from platform.
|
||||||
|
* Handles single widget, array of widgets, or widget records.
|
||||||
|
* Removes from API widget collection, selection, groups, and 3D scene.
|
||||||
|
* Can defer post-processing for batch deletions.
|
||||||
|
* Emits 'widget.delete' event.
|
||||||
|
* @param {Widget|Array<Widget>} widget - Widget(s) to delete
|
||||||
|
* @param {boolean} [defer] - Skip post-processing (for batch operations)
|
||||||
|
*/
|
||||||
function platformDelete(widget, defer) {
|
function platformDelete(widget, defer) {
|
||||||
if (!widget) {
|
if (!widget) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -511,7 +682,7 @@ function platformDelete(widget, defer) {
|
||||||
for (let i = 0; i < mc.length; i++) {
|
for (let i = 0; i < mc.length; i++) {
|
||||||
platform.delete(mc[i].widget || mc[i], true);
|
platform.delete(mc[i].widget || mc[i], true);
|
||||||
}
|
}
|
||||||
platformDeletePost();
|
delete_post();
|
||||||
api.event.emit('widget.delete', widget);
|
api.event.emit('widget.delete', widget);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -521,12 +692,17 @@ function platformDelete(widget, defer) {
|
||||||
Widget.Groups.remove(widget);
|
Widget.Groups.remove(widget);
|
||||||
space.world.remove(widget.mesh);
|
space.world.remove(widget.mesh);
|
||||||
if (!defer) {
|
if (!defer) {
|
||||||
platformDeletePost();
|
delete_post();
|
||||||
api.event.emit('widget.delete', widget);
|
api.event.emit('widget.delete', widget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function platformDeletePost() {
|
/**
|
||||||
|
* Post-deletion cleanup and updates.
|
||||||
|
* Updates slider, bounds, layout, selection, triggers save.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function delete_post() {
|
||||||
api.view.update_slider_max();
|
api.view.update_slider_max();
|
||||||
platform.update_bounds();
|
platform.update_bounds();
|
||||||
if (get_mode() !== MODES.FDM) {
|
if (get_mode() !== MODES.FDM) {
|
||||||
|
|
@ -538,11 +714,15 @@ function platformDeletePost() {
|
||||||
platform.layout();
|
platform.layout();
|
||||||
}
|
}
|
||||||
api.space.auto_save();
|
api.space.auto_save();
|
||||||
platformChanged();
|
changed();
|
||||||
}
|
}
|
||||||
|
|
||||||
// render list of current widgets
|
/**
|
||||||
function platformChanged() {
|
* Render widget list UI with action buttons.
|
||||||
|
* Creates interactive widget cards with save/rename/replace/disable/delete buttons.
|
||||||
|
* Highlights widgets on hover. Updates when widgets are added/removed/modified.
|
||||||
|
*/
|
||||||
|
function changed() {
|
||||||
h.bind($('ws-widgets'), api.widgets.all().map(w => {
|
h.bind($('ws-widgets'), api.widgets.all().map(w => {
|
||||||
let color;
|
let color;
|
||||||
let file = w.meta.file || 'no name';
|
let file = w.meta.file || 'no name';
|
||||||
|
|
@ -588,8 +768,9 @@ function platformChanged() {
|
||||||
onclick() {
|
onclick() {
|
||||||
let dis = w.meta.disabled = !w.meta.disabled;
|
let dis = w.meta.disabled = !w.meta.disabled;
|
||||||
let sel = api.selection.contains(w);
|
let sel = api.selection.contains(w);
|
||||||
|
const scheme = getColorScheme();
|
||||||
api.uc.setClass($(`w-en-${w.id}`), "disabled", dis);
|
api.uc.setClass($(`w-en-${w.id}`), "disabled", dis);
|
||||||
w.setColor(sel ? COLOR.selected : COLOR.deselected, null, false);
|
w.setColor(sel ? scheme.widget.selected : scheme.widget.deselected, null, false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[ h.i({ class:"fas fa-ban" }) ]
|
[ h.i({ class:"fas fa-ban" }) ]
|
||||||
|
|
@ -619,7 +800,7 @@ function platformChanged() {
|
||||||
// w.setColor(color, null, false);
|
// w.setColor(color, null, false);
|
||||||
// },
|
// },
|
||||||
onclick() {
|
onclick() {
|
||||||
platformSelect(w, true, false);
|
select(w, true, false);
|
||||||
color = w.getColor();
|
color = w.getColor();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -628,13 +809,37 @@ function platformChanged() {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function platformSelectAll() {
|
/**
|
||||||
|
* Select all widgets on platform.
|
||||||
|
* Adds each widget to selection without recursing into groups.
|
||||||
|
*/
|
||||||
|
function select_all() {
|
||||||
api.widgets.each(widget => {
|
api.widgets.each(widget => {
|
||||||
platform.select(widget, true, false)
|
platform.select(widget, true, false)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function platformLayout() {
|
/**
|
||||||
|
* Arrange widgets on platform using automatic layout.
|
||||||
|
* Behavior varies by mode:
|
||||||
|
* - FDM: 2D bin packing with support spacing. Belt mode uses linear Y layout with optional X randomization.
|
||||||
|
* - SLA: 2D bin packing with support spacing
|
||||||
|
* - CAM/LASER: 2D bin packing with configurable tile spacing
|
||||||
|
*
|
||||||
|
* Belt mode special handling:
|
||||||
|
* - Positions widgets linearly along Y axis
|
||||||
|
* - Adds belt lead spacing
|
||||||
|
* - Optional X randomization for better adhesion
|
||||||
|
* - Auto-expands bed depth if needed
|
||||||
|
*
|
||||||
|
* Standard mode:
|
||||||
|
* - Uses 2D bin packing algorithm (Packer class)
|
||||||
|
* - Grows packing area by 10% if widgets don't fit
|
||||||
|
* - Centers packed layout on platform
|
||||||
|
*
|
||||||
|
* Emits 'platform.layout' event when complete.
|
||||||
|
*/
|
||||||
|
function layout() {
|
||||||
const MODE = get_mode();
|
const MODE = get_mode();
|
||||||
const settings = current();
|
const settings = current();
|
||||||
const { process, device, controller } = settings;
|
const { process, device, controller } = settings;
|
||||||
|
|
@ -659,7 +864,7 @@ function platformLayout() {
|
||||||
gap = gap || ((process.sliceSupportExtra || 0) * 2) + 1;
|
gap = gap || ((process.sliceSupportExtra || 0) * 2) + 1;
|
||||||
// auto resize device to support a larger object
|
// auto resize device to support a larger object
|
||||||
if (isBelt) {
|
if (isBelt) {
|
||||||
fitDeviceToWidgets();
|
fit();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -686,7 +891,7 @@ function platformLayout() {
|
||||||
if (isBelt) {
|
if (isBelt) {
|
||||||
gap += process.firstLayerBeltLead || 0;
|
gap += process.firstLayerBeltLead || 0;
|
||||||
const WIDGETS = api.widgets.all();
|
const WIDGETS = api.widgets.all();
|
||||||
let bounds = platformUpdateBounds(),
|
let bounds = update_bounds(),
|
||||||
movey = -(device.bedDepth / 2);
|
movey = -(device.bedDepth / 2);
|
||||||
for (let widget of WIDGETS) {
|
for (let widget of WIDGETS) {
|
||||||
let ylen = widget.track.box.h;
|
let ylen = widget.track.box.h;
|
||||||
|
|
@ -737,15 +942,32 @@ function platformLayout() {
|
||||||
api.event.emit('platform.layout');
|
api.event.emit('platform.layout');
|
||||||
}
|
}
|
||||||
|
|
||||||
function platformLoadWidget(group, vertices, filename) {
|
/**
|
||||||
|
* Create widget from vertex data and add to platform.
|
||||||
|
* Optionally saves to catalog and adds to group.
|
||||||
|
* @param {Array} [group] - Optional group array for grouping multiple widgets
|
||||||
|
* @param {Float32Array|Array} vertices - Vertex data (converted to Float32Array if needed)
|
||||||
|
* @param {string} [filename] - Optional filename for metadata and catalog
|
||||||
|
* @returns {Widget} Created widget
|
||||||
|
*/
|
||||||
|
function load_verts(group, vertices, filename) {
|
||||||
const widget = newWidget(undefined, group).loadVertices(vertices.toFloat32(), true);
|
const widget = newWidget(undefined, group).loadVertices(vertices.toFloat32(), true);
|
||||||
widget.meta.file = filename;
|
widget.meta.file = filename;
|
||||||
if (filename) widget.saveToCatalog(filename);
|
if (filename) widget.saveToCatalog(filename);
|
||||||
platformAdd(widget);
|
add(widget);
|
||||||
return widget;
|
return widget;
|
||||||
}
|
}
|
||||||
|
|
||||||
function platformLoadFiles(files, group) {
|
/**
|
||||||
|
* Load multiple files from File objects.
|
||||||
|
* Supports: STL, OBJ, 3MF, SVG, PNG, JPG, KMZ, Gerber (.gbr), gcode, raw vertex data.
|
||||||
|
* Also handles settings import (.b64, .km, .ini files).
|
||||||
|
* Groups loading to prevent intermediate layouts.
|
||||||
|
* Prompts user for grouping when multiple objects detected.
|
||||||
|
* @param {Array<File>} files - Array of File objects from file input or drag/drop
|
||||||
|
* @param {Array} [group] - Optional group array for grouping loaded widgets
|
||||||
|
*/
|
||||||
|
function load_files(files, group) {
|
||||||
platform.group();
|
platform.group();
|
||||||
let loading = files.length;
|
let loading = files.length;
|
||||||
for (let file of files) {
|
for (let file of files) {
|
||||||
|
|
@ -773,7 +995,7 @@ function platformLoadFiles(files, group) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (israw) {
|
if (israw) {
|
||||||
platformLoadWidget(group, JSON.parse(data));
|
load_verts(group, JSON.parse(data));
|
||||||
load_dec();
|
load_dec();
|
||||||
} else if (api.feature.on_load && (isstl || isobj || is3mf)) {
|
} else if (api.feature.on_load && (isstl || isobj || is3mf)) {
|
||||||
api.feature.on_load(data, file);
|
api.feature.on_load(data, file);
|
||||||
|
|
@ -782,14 +1004,14 @@ function platformLoadFiles(files, group) {
|
||||||
api.feature.on_add_stl(data, file);
|
api.feature.on_add_stl(data, file);
|
||||||
load_dec();
|
load_dec();
|
||||||
} else if (isstl) {
|
} else if (isstl) {
|
||||||
const stl = new load.STL().parse(data, api.view.unit_scale());
|
const stl = new file_load.STL().parse(data, api.view.unit_scale());
|
||||||
platformLoadWidget(group, stl, name);
|
load_verts(group, stl, name);
|
||||||
load_dec();
|
load_dec();
|
||||||
} else if (isobj) {
|
} else if (isobj) {
|
||||||
const objs = load.OBJ.parse(data.textDecode('utf-8'));
|
const objs = file_load.OBJ.parse(data.textDecode('utf-8'));
|
||||||
const ondn = function() {
|
const ondn = function() {
|
||||||
for (let obj of objs) {
|
for (let obj of objs) {
|
||||||
platformLoadWidget(group, obj, obj.name ? `${obj.name}-${name}` : name);
|
load_verts(group, obj, obj.name ? `${obj.name}-${name}` : name);
|
||||||
}
|
}
|
||||||
load_dec();
|
load_dec();
|
||||||
};
|
};
|
||||||
|
|
@ -810,7 +1032,7 @@ function platformLoadFiles(files, group) {
|
||||||
api.show.progress(progress, "converting");
|
api.show.progress(progress, "converting");
|
||||||
}, vertices => {
|
}, vertices => {
|
||||||
api.show.progress(0);
|
api.show.progress(0);
|
||||||
let wid = platformLoadWidget(group, vertices, name);
|
let wid = load_verts(group, vertices, name);
|
||||||
if (api.mode.is_cam()) {
|
if (api.mode.is_cam()) {
|
||||||
// attach raw illustration
|
// attach raw illustration
|
||||||
// api.event.emit('cam.parse.gerber', { data: text, mesh: wid.mesh });
|
// api.event.emit('cam.parse.gerber', { data: text, mesh: wid.mesh });
|
||||||
|
|
@ -821,13 +1043,13 @@ function platformLoadFiles(files, group) {
|
||||||
let odon = function(models) {
|
let odon = function(models) {
|
||||||
let msg = api.show.alert('Adding Objects');
|
let msg = api.show.alert('Adding Objects');
|
||||||
for (let model of models) {
|
for (let model of models) {
|
||||||
platformLoadWidget(group, model.faces, model.name ? `${model.name}-${name}` : name);
|
load_verts(group, model.faces, model.name ? `${model.name}-${name}` : name);
|
||||||
}
|
}
|
||||||
load_dec();
|
load_dec();
|
||||||
api.hide.alert(msg);
|
api.hide.alert(msg);
|
||||||
}
|
}
|
||||||
let msg = api.show.alert('Decoding 3MF');
|
let msg = api.show.alert('Decoding 3MF');
|
||||||
load.TMF.parseAsync(data).then(models => {
|
file_load.TMF.parseAsync(data).then(models => {
|
||||||
api.hide.alert(msg);
|
api.hide.alert(msg);
|
||||||
if (models.length > 1 && !group) {
|
if (models.length > 1 && !group) {
|
||||||
api.uc.confirm(`group ${models.length} objects?`).then(ok => {
|
api.uc.confirm(`group ${models.length} objects?`).then(ok => {
|
||||||
|
|
@ -846,14 +1068,14 @@ function platformLoadFiles(files, group) {
|
||||||
} else if (issvg) {
|
} else if (issvg) {
|
||||||
loadSVGDialog(opt => {
|
loadSVGDialog(opt => {
|
||||||
group = group || [];
|
group = group || [];
|
||||||
let svg = load.SVG.parse(data.textDecode('utf-8'), opt);
|
let svg = file_load.SVG.parse(data.textDecode('utf-8'), opt);
|
||||||
let ind = 0;
|
let ind = 0;
|
||||||
if (svg.length === 0) {
|
if (svg.length === 0) {
|
||||||
api.show.alert(`SVG contains no polylines`, 10);
|
api.show.alert(`SVG contains no polylines`, 10);
|
||||||
api.show.alert(`Fonts must be converted to paths`, 10);
|
api.show.alert(`Fonts must be converted to paths`, 10);
|
||||||
}
|
}
|
||||||
for (let v of svg) {
|
for (let v of svg) {
|
||||||
platformLoadWidget(group, svg[ind++], ind ? `${name}-${ind}` : name);
|
load_verts(group, svg[ind++], ind ? `${name}-${ind}` : name);
|
||||||
}
|
}
|
||||||
load_dec();
|
load_dec();
|
||||||
});
|
});
|
||||||
|
|
@ -869,6 +1091,12 @@ function platformLoadFiles(files, group) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show dialog to configure SVG import settings.
|
||||||
|
* Prompts for extrusion depth, arc resolution, DPI, and nesting.
|
||||||
|
* @param {Function} doit - Callback with options: {soup, resolution, segmin, depth, dpi}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
function loadSVGDialog(doit) {
|
function loadSVGDialog(doit) {
|
||||||
const opt = {pre: [
|
const opt = {pre: [
|
||||||
"<div class='f-col a-center'>",
|
"<div class='f-col a-center'>",
|
||||||
|
|
@ -896,7 +1124,13 @@ function loadSVGDialog(doit) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function fitDeviceToWidgets() {
|
/**
|
||||||
|
* Expand platform bed depth to fit widgets (belt mode only).
|
||||||
|
* Finds maximum Y dimension of all widgets and expands bed if needed.
|
||||||
|
* Adds 10mm padding. Saves original bed depth.
|
||||||
|
* @returns {boolean} True if bed was expanded
|
||||||
|
*/
|
||||||
|
function fit() {
|
||||||
let maxy = 0;
|
let maxy = 0;
|
||||||
api.widgets.each(widget => {
|
api.widgets.each(widget => {
|
||||||
let wb = widget.mesh.getBoundingBox().clone();
|
let wb = widget.mesh.getBoundingBox().clone();
|
||||||
|
|
@ -919,28 +1153,28 @@ function fitDeviceToWidgets() {
|
||||||
|
|
||||||
// extend API (api.platform)
|
// extend API (api.platform)
|
||||||
export const platform = {
|
export const platform = {
|
||||||
fit: fitDeviceToWidgets,
|
fit,
|
||||||
add: platformAdd,
|
add,
|
||||||
changed: platformChanged,
|
changed,
|
||||||
delete: platformDelete,
|
delete: platformDelete,
|
||||||
layout: platformLayout,
|
layout: layout,
|
||||||
group: platformGroup,
|
group,
|
||||||
group_done: platformGroupDone,
|
group_done,
|
||||||
load: platformLoad,
|
load,
|
||||||
load_stl: platformLoadSTL,
|
load_stl,
|
||||||
load_url: platformLoadURL,
|
load_url,
|
||||||
load_files: platformLoadFiles,
|
load_files,
|
||||||
load_verts: platformLoadWidget,
|
load_verts,
|
||||||
deselect: platformDeselect,
|
deselect,
|
||||||
select: platformSelect,
|
select,
|
||||||
select_all: platformSelectAll,
|
select_all,
|
||||||
selected_count: platformSelectedCount,
|
selected_count,
|
||||||
set_bounds: platformSetBounds,
|
set_bounds,
|
||||||
update_origin: platformUpdateOrigin,
|
update_origin,
|
||||||
update_bounds: platformUpdateBounds,
|
update_bounds,
|
||||||
update_size: platformUpdateSize,
|
update_size,
|
||||||
update_top_z: platformUpdateTopZ,
|
update_top_z,
|
||||||
update_selected: platformUpdateSelected,
|
update_selected,
|
||||||
update: space.platform.update,
|
update: space.platform.update,
|
||||||
set_font: space.platform.setFont,
|
set_font: space.platform.setFont,
|
||||||
show_axes: space.platform.showAxes,
|
show_axes: space.platform.showAxes,
|
||||||
129
src/kiri/app/preferences.js
Normal file
129
src/kiri/app/preferences.js
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
import { $c } from '../../moto/webui.js';
|
||||||
|
import { api } from './api.js';
|
||||||
|
import { base } from '../../geo/base.js';
|
||||||
|
import { space } from '../../moto/space.js';
|
||||||
|
|
||||||
|
let { client, platform } = api;
|
||||||
|
|
||||||
|
// Lazy initialization to avoid circular dependencies
|
||||||
|
function getUI() {
|
||||||
|
return api.ui;
|
||||||
|
}
|
||||||
|
|
||||||
|
function settings() {
|
||||||
|
return api.conf.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
function unitsSave() {
|
||||||
|
api.conf.update({ controller: true });
|
||||||
|
platform.update_size();
|
||||||
|
}
|
||||||
|
|
||||||
|
function aniMeshSave() {
|
||||||
|
api.conf.update({ controller: true });
|
||||||
|
api.conf.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
function lineTypeSave() {
|
||||||
|
const ui = getUI();
|
||||||
|
const sel = ui.lineType.options[ui.lineType.selectedIndex];
|
||||||
|
if (sel) {
|
||||||
|
settings().controller.lineType = sel.value;
|
||||||
|
api.conf.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function detailSave() {
|
||||||
|
const ui = getUI();
|
||||||
|
let level = ui.detail.options[ui.detail.selectedIndex];
|
||||||
|
if (level) {
|
||||||
|
level = level.value;
|
||||||
|
let rez = base.config.clipperClean;
|
||||||
|
switch (level) {
|
||||||
|
case '100': rez = 50; break;
|
||||||
|
case '75': rez = base.config.clipperClean; break;
|
||||||
|
case '50': rez = 500; break;
|
||||||
|
case '25': rez = 1000; break;
|
||||||
|
}
|
||||||
|
client.config({
|
||||||
|
base: { clipperClean: rez }
|
||||||
|
});
|
||||||
|
settings().controller.detail = level;
|
||||||
|
api.conf.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function speedSave() {
|
||||||
|
const ui = getUI();
|
||||||
|
settings().controller.showSpeeds = ui.showSpeeds.checked;
|
||||||
|
api.view.update_speeds();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setThreaded(bool) {
|
||||||
|
if (bool) {
|
||||||
|
client.pool.start();
|
||||||
|
} else {
|
||||||
|
client.pool.stop();
|
||||||
|
}
|
||||||
|
return bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
function booleanSave() {
|
||||||
|
const ui = getUI();
|
||||||
|
let control = settings().controller;
|
||||||
|
if (control.assembly != ui.assembly.checked) {
|
||||||
|
client.wasm(ui.assembly.checked);
|
||||||
|
}
|
||||||
|
if (control.antiAlias != ui.antiAlias.checked) {
|
||||||
|
api.show.alert('Page Reload Required to Change Aliasing');
|
||||||
|
}
|
||||||
|
control.antiAlias = ui.antiAlias.checked;
|
||||||
|
control.assembly = ui.assembly.checked;
|
||||||
|
control.autoLayout = ui.autoLayout.checked;
|
||||||
|
control.autoSave = ui.autoSave.checked;
|
||||||
|
control.dark = ui.dark.checked;
|
||||||
|
control.devel = ui.devel.checked;
|
||||||
|
control.drawer = ui.drawer.checked;
|
||||||
|
control.exportOcto = ui.exportOcto.checked;
|
||||||
|
control.exportPreview = ui.exportPreview.checked;
|
||||||
|
control.exportThumb = ui.exportThumb.checked;
|
||||||
|
control.freeLayout = ui.freeLayout.checked;
|
||||||
|
control.healMesh = ui.healMesh.checked;
|
||||||
|
control.ortho = ui.ortho.checked;
|
||||||
|
control.manifold = ui.manifold.checked;
|
||||||
|
control.reverseZoom = ui.reverseZoom.checked;
|
||||||
|
control.scrolls = ui.scrolls.checked;
|
||||||
|
control.shiny = ui.shiny.checked;
|
||||||
|
control.showOrigin = ui.showOrigin.checked;
|
||||||
|
control.showRulers = ui.showRulers.checked;
|
||||||
|
control.spaceRandoX = ui.spaceRandoX.checked;
|
||||||
|
// control.threaded = setThreaded(ui.threaded.checked);
|
||||||
|
control.webGPU = ui.webGPU.checked;
|
||||||
|
space.view.setZoom(control.reverseZoom, control.zoomSpeed);
|
||||||
|
// platform.layout();
|
||||||
|
api.conf.save();
|
||||||
|
api.platform.update_size();
|
||||||
|
api.visuals.update_stats();
|
||||||
|
updateDrawer();
|
||||||
|
api.event.emit('boolean.update');
|
||||||
|
space.view.setProjection(control.ortho ? 'orthographic' : 'perspective');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDrawer() {
|
||||||
|
const { drawer, scrolls } = settings().controller;
|
||||||
|
$c('app', drawer ? 'slideshow' : '', drawer ? '' : 'slideshow');
|
||||||
|
$c('app', scrolls ? '' : 'hide-scroll', scrolls ? 'hide-scroll' : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const preferences = {
|
||||||
|
unitsSave,
|
||||||
|
aniMeshSave,
|
||||||
|
lineTypeSave,
|
||||||
|
detailSave,
|
||||||
|
speedSave,
|
||||||
|
setThreaded,
|
||||||
|
booleanSave,
|
||||||
|
updateDrawer
|
||||||
|
};
|
||||||
602
src/kiri/app/selected.js
Normal file
602
src/kiri/app/selected.js
Normal file
|
|
@ -0,0 +1,602 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
import { api } from './api.js';
|
||||||
|
import { space } from '../../moto/space.js';
|
||||||
|
import { THREE } from '../../ext/three.js';
|
||||||
|
import { tool as MeshTool } from '../../mesh/tool.js';
|
||||||
|
import { encode as objEncode } from '../../load/obj.js';
|
||||||
|
import { encode as stlEncode } from '../../load/stl.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of currently selected widget meshes.
|
||||||
|
* @type {Array}
|
||||||
|
*/
|
||||||
|
const selectedMeshes = [];
|
||||||
|
|
||||||
|
function updateTool(ev) {
|
||||||
|
api.tool.update(ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute function for each unique group represented in selection.
|
||||||
|
* Passes the first widget from each group to the function.
|
||||||
|
* @param {function} fn - Function(widget) to execute for each group
|
||||||
|
*/
|
||||||
|
function for_groups(fn) {
|
||||||
|
let groups = widgets(true).map(w => w.group).uniq();
|
||||||
|
for (let group of groups) {
|
||||||
|
fn(group[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute function for each selected widget.
|
||||||
|
* If nothing selected, defaults to all widgets unless noauto=true.
|
||||||
|
* @param {function} fn - Function(widget) to execute
|
||||||
|
* @param {boolean} [noauto] - If true, don't auto-select all widgets when none selected
|
||||||
|
*/
|
||||||
|
function for_widgets(fn, noauto) {
|
||||||
|
let m = selectedMeshes;
|
||||||
|
let w = api.widgets.all();
|
||||||
|
if (m.length === 0 && w.length) {
|
||||||
|
m = noauto ? [] : w.map(w => w.mesh);
|
||||||
|
}
|
||||||
|
m.slice().forEach(mesh => { fn(mesh.widget) });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute function for all widgets with selection status.
|
||||||
|
* @param {function} fn - Function(widget, is_selected) to execute
|
||||||
|
*/
|
||||||
|
function for_status(fn) {
|
||||||
|
api.widgets.all().forEach(w => {
|
||||||
|
fn(w, selectedMeshes.contains(w.mesh));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute function for each selected mesh.
|
||||||
|
* @param {function} fn - Function(mesh) to execute
|
||||||
|
*/
|
||||||
|
function for_meshes(fn) {
|
||||||
|
selectedMeshes.slice().forEach(mesh => { fn(mesh) });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move selected widget groups.
|
||||||
|
* Only works in ARRANGE view.
|
||||||
|
* @param {number} x - X offset
|
||||||
|
* @param {number} y - Y offset
|
||||||
|
* @param {number} z - Z offset
|
||||||
|
* @param {boolean} [abs] - If true, use absolute positioning instead of relative
|
||||||
|
*/
|
||||||
|
function move(x, y, z, abs) {
|
||||||
|
if (!api.view.is_arrange()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for_groups(w => w.move(x, y, z, abs));
|
||||||
|
update_bounds();
|
||||||
|
api.platform.update_bounds();
|
||||||
|
api.event.emit('selection.move', {x, y, z, abs});
|
||||||
|
space.update();
|
||||||
|
api.space.auto_save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute bounding box of all selected meshes.
|
||||||
|
* @returns {THREE.Box3} Bounding box
|
||||||
|
*/
|
||||||
|
function get_bounds() {
|
||||||
|
// Helper to compute bounds of selected meshes
|
||||||
|
const THREE = self.THREE;
|
||||||
|
let bounds = new THREE.Box3();
|
||||||
|
selection.for_meshes(mesh => {
|
||||||
|
let box = mesh.getBoundingBox().clone();
|
||||||
|
let pos = mesh.widget.mesh.position;
|
||||||
|
box.min.add(pos);
|
||||||
|
box.max.add(pos);
|
||||||
|
bounds = bounds.union(box);
|
||||||
|
});
|
||||||
|
return bounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update UI size/scale info fields based on selected mesh bounds.
|
||||||
|
* Calculates combined bounding box and displays dimensions/scale in UI.
|
||||||
|
*/
|
||||||
|
function update_info() {
|
||||||
|
const ui = api.ui;
|
||||||
|
let bounds = new THREE.Box3(), track;
|
||||||
|
for_meshes(mesh => {
|
||||||
|
let box = mesh.getBoundingBox().clone();
|
||||||
|
track = mesh.widget.track;
|
||||||
|
let {pos} = track
|
||||||
|
box.min.add(pos);
|
||||||
|
box.max.add(pos);
|
||||||
|
bounds = bounds.union(box);
|
||||||
|
});
|
||||||
|
if (bounds.min.x === Infinity) {
|
||||||
|
if (selectedMeshes.length === 0) {
|
||||||
|
ui.size.X.value = 0;
|
||||||
|
ui.size.Y.value = 0;
|
||||||
|
ui.size.Z.value = 0;
|
||||||
|
ui.scale.X.value = 1;
|
||||||
|
ui.scale.Y.value = 1;
|
||||||
|
ui.scale.Z.value = 1;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let dx = bounds.max.x - bounds.min.x,
|
||||||
|
dy = bounds.max.y - bounds.min.y,
|
||||||
|
dz = bounds.max.z - bounds.min.z,
|
||||||
|
scale = api.view.unit_scale();
|
||||||
|
ui.size.X.value = ui.size.X.was = (dx / scale).round(2)
|
||||||
|
ui.size.Y.value = ui.size.Y.was = (dy / scale).round(2)
|
||||||
|
ui.size.Z.value = ui.size.Z.was = (dz / scale).round(2)
|
||||||
|
ui.scale.X.value = ui.scale.X.was = track.scale.x.round(2);
|
||||||
|
ui.scale.Y.value = ui.scale.Y.was = track.scale.y.round(2);
|
||||||
|
ui.scale.Z.value = ui.scale.Z.was = track.scale.z.round(2);
|
||||||
|
update_bounds();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update platform bounds and constrain widgets to bed limits.
|
||||||
|
* For belt mode, fits widgets and updates origin.
|
||||||
|
* Automatically moves widgets back into bounds if they exceed bed size.
|
||||||
|
* @param {Array<Widget>} [widgets] - Widgets to bound. Defaults to selected widgets.
|
||||||
|
*/
|
||||||
|
function update_bounds(widgets) {
|
||||||
|
const settings = api.conf.get();
|
||||||
|
// update bounds on selection for drag limiting
|
||||||
|
const isBelt = settings.device.bedBelt;
|
||||||
|
if (isBelt) {
|
||||||
|
if (api.platform.fit()) {
|
||||||
|
api.platform.update_origin();
|
||||||
|
space.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const dvy = settings.device.bedDepth;
|
||||||
|
const dvx = settings.device.bedWidth;
|
||||||
|
const bounds_sel = new THREE.Box3();
|
||||||
|
if (!widgets) {
|
||||||
|
widgets = selectedMeshes.map(m => m.widget);
|
||||||
|
}
|
||||||
|
for (let widget of widgets) {
|
||||||
|
const wp = widget.track.pos;
|
||||||
|
const bx = widget.track.box;
|
||||||
|
const miny = wp.y - bx.h / 2 + dvy / 2;
|
||||||
|
const maxy = wp.y + bx.h / 2 + dvy / 2;
|
||||||
|
const minx = wp.x - bx.w / 2 + dvx / 2;
|
||||||
|
const maxx = wp.x + bx.w / 2 + dvx / 2;
|
||||||
|
// keep widget in bounds when rotated or scaled
|
||||||
|
const ylo = miny < 0;
|
||||||
|
const yhi = !isBelt && maxy > dvy
|
||||||
|
if (ylo && !yhi) {
|
||||||
|
widget.move(0, -miny, 0);
|
||||||
|
} else if (yhi && !ylo) {
|
||||||
|
widget.move(0, dvy - maxy, 0);
|
||||||
|
}
|
||||||
|
const xlo = minx < 0;
|
||||||
|
const xhi = maxx > dvx;
|
||||||
|
if (xlo && !xhi) {
|
||||||
|
widget.move(-minx, 0, 0);
|
||||||
|
} else if (xhi && !xlo) {
|
||||||
|
widget.move(dvx - maxx, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const wb = widget.mesh.getBoundingBox().clone();
|
||||||
|
wb.min.x += wp.x;
|
||||||
|
wb.max.x += wp.x;
|
||||||
|
wb.min.y += wp.y;
|
||||||
|
wb.max.y += wp.y;
|
||||||
|
bounds_sel.union(wb);
|
||||||
|
}
|
||||||
|
settings.bounds_sel = bounds_sel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplicate selected widgets with offset.
|
||||||
|
* Only works in ARRANGE view.
|
||||||
|
* New widgets offset by bounding box width + 1 in X direction.
|
||||||
|
*/
|
||||||
|
function duplicate() {
|
||||||
|
if (!api.view.is_arrange()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for_widgets(widget => {
|
||||||
|
const mesh = widget.mesh;
|
||||||
|
const bb = mesh.getBoundingBox();
|
||||||
|
const ow = widget;
|
||||||
|
const nw = api.widgets.new().loadGeometry(mesh.geometry.clone());
|
||||||
|
nw.meta.file = ow.meta.file;
|
||||||
|
nw.meta.vertices = ow.meta.vertices;
|
||||||
|
nw.anno = ow.annotations();
|
||||||
|
nw.move(bb.max.x - bb.min.x + 1, 0, 0);
|
||||||
|
api.platform.add(nw, true);
|
||||||
|
api.event.emit("widget.duplicate", nw, ow);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mirror selected widgets along X axis.
|
||||||
|
* Only works in ARRANGE view.
|
||||||
|
*/
|
||||||
|
function mirror() {
|
||||||
|
if (!api.view.is_arrange()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for_widgets(widget => {
|
||||||
|
widget.mirror();
|
||||||
|
});
|
||||||
|
space.update();
|
||||||
|
api.space.auto_save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scale selected widget groups.
|
||||||
|
* Only works in ARRANGE view.
|
||||||
|
* Updates bounds, saves workspace, refreshes info and display.
|
||||||
|
* @param {number} x - X scale factor
|
||||||
|
* @param {number} y - Y scale factor
|
||||||
|
* @param {number} z - Z scale factor
|
||||||
|
* @param {boolean} [last] - If strictly false, skips auto-save
|
||||||
|
*/
|
||||||
|
function scale() {
|
||||||
|
if (!api.view.is_arrange()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let args = [...arguments];
|
||||||
|
for_groups(w => w.scale(...args));
|
||||||
|
selection.update_bounds();
|
||||||
|
api.platform.update_bounds();
|
||||||
|
api.event.emit('selection.scale', args);
|
||||||
|
// skip update if last argument is strictly 'false'
|
||||||
|
if (args.last() === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
api.space.auto_save();
|
||||||
|
selection.update_info();
|
||||||
|
space.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotate selected widget groups.
|
||||||
|
* Only works in ARRANGE view.
|
||||||
|
* Updates bounds, saves workspace, refreshes info and display.
|
||||||
|
* @param {number} x - X rotation in radians
|
||||||
|
* @param {number} y - Y rotation in radians
|
||||||
|
* @param {number} z - Z rotation in radians
|
||||||
|
*/
|
||||||
|
function rotate(x, y, z) {
|
||||||
|
if (!api.view.is_arrange()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for_groups(w => w.rotate(x, y, z));
|
||||||
|
selection.update_bounds();
|
||||||
|
api.platform.update_bounds();
|
||||||
|
api.event.emit('selection.rotate', {x, y, z});
|
||||||
|
api.space.auto_save();
|
||||||
|
selection.update_info();
|
||||||
|
space.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge selected widgets into a single widget.
|
||||||
|
* Combines all vertex data from selected widgets into one mesh.
|
||||||
|
* @param {object} options - Options
|
||||||
|
* @param {boolean} [options.deleteMerged=true] - If true, deletes original widgets after merge
|
||||||
|
*/
|
||||||
|
function merge({ deleteMerged = true }) {
|
||||||
|
let sel = widgets();
|
||||||
|
if (sel.length === 0) {
|
||||||
|
sel = api.widgets.all();
|
||||||
|
}
|
||||||
|
let obj = [];
|
||||||
|
sel.forEach(widget => {
|
||||||
|
let {x, y, z} = widget.track.pos || { x:0, y:0, z:0 };
|
||||||
|
let pos = widget.mesh.geometry.attributes.position;
|
||||||
|
let pvals = pos.array;
|
||||||
|
for (let i=0, il=pos.count; i<il; i += 3) {
|
||||||
|
let pi = i * pos.itemSize;
|
||||||
|
obj.push(pvals[pi++] + x, pvals[pi++] + y, pvals[pi++] + z);
|
||||||
|
obj.push(pvals[pi++] + x, pvals[pi++] + y, pvals[pi++] + z);
|
||||||
|
obj.push(pvals[pi++] + x, pvals[pi++] + y, pvals[pi++] + z);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let w = api.platform.load_verts([], obj, "merged");
|
||||||
|
if (deleteMerged) api.platform.delete(sel);
|
||||||
|
api.platform.select(w);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Isolates the bodies within the selected widgets. If no widgets are selected,
|
||||||
|
* it processes all available widgets. For each widget, it checks if a tool is
|
||||||
|
* associated; if not, it creates a new MeshTool and indexes the vertices.
|
||||||
|
* The function identifies isolated bodies within each widget. If a widget has
|
||||||
|
* a single body, it remains selected. Otherwise, it creates new widgets for
|
||||||
|
* each isolated body and selects them while deleting the original widget.
|
||||||
|
*/
|
||||||
|
function isolateBodies(){
|
||||||
|
let sel = widgets();
|
||||||
|
console.log('isolate', sel);
|
||||||
|
if (sel.length === 0) {
|
||||||
|
sel = api.widgets.all();
|
||||||
|
}
|
||||||
|
for(let widget of sel){
|
||||||
|
if (!widget.tool) {
|
||||||
|
let tool = widget.tool = new MeshTool();
|
||||||
|
tool.index(widget.getGeoVertices({ unroll: true, translate:true }));
|
||||||
|
}
|
||||||
|
let bodies = widget.tool.isolateBodies()
|
||||||
|
if(bodies.length ==1){
|
||||||
|
api.platform.select(widget);
|
||||||
|
}else{
|
||||||
|
// console.log(bodies,Array.from(bodies.entries()))
|
||||||
|
for(let [i,verts] of bodies.entries()){
|
||||||
|
let w = api.platform.load_verts([], verts, `isolate ${i} of ${ widget.anno.file}`);
|
||||||
|
api.platform.select(w)
|
||||||
|
}
|
||||||
|
api.platform.delete(widget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export selected widgets to STL or OBJ format.
|
||||||
|
* If multiple widgets, applies position offsets in STL format.
|
||||||
|
* @param {string} [format="stl"] - Export format: "stl" or "obj"
|
||||||
|
* @returns {ArrayBuffer|string} Encoded file data
|
||||||
|
*/
|
||||||
|
function exportWidgets(format = "stl") {
|
||||||
|
let sel = widgets();
|
||||||
|
if (sel.length === 0) {
|
||||||
|
sel = api.widgets.all();
|
||||||
|
}
|
||||||
|
let facets = 0;
|
||||||
|
let outs = [];
|
||||||
|
sel.forEach(widget => {
|
||||||
|
let mesh = widget.mesh;
|
||||||
|
let geo = mesh.geometry;
|
||||||
|
outs.push({geo, widget});
|
||||||
|
facets += geo.attributes.position.count;
|
||||||
|
});
|
||||||
|
if (format === "obj") {
|
||||||
|
let recs = outs.map(out => {
|
||||||
|
let meta = out.widget.meta;
|
||||||
|
let name = meta.file || 'unnamed';
|
||||||
|
let { position } = out.geo.attributes;
|
||||||
|
return { file: name, varr: position.array };
|
||||||
|
});
|
||||||
|
return objEncode(recs);
|
||||||
|
}
|
||||||
|
let recs = outs.map(out => {
|
||||||
|
let { position } = out.geo.attributes;
|
||||||
|
let pvals = position.array;
|
||||||
|
let varr = pvals;
|
||||||
|
|
||||||
|
// Apply position offset if exporting multiple widgets
|
||||||
|
if (outs.length > 1) {
|
||||||
|
let {x, y, z} = out.widget.track.pos;
|
||||||
|
varr = new Float32Array(pvals.length);
|
||||||
|
for (let i = 0; i < pvals.length; i += 3) {
|
||||||
|
varr[i] = pvals[i] + x;
|
||||||
|
varr[i+1] = pvals[i+1] + y;
|
||||||
|
varr[i+2] = pvals[i+2] + z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { file: out.widget.meta.file || 'unnamed', varr };
|
||||||
|
});
|
||||||
|
return stlEncode(recs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get selected widgets.
|
||||||
|
* @param {boolean} [orall] - If true and nothing selected, returns all widgets
|
||||||
|
* @returns {Array<Widget>} Selected widgets or empty array
|
||||||
|
*/
|
||||||
|
function widgets(orall) {
|
||||||
|
let sel = selectedMeshes.slice().map(m => m.widget);
|
||||||
|
return sel.length ? sel : orall ? api.widgets.all() : []
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDisabled(bool) {
|
||||||
|
for_widgets(w => w.meta.disabled = bool);
|
||||||
|
api.platform.update_selected();
|
||||||
|
}
|
||||||
|
|
||||||
|
function settings() {
|
||||||
|
return api.conf.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompt user for rotation angles and rotate selection.
|
||||||
|
* Shows alert if nothing selected.
|
||||||
|
* Accepts comma-separated X,Y,Z degrees.
|
||||||
|
*/
|
||||||
|
function input_rotate() {
|
||||||
|
if (selection.meshes().length === 0) {
|
||||||
|
api.show.alert("select object to rotate");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
api.uc.prompt("Enter X,Y,Z degrees of rotation","").then(coord => {
|
||||||
|
coord = (coord || '').split(',');
|
||||||
|
let prod = Math.PI / 180,
|
||||||
|
x = parseFloat(coord[0] || 0.0) * prod,
|
||||||
|
y = parseFloat(coord[1] || 0.0) * prod,
|
||||||
|
z = parseFloat(coord[2] || 0.0) * prod;
|
||||||
|
selection.rotate(x, y, z);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompt user for position coordinates and move selection.
|
||||||
|
* Shows alert if nothing selected.
|
||||||
|
* Accepts comma-separated X,Y coordinates.
|
||||||
|
* Adjusts for center vs corner origin modes.
|
||||||
|
*/
|
||||||
|
function input_position() {
|
||||||
|
if (selection.meshes().length === 0) {
|
||||||
|
api.show.alert("select object to position");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let current = settings(),
|
||||||
|
{ device, process} = current,
|
||||||
|
center = process.ctOriginCenter || process.camOriginCenter || device.bedRound || device.originCenter,
|
||||||
|
bounds = selection.get_bounds();
|
||||||
|
|
||||||
|
api.uc.prompt("Enter X,Y coordinates for selection","").then(coord => {
|
||||||
|
coord = (coord || '').split(',');
|
||||||
|
let x = parseFloat(coord[0] || 0.0),
|
||||||
|
y = parseFloat(coord[1] || 0.0),
|
||||||
|
z = parseFloat(coord[2] || 0.0);
|
||||||
|
|
||||||
|
if (!center) {
|
||||||
|
x = x - device.bedWidth/2 + (bounds.max.x - bounds.min.x)/2;
|
||||||
|
y = y - device.bedDepth/2 + (bounds.max.y - bounds.min.y)/2
|
||||||
|
}
|
||||||
|
|
||||||
|
selection.move(x, y, z, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle size input change event.
|
||||||
|
* Calculates scale ratio from size change and applies to selection.
|
||||||
|
* Respects lock checkboxes for proportional scaling.
|
||||||
|
* Prevents scales below 0.1.
|
||||||
|
* @param {Event} e - Input change event
|
||||||
|
* @param {object} ui - UI elements object with size/lock fields
|
||||||
|
*/
|
||||||
|
function input_resize(e, ui) {
|
||||||
|
let dv = parseFloat(e.target.value || 1),
|
||||||
|
pv = parseFloat(e.target.was || 1),
|
||||||
|
ra = dv / pv,
|
||||||
|
xv = parseFloat(ui.size.X.was ?? ui.scale.X.value) || 1,
|
||||||
|
yv = parseFloat(ui.size.Y.was ?? ui.scale.Y.value) || 1,
|
||||||
|
zv = parseFloat(ui.size.Z.was ?? ui.scale.Z.value) || 1,
|
||||||
|
ta = e.target,
|
||||||
|
xc = ui.lock.X.checked,
|
||||||
|
yc = ui.lock.Y.checked,
|
||||||
|
zc = ui.lock.Z.checked,
|
||||||
|
xt = ta === ui.size.X,
|
||||||
|
yt = ta === ui.size.Y,
|
||||||
|
zt = ta === ui.size.Z,
|
||||||
|
tl = (xt && xc) || (yt && yc) || (zt && zc),
|
||||||
|
xr = ((tl && xc) || (!tl && xt) ? ra : 1),
|
||||||
|
yr = ((tl && yc) || (!tl && yt) ? ra : 1),
|
||||||
|
zr = ((tl && zc) || (!tl && zt) ? ra : 1);
|
||||||
|
// prevent null scale
|
||||||
|
if (xv * xr < 0.1 || yv * yr < 0.1 || zv * zr < 0.1) {
|
||||||
|
api.alerts.show('invalid scale value');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selection.scale(xr,yr,zr);
|
||||||
|
ui.size.X.was = ui.size.X.value = xv * xr;
|
||||||
|
ui.size.Y.was = ui.size.Y.value = yv * yr;
|
||||||
|
ui.size.Z.was = ui.size.Z.value = zv * zr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle scale input change event.
|
||||||
|
* Calculates scale ratio from scale change and applies to selection.
|
||||||
|
* Respects lock checkboxes for proportional scaling.
|
||||||
|
* Prevents scales below 0.1.
|
||||||
|
* @param {Event} e - Input change event
|
||||||
|
* @param {object} ui - UI elements object with scale/lock fields
|
||||||
|
*/
|
||||||
|
function input_scale(e, ui) {
|
||||||
|
let dv = parseFloat(e.target.value || 1),
|
||||||
|
pv = parseFloat(e.target.was || 1),
|
||||||
|
ra = dv / pv,
|
||||||
|
xv = parseFloat(ui.scale.X.was ?? ui.scale.X.value) || 1,
|
||||||
|
yv = parseFloat(ui.scale.Y.was ?? ui.scale.Y.value) || 1,
|
||||||
|
zv = parseFloat(ui.scale.Z.was ?? ui.scale.Y.value) || 1,
|
||||||
|
ta = e.target,
|
||||||
|
xc = ui.lock.X.checked,
|
||||||
|
yc = ui.lock.Y.checked,
|
||||||
|
zc = ui.lock.Z.checked,
|
||||||
|
xt = ta === ui.scale.X,
|
||||||
|
yt = ta === ui.scale.Y,
|
||||||
|
zt = ta === ui.scale.Z,
|
||||||
|
tl = (xt && xc) || (yt && yc) || (zt && zc),
|
||||||
|
xr = ((tl && xc) || (!tl && xt) ? ra : 1),
|
||||||
|
yr = ((tl && yc) || (!tl && yt) ? ra : 1),
|
||||||
|
zr = ((tl && zc) || (!tl && zt) ? ra : 1);
|
||||||
|
// prevent null scale
|
||||||
|
if (xv * xr < 0.1 || yv * yr < 0.1 || zv * zr < 0.1) {
|
||||||
|
api.alerts.show('invalid scale value');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selection.scale(xr,yr,zr);
|
||||||
|
ui.scale.X.was = ui.scale.X.value = xv * xr;
|
||||||
|
ui.scale.Y.was = ui.scale.Y.value = yv * yr;
|
||||||
|
ui.scale.Z.was = ui.scale.Z.value = zv * zr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parse_as_float(e) {
|
||||||
|
e.target.value = parseFloat(e.target.value) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind input handlers to UI elements.
|
||||||
|
* Sets up Enter key handlers for scale, size, tool, and rotation inputs.
|
||||||
|
* @param {object} ui - UI elements object
|
||||||
|
*/
|
||||||
|
function input_binding(ui) {
|
||||||
|
// on enter but not on blur
|
||||||
|
space.event.onEnterKey([
|
||||||
|
ui.scale.X, (e) => input_scale(e, ui),
|
||||||
|
ui.scale.Y, (e) => input_scale(e, ui),
|
||||||
|
ui.scale.Z, (e) => input_scale(e, ui),
|
||||||
|
ui.size.X, (e) => input_resize(e, ui),
|
||||||
|
ui.size.Y, (e) => input_resize(e, ui),
|
||||||
|
ui.size.Z, (e) => input_resize(e, ui),
|
||||||
|
]);
|
||||||
|
// on enter and blur
|
||||||
|
space.event.onEnterKey([
|
||||||
|
ui.toolName, updateTool,
|
||||||
|
ui.toolNum, updateTool,
|
||||||
|
ui.toolFluteDiam, updateTool,
|
||||||
|
ui.toolFluteLen, updateTool,
|
||||||
|
ui.toolShaftDiam, updateTool,
|
||||||
|
ui.toolShaftLen, updateTool,
|
||||||
|
ui.toolTaperTip, updateTool,
|
||||||
|
ui.toolTaperAngle, updateTool,
|
||||||
|
$('rot_x'), parse_as_float,
|
||||||
|
$('rot_y'), parse_as_float,
|
||||||
|
$('rot_z'), parse_as_float
|
||||||
|
], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// extend API (api.selection)
|
||||||
|
export const selection = {
|
||||||
|
duplicate,
|
||||||
|
for_groups,
|
||||||
|
for_meshes,
|
||||||
|
for_status,
|
||||||
|
for_widgets,
|
||||||
|
get_bounds,
|
||||||
|
input_binding,
|
||||||
|
input_position,
|
||||||
|
input_rotate,
|
||||||
|
isolateBodies,
|
||||||
|
merge,
|
||||||
|
mirror,
|
||||||
|
move,
|
||||||
|
rotate,
|
||||||
|
scale,
|
||||||
|
update_bounds,
|
||||||
|
update_info,
|
||||||
|
widgets,
|
||||||
|
export: exportWidgets,
|
||||||
|
add(w) { selectedMeshes.addOnce(w.mesh) },
|
||||||
|
delete() { api.platform.delete(widgets()) },
|
||||||
|
remove(w) { return selectedMeshes.remove(w.mesh) },
|
||||||
|
count() { return selectedMeshes.length },
|
||||||
|
contains(w) { return selectedMeshes.indexOf(w.mesh) >= 0 },
|
||||||
|
enable() { setDisabled(false) },
|
||||||
|
disable() { setDisabled(true) },
|
||||||
|
opacity() { api.widgets.opacity(...arguments) },
|
||||||
|
meshes() { return selectedMeshes.slice() },
|
||||||
|
};
|
||||||
309
src/kiri/app/slider.js
Normal file
309
src/kiri/app/slider.js
Normal file
|
|
@ -0,0 +1,309 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layer Slider Controller (Singleton)
|
||||||
|
* Manages layer range selection and visualization for slice/preview views
|
||||||
|
*/
|
||||||
|
|
||||||
|
class SliderControl {
|
||||||
|
#layerLo = 0;
|
||||||
|
#layerHi = 0;
|
||||||
|
#layerMax = 0;
|
||||||
|
#ui;
|
||||||
|
#slider;
|
||||||
|
#tracker;
|
||||||
|
#mobile;
|
||||||
|
#sliderBar;
|
||||||
|
#sliderBar2;
|
||||||
|
#drag = {};
|
||||||
|
|
||||||
|
// Callbacks to replace event emissions
|
||||||
|
#onLayerChange;
|
||||||
|
#onStackUpdate;
|
||||||
|
#onSceneUpdate;
|
||||||
|
|
||||||
|
#initialized = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the slider with configuration
|
||||||
|
* @param {Object} config
|
||||||
|
* @param {Object} config.ui - UI element references
|
||||||
|
* @param {HTMLElement} config.tracker - Drag tracker element
|
||||||
|
* @param {boolean} config.mobile - Mobile device flag
|
||||||
|
* @param {Function} config.onLayerChange - Called when layer range changes
|
||||||
|
* @param {Function} config.onStackUpdate - Called to update stack visibility
|
||||||
|
* @param {Function} config.onSceneUpdate - Called to refresh scene
|
||||||
|
*/
|
||||||
|
init({ ui, tracker, mobile, onLayerChange, onStackUpdate, onSceneUpdate }) {
|
||||||
|
if (this.#initialized) {
|
||||||
|
throw new Error('Slider already initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#ui = ui;
|
||||||
|
this.#slider = ui.slider;
|
||||||
|
this.#tracker = tracker;
|
||||||
|
this.#mobile = mobile;
|
||||||
|
this.#sliderBar = mobile ? 80 : 30;
|
||||||
|
this.#sliderBar2 = this.#sliderBar * 2;
|
||||||
|
this.#onLayerChange = onLayerChange;
|
||||||
|
this.#onStackUpdate = onStackUpdate;
|
||||||
|
this.#onSceneUpdate = onSceneUpdate;
|
||||||
|
|
||||||
|
this.#setupUI();
|
||||||
|
this.#bindEvents();
|
||||||
|
this.#initialized = true;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current layer range
|
||||||
|
* @returns {{lo: number, hi: number, max: number}}
|
||||||
|
*/
|
||||||
|
getRange() {
|
||||||
|
return {
|
||||||
|
lo: this.#layerLo,
|
||||||
|
hi: this.#layerHi,
|
||||||
|
max: this.#layerMax
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set layer range (bounds are enforced)
|
||||||
|
* @param {number} lo - Lower layer
|
||||||
|
* @param {number} hi - Upper layer
|
||||||
|
* @param {boolean} notify - Whether to trigger callbacks (default: true)
|
||||||
|
*/
|
||||||
|
setRange(lo, hi, notify = true) {
|
||||||
|
this.#layerLo = Math.max(0, Math.min(hi, lo));
|
||||||
|
this.#layerHi = Math.max(this.#layerLo, Math.min(this.#layerMax, hi));
|
||||||
|
this.#updateVisuals();
|
||||||
|
this.showLabels();
|
||||||
|
if (notify) {
|
||||||
|
this.#notifyChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set maximum layer count
|
||||||
|
* @param {number} max - Maximum layer value
|
||||||
|
*/
|
||||||
|
setMax(max) {
|
||||||
|
this.#layerMax = max;
|
||||||
|
this.#slider.max.innerText = max;
|
||||||
|
if (max < this.#layerHi) {
|
||||||
|
this.#layerHi = max;
|
||||||
|
this.showLabels();
|
||||||
|
this.#updateVisuals();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update slider visual position from normalized values
|
||||||
|
* @param {number} startPercent - Start position (0-1)
|
||||||
|
* @param {number} endPercent - End position (0-1)
|
||||||
|
*/
|
||||||
|
updatePosition(startPercent, endPercent) {
|
||||||
|
const width = this.#slider.range.clientWidth;
|
||||||
|
const maxval = width - this.#sliderBar2;
|
||||||
|
const start = Math.max(0, Math.min(1, startPercent));
|
||||||
|
const end = Math.max(start, Math.min(1, endPercent));
|
||||||
|
|
||||||
|
const lowval = start * maxval;
|
||||||
|
const midval = ((end - start) * maxval) + this.#sliderBar;
|
||||||
|
const hival = maxval - end * maxval;
|
||||||
|
|
||||||
|
this.#slider.hold.style.marginLeft = `${lowval}px`;
|
||||||
|
this.#slider.mid.style.width = `${midval}px`;
|
||||||
|
this.#slider.hold.style.marginRight = `${hival}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show layer number labels
|
||||||
|
*/
|
||||||
|
showLabels() {
|
||||||
|
const digits = this.#layerMax.toString().length;
|
||||||
|
this.#slider.min.style.width = `${digits}em`;
|
||||||
|
this.#slider.max.style.width = `${digits}em`;
|
||||||
|
this.#slider.min.innerText = this.#layerLo;
|
||||||
|
this.#slider.max.innerText = this.#layerHi;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show slider UI
|
||||||
|
*/
|
||||||
|
show() {
|
||||||
|
this.#ui.layers.style.display = 'flex';
|
||||||
|
this.#slider.div.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide slider UI
|
||||||
|
*/
|
||||||
|
hide() {
|
||||||
|
this.#ui.layers.style.display = 'none';
|
||||||
|
this.#slider.div.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
#setupUI() {
|
||||||
|
if (this.#mobile) {
|
||||||
|
this.#slider.div.classList.add('slider-mobile');
|
||||||
|
this.#slider.lo.classList.add('slider-mobile');
|
||||||
|
this.#slider.hi.classList.add('slider-mobile');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#bindEvents() {
|
||||||
|
// Min/Max buttons
|
||||||
|
this.#slider.min.onclick = () => {
|
||||||
|
this.#layerLo = 0;
|
||||||
|
this.#layerHi = 0;
|
||||||
|
this.showLabels();
|
||||||
|
this.#updateVisuals();
|
||||||
|
this.#notifyChange();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.#slider.max.onclick = () => {
|
||||||
|
this.#layerLo = 0;
|
||||||
|
this.#layerHi = this.#layerMax;
|
||||||
|
this.showLabels();
|
||||||
|
this.#updateVisuals();
|
||||||
|
this.#notifyChange();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mouse hover for labels
|
||||||
|
this.#slider.div.onmouseover = () => {
|
||||||
|
this.showLabels();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.#slider.div.onmouseleave = (ev) => {
|
||||||
|
if (!ev.buttons) {
|
||||||
|
this.hideLabels();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Drag handlers for the three slider sections
|
||||||
|
this.#bindDrag(this.#slider.lo, this.#handleLoDrag.bind(this));
|
||||||
|
this.#bindDrag(this.#slider.mid, this.#handleMidDrag.bind(this));
|
||||||
|
this.#bindDrag(this.#slider.hi, this.#handleHiDrag.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
#bindDrag(el, deltaHandler) {
|
||||||
|
const slider = this.#slider.range;
|
||||||
|
|
||||||
|
el.ontouchstart = el.onmousedown = (ev) => {
|
||||||
|
this.#tracker.style.display = 'block';
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
let obj = (ev.touches ? ev.touches[0] : ev);
|
||||||
|
this.#drag.width = slider.clientWidth;
|
||||||
|
this.#drag.maxval = this.#drag.width - this.#sliderBar2;
|
||||||
|
this.#drag.start = obj.screenX;
|
||||||
|
this.#drag.loat = this.#drag.low = this.#pxToInt(this.#slider.hold.style.marginLeft);
|
||||||
|
this.#drag.mdat = this.#drag.mid = this.#slider.mid.clientWidth;
|
||||||
|
this.#drag.hiat = this.#pxToInt(this.#slider.hold.style.marginRight);
|
||||||
|
this.#drag.mdmax = this.#drag.width - this.#sliderBar - this.#drag.loat;
|
||||||
|
this.#drag.himax = this.#drag.width - this.#sliderBar - this.#drag.mdat;
|
||||||
|
|
||||||
|
const cancelDrag = this.#tracker.ontouchend = this.#tracker.onmouseup = (ev) => {
|
||||||
|
if (ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
slider.onmousemove = undefined;
|
||||||
|
this.#tracker.style.display = 'none';
|
||||||
|
};
|
||||||
|
|
||||||
|
el.ontouchend = cancelDrag;
|
||||||
|
el.ontouchmove = this.#tracker.ontouchmove = this.#tracker.onmousemove = (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
if (ev.buttons === 0) {
|
||||||
|
return cancelDrag();
|
||||||
|
}
|
||||||
|
if (deltaHandler) {
|
||||||
|
let obj = (ev.touches ? ev.touches[0] : ev);
|
||||||
|
deltaHandler(obj.screenX - this.#drag.start);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleLoDrag(delta) {
|
||||||
|
let midval = this.#drag.mdat - delta;
|
||||||
|
let lowval = this.#drag.loat + delta;
|
||||||
|
|
||||||
|
if (midval < this.#sliderBar || lowval < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#slider.hold.style.marginLeft = `${lowval}px`;
|
||||||
|
this.#slider.mid.style.width = `${midval}px`;
|
||||||
|
this.#drag.low = lowval;
|
||||||
|
this.#drag.mid = midval;
|
||||||
|
this.#updateFromDrag();
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleMidDrag(delta) {
|
||||||
|
let loval = this.#drag.loat + delta;
|
||||||
|
let hival = this.#drag.hiat - delta;
|
||||||
|
|
||||||
|
if (loval < 0 || hival < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#slider.hold.style.marginLeft = `${loval}px`;
|
||||||
|
this.#slider.hold.style.marginRight = `${hival}px`;
|
||||||
|
this.#drag.low = loval;
|
||||||
|
this.#updateFromDrag();
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleHiDrag(delta) {
|
||||||
|
let midval = this.#drag.mdat + delta;
|
||||||
|
let hival = this.#drag.hiat - delta;
|
||||||
|
|
||||||
|
if (midval < this.#sliderBar || midval > this.#drag.mdmax || hival < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#slider.mid.style.width = `${midval}px`;
|
||||||
|
this.#slider.hold.style.marginRight = `${hival}px`;
|
||||||
|
this.#drag.mid = midval;
|
||||||
|
this.#updateFromDrag();
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateFromDrag() {
|
||||||
|
const start = this.#drag.low / this.#drag.maxval;
|
||||||
|
const end = (this.#drag.low + this.#drag.mid - this.#sliderBar) / this.#drag.maxval;
|
||||||
|
|
||||||
|
this.#layerLo = Math.round(start * this.#layerMax);
|
||||||
|
this.#layerHi = Math.round(end * this.#layerMax);
|
||||||
|
|
||||||
|
this.#notifyChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateVisuals() {
|
||||||
|
if (this.#layerMax === 0) return;
|
||||||
|
const start = this.#layerLo / this.#layerMax;
|
||||||
|
const end = this.#layerHi / this.#layerMax;
|
||||||
|
this.updatePosition(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
#notifyChange() {
|
||||||
|
// Notify callbacks of layer change
|
||||||
|
this.#onLayerChange?.(this.#layerHi, this.#layerLo);
|
||||||
|
this.#onStackUpdate?.(this.#layerLo, this.#layerHi);
|
||||||
|
this.#onSceneUpdate?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
#pxToInt(txt) {
|
||||||
|
return txt ? parseInt(txt.substring(0, txt.length - 2)) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
hideLabels() {
|
||||||
|
// Labels hide themselves on mouse leave, but can be called explicitly
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export singleton instance
|
||||||
|
export const slider = new SliderControl();
|
||||||
215
src/kiri/app/state/visual.js
Normal file
215
src/kiri/app/state/visual.js
Normal file
|
|
@ -0,0 +1,215 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VisualStateManager: Manages 3-layer visual state hierarchy for widgets
|
||||||
|
*
|
||||||
|
* Layer hierarchy (bottom to top):
|
||||||
|
* 1. Base Layer: User persistent preferences (edges, wireframe, opacity)
|
||||||
|
* 2. Mode Layer: Mode+view specific defaults (FDM/ARRANGE vs CAM/PREVIEW)
|
||||||
|
* 3. Operation Layer: Temporary context overrides (paint, selection)
|
||||||
|
*
|
||||||
|
* Context types:
|
||||||
|
* - 'paint': OVERRIDE context (FDM support painting, completely replaces material)
|
||||||
|
* - 'select': TEMPORARY context (selection highlighting)
|
||||||
|
* - null: Normal state changes following hierarchy
|
||||||
|
*/
|
||||||
|
class VisualStateManager {
|
||||||
|
constructor(widget, scheme, api) {
|
||||||
|
this.widget = widget;
|
||||||
|
this.scheme = scheme;
|
||||||
|
this.api = api;
|
||||||
|
|
||||||
|
// State stack (base → mode → operation)
|
||||||
|
this.stack = [];
|
||||||
|
|
||||||
|
// Active operation states
|
||||||
|
this.operations = {}; // { context: state }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load user preferences from localStorage (base layer)
|
||||||
|
*/
|
||||||
|
loadPreferences() {
|
||||||
|
const edges = this.api.local.getBoolean('model.edges');
|
||||||
|
const wireframe = this.api.local.getBoolean('model.wireframe');
|
||||||
|
const wireframeColor = this.api.local.getInt('model.wireframe.color') || 0;
|
||||||
|
const wireframeOpacity = this.api.local.getFloat('model.wireframe.opacity');
|
||||||
|
|
||||||
|
this.baseState = {
|
||||||
|
edges: edges || false,
|
||||||
|
wireframe: wireframe || false,
|
||||||
|
wireframeColor: wireframeColor,
|
||||||
|
wireframeOpacity: wireframeOpacity !== null ? wireframeOpacity : this.scheme.wireframe.opacity,
|
||||||
|
opacity: 1.0
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.baseState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save user preferences to localStorage
|
||||||
|
*/
|
||||||
|
savePreferences() {
|
||||||
|
if (!this.baseState) return;
|
||||||
|
|
||||||
|
this.api.local.set('model.edges', this.baseState.edges);
|
||||||
|
this.api.local.set('model.wireframe', this.baseState.wireframe);
|
||||||
|
if (this.baseState.wireframeColor !== undefined) {
|
||||||
|
this.api.local.set('model.wireframe.color', this.baseState.wireframeColor);
|
||||||
|
}
|
||||||
|
if (this.baseState.wireframeOpacity !== undefined) {
|
||||||
|
this.api.local.set('model.wireframe.opacity', this.baseState.wireframeOpacity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply mode-specific defaults (mode layer)
|
||||||
|
* @param {number} mode - Mode ID
|
||||||
|
* @param {number} viewMode - View mode ID (ARRANGE=1, SLICE=2, PREVIEW=3, ANIMATE=4)
|
||||||
|
*/
|
||||||
|
applyModeState(mode, viewMode) {
|
||||||
|
const viewName = this._viewModeToName(viewMode);
|
||||||
|
const viewScheme = this.scheme.views[viewName];
|
||||||
|
|
||||||
|
this.modeState = {
|
||||||
|
mode,
|
||||||
|
viewMode,
|
||||||
|
opacity: viewScheme?.model_opacity ?? 1.0,
|
||||||
|
previewOpacity: viewScheme?.preview_opacity,
|
||||||
|
slicingOpacity: viewScheme?.slicing_opacity,
|
||||||
|
slicedOpacity: viewScheme?.sliced_opacity
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.modeState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push a temporary operation state (operation layer)
|
||||||
|
* @param {string} context - Operation context ('paint', 'select', etc)
|
||||||
|
* @param {object} state - State to push
|
||||||
|
* @param {function} state.restoreCallback - Optional callback to restore state on pop
|
||||||
|
*/
|
||||||
|
pushState(context, state) {
|
||||||
|
if (!context) {
|
||||||
|
console.warn('pushState called without context');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.operations[context] = state;
|
||||||
|
this.stack.push({ layer: 'operation', context, state });
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pop a temporary operation state
|
||||||
|
* @param {string} context - Operation context to pop
|
||||||
|
*/
|
||||||
|
popState(context) {
|
||||||
|
if (!context) {
|
||||||
|
console.warn('popState called without context');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const operation = this.operations[context];
|
||||||
|
if (!operation) {
|
||||||
|
console.warn(`No operation found for context: ${context}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call restore callback if provided
|
||||||
|
if (operation.restoreCallback) {
|
||||||
|
operation.restoreCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from operations and stack
|
||||||
|
delete this.operations[context];
|
||||||
|
this.stack = this.stack.filter(s => s.context !== context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current effective state (topmost in hierarchy)
|
||||||
|
*/
|
||||||
|
getCurrentState() {
|
||||||
|
if (this.stack.length > 0) {
|
||||||
|
return this.stack[this.stack.length - 1].state;
|
||||||
|
}
|
||||||
|
if (this.modeState) {
|
||||||
|
return this.modeState;
|
||||||
|
}
|
||||||
|
return this.baseState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an operation context is currently active
|
||||||
|
* @param {string} context - Operation context to check
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isOperationActive(context) {
|
||||||
|
return context in this.operations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all active operation contexts
|
||||||
|
* @returns {Array<string>}
|
||||||
|
*/
|
||||||
|
getActiveContexts() {
|
||||||
|
return Object.keys(this.operations);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all operation states (e.g., on mode switch)
|
||||||
|
*/
|
||||||
|
clearOperations() {
|
||||||
|
// Call restore callbacks for all active operations
|
||||||
|
for (const context in this.operations) {
|
||||||
|
const operation = this.operations[context];
|
||||||
|
if (operation.restoreCallback) {
|
||||||
|
operation.restoreCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.operations = {};
|
||||||
|
this.stack = this.stack.filter(s => s.layer !== 'operation');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get visual state snapshot for undo/redo
|
||||||
|
*/
|
||||||
|
getVisualState() {
|
||||||
|
return {
|
||||||
|
edges: this.widget.outline ? true : false,
|
||||||
|
wires: this.widget.wire ? true : false,
|
||||||
|
opacity: this.widget.getMaterial()?.opacity ?? 1.0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore visual state from snapshot
|
||||||
|
* @param {object} state - State snapshot
|
||||||
|
*/
|
||||||
|
setVisualState({ edges, wires, opacity }) {
|
||||||
|
this.widget.cache.vizstate = this.getVisualState();
|
||||||
|
this.widget._setEdges(edges ?? false);
|
||||||
|
this.widget._setWireframe(wires ?? false);
|
||||||
|
if (opacity !== undefined) {
|
||||||
|
this.widget.setOpacity(opacity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert view mode ID to name
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_viewModeToName(viewMode) {
|
||||||
|
const views = {
|
||||||
|
1: 'ARRANGE',
|
||||||
|
2: 'SLICE',
|
||||||
|
3: 'PREVIEW',
|
||||||
|
4: 'ANIMATE'
|
||||||
|
};
|
||||||
|
return views[viewMode] || 'ARRANGE';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { VisualStateManager };
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { local } from '../../data/local.js';
|
import { local } from '../../data/local.js';
|
||||||
import { broker } from '../../moto/broker.js';
|
import { broker } from '../../moto/broker.js';
|
||||||
import { js2o, o2js } from './utils.js';
|
import { js2o, o2js } from '../core/utils.js';
|
||||||
|
|
||||||
class Stats {
|
class Stats {
|
||||||
constructor(db) {
|
constructor(db) {
|
||||||
112
src/kiri/app/view-mode.js
Normal file
112
src/kiri/app/view-mode.js
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
import { $ } from '../../moto/webui.js';
|
||||||
|
import { api } from './api.js';
|
||||||
|
import { consts } from './consts.js';
|
||||||
|
import { platform } from './platform.js';
|
||||||
|
import { selection } from './selected.js';
|
||||||
|
import { settings } from './conf/manager.js';
|
||||||
|
import STACKS from './stacks.js';
|
||||||
|
|
||||||
|
const { VIEWS } = consts;
|
||||||
|
const DOC = self.document;
|
||||||
|
|
||||||
|
/** Current view mode (ARRANGE, SLICE, PREVIEW, or ANIMATE) */
|
||||||
|
let viewMode = VIEWS.ARRANGE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set application view mode and update UI accordingly.
|
||||||
|
* Modes:
|
||||||
|
* - ARRANGE: Widget positioning and arrangement
|
||||||
|
* - SLICE: Sliced layer visualization
|
||||||
|
* - PREVIEW: Toolpath/gcode preview
|
||||||
|
* - ANIMATE: Animated toolpath playback
|
||||||
|
*
|
||||||
|
* Each mode:
|
||||||
|
* - Updates UI button states
|
||||||
|
* - Clears selections and bounds
|
||||||
|
* - Shows/hides relevant tools and sliders
|
||||||
|
* - Configures widget visibility and rendering
|
||||||
|
* - Emits 'view.set' event
|
||||||
|
*
|
||||||
|
* @param {number} mode - View mode constant from VIEWS
|
||||||
|
*/
|
||||||
|
function setViewMode(mode) {
|
||||||
|
const isCAM = settings.mode() === 'CAM';
|
||||||
|
viewMode = mode;
|
||||||
|
platform.deselect();
|
||||||
|
selection.update_info();
|
||||||
|
// clear any bounds forced by, for example, WireEDM previews
|
||||||
|
api.platform.set_bounds();
|
||||||
|
// disable clear in non-arrange modes
|
||||||
|
['view-arrange','act-slice','act-preview','act-animate'].forEach(el => {
|
||||||
|
$(el).classList.remove('selected')
|
||||||
|
});
|
||||||
|
$('render-tools').classList.add('hide');
|
||||||
|
switch (mode) {
|
||||||
|
case VIEWS.ARRANGE:
|
||||||
|
$('view-arrange').classList.add('selected');
|
||||||
|
$('render-tools').classList.remove('hide');
|
||||||
|
api.function.clear_progress();
|
||||||
|
api.client.clear();
|
||||||
|
STACKS.clear();
|
||||||
|
api.visuals.hide_slider();
|
||||||
|
api.visuals.update_speeds();
|
||||||
|
api.visuals.set_visible_layer();
|
||||||
|
api.visuals.set_widget_visibility(true);
|
||||||
|
// Restore global visual state (opacity, edges, wireframe)
|
||||||
|
api.widgets.each(w => w.applyGlobalVisualState());
|
||||||
|
break;
|
||||||
|
case VIEWS.SLICE:
|
||||||
|
$('act-slice').classList.add('selected');
|
||||||
|
api.visuals.update_speeds();
|
||||||
|
api.visuals.update_slider_max();
|
||||||
|
api.visuals.set_widget_visibility(true);
|
||||||
|
// Hide edges without persisting (preserve user preference)
|
||||||
|
!isCAM && api.view.set_edges(false, false);
|
||||||
|
break;
|
||||||
|
case VIEWS.PREVIEW:
|
||||||
|
$('act-preview').classList.add('selected');
|
||||||
|
api.visuals.set_widget_visibility(true);
|
||||||
|
// Hide edges without persisting (preserve user preference)
|
||||||
|
!isCAM && api.view.set_edges(false, false);
|
||||||
|
break;
|
||||||
|
case VIEWS.ANIMATE:
|
||||||
|
$('act-animate').classList.add('selected');
|
||||||
|
// Hide edges without persisting (preserve user preference)
|
||||||
|
!isCAM && api.view.set_edges(false, false);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log("invalid view mode: "+mode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
api.event.emit('view.set', mode);
|
||||||
|
DOC.activeElement.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View mode management API.
|
||||||
|
* Provides getters/setters and mode checking utilities.
|
||||||
|
*/
|
||||||
|
export const view = {
|
||||||
|
/** Get current view mode */
|
||||||
|
get() { return viewMode },
|
||||||
|
/** Set view mode */
|
||||||
|
set() { setViewMode(...arguments) },
|
||||||
|
/** Switch to arrange mode */
|
||||||
|
set_arrange() { setViewMode(VIEWS.ARRANGE) },
|
||||||
|
/** Switch to slice mode */
|
||||||
|
set_slice() { setViewMode(VIEWS.SLICE) },
|
||||||
|
/** Switch to preview mode */
|
||||||
|
set_preview() { setViewMode(VIEWS.PREVIEW) },
|
||||||
|
/** Switch to animate mode */
|
||||||
|
set_animate() { setViewMode(VIEWS.ANIMATE) },
|
||||||
|
/** Check if in arrange mode */
|
||||||
|
is_arrange() { return viewMode === VIEWS.ARRANGE },
|
||||||
|
/** Check if in slice mode */
|
||||||
|
is_slice() { return viewMode === VIEWS.SLICE },
|
||||||
|
/** Check if in preview mode */
|
||||||
|
is_preview() { return viewMode === VIEWS.PREVIEW },
|
||||||
|
/** Check if in animate mode */
|
||||||
|
is_animate() { return viewMode === VIEWS.ANIMATE }
|
||||||
|
};
|
||||||
390
src/kiri/app/visuals.js
Normal file
390
src/kiri/app/visuals.js
Normal file
|
|
@ -0,0 +1,390 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
import { api } from './api.js';
|
||||||
|
import { base } from '../../geo/base.js';
|
||||||
|
import { consts } from './consts.js';
|
||||||
|
import { colorSchemeRegistry } from './color/schemes.js';
|
||||||
|
import { settings } from './conf/manager.js';
|
||||||
|
import { space } from '../../moto/space.js';
|
||||||
|
|
||||||
|
import STACKS from './stacks.js';
|
||||||
|
|
||||||
|
const { VIEWS } = consts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current color scheme
|
||||||
|
*/
|
||||||
|
function getColorScheme() {
|
||||||
|
const mode = api.mode.get_id();
|
||||||
|
const theme = api.space.is_dark() ? 'dark' : 'light';
|
||||||
|
return colorSchemeRegistry.getScheme(mode, theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constrain value to min/max bounds.
|
||||||
|
* @param {number} v - Value to constrain
|
||||||
|
* @param {number} min - Minimum value
|
||||||
|
* @param {number} max - Maximum value
|
||||||
|
* @returns {number} Constrained value
|
||||||
|
*/
|
||||||
|
function bound(v,min,max) {
|
||||||
|
return Math.max(min,Math.min(max,v));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get unit scale factor for coordinate conversion.
|
||||||
|
* Returns 25.4 for CAM mode with inches, 1.0 otherwise (mm).
|
||||||
|
* @returns {number} Unit scale factor
|
||||||
|
*/
|
||||||
|
function unitScale() {
|
||||||
|
return api.mode.is_cam() && settings.ctrl().units === 'in' ? 25.4 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update speed color legend for preview mode.
|
||||||
|
* Shows color-coded speed visualization in preview mode for FDM.
|
||||||
|
* Generates 20 color swatches from worker and displays in UI.
|
||||||
|
* Only visible in PREVIEW mode for FDM (not SLA/LASER) when enabled.
|
||||||
|
* Emits 'preview.speeds' event with min/max speeds.
|
||||||
|
* @param {number} [maxSpeed] - Maximum speed value
|
||||||
|
* @param {number} [minSpeed] - Minimum speed value
|
||||||
|
*/
|
||||||
|
function updateSpeeds(maxSpeed, minSpeed) {
|
||||||
|
const { ui } = api;
|
||||||
|
const viewMode = api.view.get();
|
||||||
|
ui.speeds.style.display =
|
||||||
|
maxSpeed &&
|
||||||
|
settings.mode !== 'SLA' &&
|
||||||
|
settings.mode !== 'LASER' &&
|
||||||
|
viewMode === VIEWS.PREVIEW &&
|
||||||
|
ui.showSpeeds.checked ? 'block' : '';
|
||||||
|
if (maxSpeed) {
|
||||||
|
const colors = [];
|
||||||
|
for (let i = 0; i <= maxSpeed; i += maxSpeed / 20) {
|
||||||
|
colors.push(Math.round(Math.max(i, 1)));
|
||||||
|
}
|
||||||
|
api.client.colors(colors, maxSpeed, speedColors => {
|
||||||
|
const list = [];
|
||||||
|
Object.keys(speedColors).map(v => parseInt(v)).sort((a, b) => b - a).forEach(speed => {
|
||||||
|
const color = speedColors[speed];
|
||||||
|
const hex = color.toString(16).padStart(6, 0);
|
||||||
|
const r = (color >> 16) & 0xff;
|
||||||
|
const g = (color >> 8) & 0xff;
|
||||||
|
const b = (color >> 0) & 0xff;
|
||||||
|
const style = `background-color:#${hex}`;
|
||||||
|
list.push(`<label style="${style}">${speed}</label>`);
|
||||||
|
});
|
||||||
|
ui.speedbar.innerHTML = list.join('');
|
||||||
|
});
|
||||||
|
api.event.emit('preview.speeds', {
|
||||||
|
min: minSpeed,
|
||||||
|
max: maxSpeed
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update slider visual position and sync settings.
|
||||||
|
* Updates slider thumb positions based on current range.
|
||||||
|
* Synchronizes configuration fields from range values.
|
||||||
|
*/
|
||||||
|
function updateSlider() {
|
||||||
|
const { lo, hi, max } = api.slider.getRange();
|
||||||
|
if (max > 0) {
|
||||||
|
api.slider.updatePosition(lo / max, hi / max);
|
||||||
|
}
|
||||||
|
api.conf.update_fields_from_range();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set visible layer range and update visualization.
|
||||||
|
* Bounds values to valid range and updates slider.
|
||||||
|
* Triggers slice visualization update.
|
||||||
|
* @param {number} [h] - High layer (defaults to current hi)
|
||||||
|
* @param {number} [l] - Low layer (defaults to current lo)
|
||||||
|
*/
|
||||||
|
function setVisibleLayer(h, l) {
|
||||||
|
const { lo, hi, max } = api.slider.getRange();
|
||||||
|
h = h >= 0 ? h : hi;
|
||||||
|
l = l >= 0 ? l : lo;
|
||||||
|
const newHi = bound(h, 0, max);
|
||||||
|
const newLo = bound(l, 0, newHi);
|
||||||
|
api.slider.setRange(newLo, newHi); // Notify callbacks (STACKS, scene updates)
|
||||||
|
api.slider.showLabels();
|
||||||
|
showSlices();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set global opacity for all widgets.
|
||||||
|
* Updates material opacity for all widgets and persists to localStorage.
|
||||||
|
* This is the primary opacity control - all widgets share the same opacity.
|
||||||
|
* @param {number} value - Opacity value (0.0 to 1.0)
|
||||||
|
*/
|
||||||
|
function setOpacity(value) {
|
||||||
|
api.local.set('model.opacity', value);
|
||||||
|
// Also update old key for backwards compatibility
|
||||||
|
api.local.set('model.wireframe.opacity', value);
|
||||||
|
api.widgets.each(w => {
|
||||||
|
const mat = w.getMaterial();
|
||||||
|
mat.opacity = value;
|
||||||
|
mat.transparent = value < 1.0;
|
||||||
|
mat.visible = value > 0.0;
|
||||||
|
});
|
||||||
|
space.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set wireframe rendering mode for all widgets.
|
||||||
|
* Updates 3D scene after applying to all widgets.
|
||||||
|
* Persists state to local storage.
|
||||||
|
* @param {boolean|object} bool - Enable/disable wireframe, or {toggle: true} to toggle
|
||||||
|
* @param {number} [color] - Optional wireframe color
|
||||||
|
* @param {number} [opacity] - Optional wireframe opacity
|
||||||
|
*/
|
||||||
|
function setWireframe(bool, color, opacity) {
|
||||||
|
if (bool && bool.toggle) {
|
||||||
|
api.local.toggle('model.wireframe');
|
||||||
|
} else if (typeof bool === 'boolean') {
|
||||||
|
api.local.set('model.wireframe', bool);
|
||||||
|
// Persist color and opacity when explicitly setting wireframe
|
||||||
|
if (color !== undefined) {
|
||||||
|
api.local.set('model.wireframe.color', color);
|
||||||
|
}
|
||||||
|
if (opacity !== undefined) {
|
||||||
|
api.local.set('model.wireframe.opacity', opacity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool = api.local.getBoolean('model.wireframe');
|
||||||
|
// Retrieve saved color/opacity if not provided
|
||||||
|
if (color === undefined) {
|
||||||
|
color = api.local.getInt('model.wireframe.color') || 0;
|
||||||
|
}
|
||||||
|
if (opacity === undefined) {
|
||||||
|
const saved = api.local.getFloat('model.wireframe.opacity');
|
||||||
|
opacity = saved !== null ? saved : (api.space.is_dark() ? 0.25 : 0.5);
|
||||||
|
}
|
||||||
|
api.widgets.each((w) => { w._setWireframe(bool, color, opacity) });
|
||||||
|
space.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle or set edge rendering for all widgets.
|
||||||
|
* If bool.toggle is truthy, toggles the current state.
|
||||||
|
* Otherwise sets edge visibility to bool value.
|
||||||
|
* Persists state to local storage and updates scene.
|
||||||
|
* @param {boolean|object} bool - Enable/disable edges, or {toggle: true} to toggle
|
||||||
|
* @param {boolean} [persist=true] - Whether to save state to local storage
|
||||||
|
*/
|
||||||
|
function setEdges(bool, persist = true) {
|
||||||
|
if (bool && bool.toggle) {
|
||||||
|
api.local.toggle('model.edges');
|
||||||
|
bool = api.local.getBoolean('model.edges');
|
||||||
|
} else {
|
||||||
|
if (persist) {
|
||||||
|
api.local.set('model.edges', bool);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
api.widgets.each(w => w._setEdges(bool));
|
||||||
|
space.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update slider maximum from stack height.
|
||||||
|
* Sets slider max to tallest stack layer - 1.
|
||||||
|
* If set=true or current hi exceeds new max, resets range to show top layer.
|
||||||
|
* @param {boolean} [set] - Force reset range to max
|
||||||
|
*/
|
||||||
|
function updateSliderMax(set) {
|
||||||
|
let max = STACKS.getRange().tallest - 1;
|
||||||
|
api.slider.setMax(max);
|
||||||
|
const { hi } = api.slider.getRange();
|
||||||
|
if (set || max < hi) {
|
||||||
|
api.slider.setRange(0, max);
|
||||||
|
api.slider.showLabels();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide slice visualization and restore widget rendering.
|
||||||
|
* Clears stack display, restores saved wireframe/opacity state.
|
||||||
|
*/
|
||||||
|
function hideSlices() {
|
||||||
|
STACKS.clear();
|
||||||
|
// Restore global visual state
|
||||||
|
api.widgets.each(widget => widget.applyGlobalVisualState());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show or hide all widgets in 3D scene.
|
||||||
|
* @param {boolean} bool - True to show, false to hide
|
||||||
|
*/
|
||||||
|
function setWidgetVisibility(bool) {
|
||||||
|
api.widgets.each(w => {
|
||||||
|
if (bool) {
|
||||||
|
w.show();
|
||||||
|
} else {
|
||||||
|
w.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show slice layer visualization.
|
||||||
|
* Only works in SLICE/PREVIEW/ANIMATE views (not ARRANGE).
|
||||||
|
* Updates slider range to show specified layer(s).
|
||||||
|
* If no layer specified, shows current hi layer.
|
||||||
|
* Expands range downward if needed (newLo = min(layer, lo)).
|
||||||
|
* Updates slider position and triggers stack visualization.
|
||||||
|
* @param {number|string} [layer] - Layer to show (defaults to current hi)
|
||||||
|
*/
|
||||||
|
function showSlices(layer) {
|
||||||
|
const viewMode = api.view.get();
|
||||||
|
if (viewMode === VIEWS.ARRANGE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showSlider();
|
||||||
|
|
||||||
|
const { lo, hi, max } = api.slider.getRange();
|
||||||
|
|
||||||
|
if (typeof(layer) === 'string' || typeof(layer) === 'number') {
|
||||||
|
layer = parseInt(layer);
|
||||||
|
} else {
|
||||||
|
layer = hi;
|
||||||
|
}
|
||||||
|
|
||||||
|
layer = bound(layer, 0, max);
|
||||||
|
const newLo = layer < lo ? layer : lo;
|
||||||
|
api.slider.setRange(newLo, layer);
|
||||||
|
api.slider.showLabels();
|
||||||
|
|
||||||
|
updateSlider();
|
||||||
|
// STACKS.setRange is called by slider callback
|
||||||
|
|
||||||
|
space.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show layer slider UI element.
|
||||||
|
*/
|
||||||
|
function showSlider() {
|
||||||
|
api.slider.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide layer slider and speed legend UI elements.
|
||||||
|
*/
|
||||||
|
function hideSlider() {
|
||||||
|
api.slider.hide();
|
||||||
|
api.ui.speeds.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronize stack label visibility with saved preferences.
|
||||||
|
* Reads visibility state from settings.labels for current mode/view combination.
|
||||||
|
* Applies visibility to all stack labels.
|
||||||
|
*/
|
||||||
|
function updateStackLabelState() {
|
||||||
|
const settings = api.conf.get();
|
||||||
|
// match label checkboxes to preference
|
||||||
|
for (let label of api.stacks.getLabels()) {
|
||||||
|
let check = `${settings.mode}-${api.view.get()}-${label}`;
|
||||||
|
api.stacks.setVisible(label, settings.labels[check] !== false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply saved visual state to a widget.
|
||||||
|
* Delegates to widget's applyGlobalVisualState method.
|
||||||
|
* Only applies in ARRANGE view mode.
|
||||||
|
* @param {Widget} widget - Widget to apply visual state to
|
||||||
|
*/
|
||||||
|
function applyVisualState(widget) {
|
||||||
|
const viewMode = api.view.get();
|
||||||
|
// Only apply visual state in ARRANGE mode
|
||||||
|
if (viewMode !== VIEWS.ARRANGE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
widget.applyGlobalVisualState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update progress bar display.
|
||||||
|
* In debug mode, also shows progress status message.
|
||||||
|
* @param {number} [value=0] - Progress value (0.0 to 1.0)
|
||||||
|
* @param {string} [msg] - Optional status message to display
|
||||||
|
*/
|
||||||
|
function setProgress(value = 0, msg) {
|
||||||
|
value = (value * 100).round(4);
|
||||||
|
api.ui.progress.width = value+'%';
|
||||||
|
if (self.debug) {
|
||||||
|
// console.log(msg, value.round(2));
|
||||||
|
api.ui.prostatus.style.display = 'flex';
|
||||||
|
if (msg) {
|
||||||
|
api.ui.prostatus.innerHTML = msg;
|
||||||
|
} else {
|
||||||
|
api.ui.prostatus.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let statsTimer;
|
||||||
|
let statsAdd;
|
||||||
|
|
||||||
|
function updateStats(add) {
|
||||||
|
statsAdd = add ? { ...add, 'hr': 1 } : {};
|
||||||
|
clearTimeout(statsTimer);
|
||||||
|
let { div, fps, rms, rnfo } = api.ui.stats;
|
||||||
|
if (api.devel.enabled === false) {
|
||||||
|
fps.innerText = '';
|
||||||
|
rms.innerText = '';
|
||||||
|
rnfo.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
div.style.display = 'flex';
|
||||||
|
statsTimer = setInterval(() => {
|
||||||
|
const nrms = space.view.getRMS().toFixed(1);
|
||||||
|
const nfps = space.view.getFPS().toFixed(1);
|
||||||
|
const rend = space.renderInfo();
|
||||||
|
const { memory, render } = rend;
|
||||||
|
if (nfps !== fps.innerText) {
|
||||||
|
fps.innerText = nfps;
|
||||||
|
}
|
||||||
|
if (nrms !== rms.innerText) {
|
||||||
|
rms.innerText = nrms;
|
||||||
|
}
|
||||||
|
if (rnfo.offsetParent !== null) {
|
||||||
|
rnfo.innerHTML = Object.entries({
|
||||||
|
...statsAdd,
|
||||||
|
...memory,
|
||||||
|
...render,
|
||||||
|
render_ms: nrms,
|
||||||
|
frames_sec: nfps
|
||||||
|
}).map(row => {
|
||||||
|
return row[0] === 'hr' ?
|
||||||
|
'<hr><hr>' :
|
||||||
|
`<div>${row[0]}</div><label>${base.util.comma(row[1])}</label>` ;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const visuals = {
|
||||||
|
apply_visual_state: applyVisualState,
|
||||||
|
hide_slices: hideSlices,
|
||||||
|
hide_slider: hideSlider,
|
||||||
|
set_edges: setEdges,
|
||||||
|
set_opacity: setOpacity,
|
||||||
|
set_progress: setProgress,
|
||||||
|
set_visible_layer: setVisibleLayer,
|
||||||
|
set_widget_visibility: setWidgetVisibility,
|
||||||
|
set_wireframe: setWireframe,
|
||||||
|
show_slices: showSlices,
|
||||||
|
show_slider: showSlider,
|
||||||
|
unit_scale: unitScale,
|
||||||
|
update_slider_max: updateSliderMax,
|
||||||
|
update_slider: updateSlider,
|
||||||
|
update_speeds: updateSpeeds,
|
||||||
|
update_stack_labels: updateStackLabelState,
|
||||||
|
update_stats: updateStats
|
||||||
|
};
|
||||||
404
src/kiri/app/widget.js
Normal file
404
src/kiri/app/widget.js
Normal file
|
|
@ -0,0 +1,404 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
import { Widget as WidgetCore } from '../core/widget.js';
|
||||||
|
import { util } from '../../geo/base.js';
|
||||||
|
import { avgc } from '../core/utils.js';
|
||||||
|
import { colorSchemeRegistry } from './color/schemes.js';
|
||||||
|
import { VisualStateManager } from './state/visual.js';
|
||||||
|
|
||||||
|
const solid_opacity = 1.0;
|
||||||
|
|
||||||
|
function catalog() { return self.kiri_catalog };
|
||||||
|
function index() { return self.kiri_catalog.index }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI-enhanced Widget subclass for main thread
|
||||||
|
* Adds rendering, persistence, and visual state management
|
||||||
|
*/
|
||||||
|
class Widget extends WidgetCore {
|
||||||
|
constructor(id, group) {
|
||||||
|
super(id, group);
|
||||||
|
// Visual state manager will be initialized when api is available
|
||||||
|
this.visualStateManager = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize visual state manager (called when api is available)
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_initVisualStateManager() {
|
||||||
|
if (this.visualStateManager || !this.api) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mode = this.api.mode.get_id();
|
||||||
|
const theme = this.api.space.is_dark() ? 'dark' : 'light';
|
||||||
|
const scheme = colorSchemeRegistry.getScheme(mode, theme);
|
||||||
|
|
||||||
|
this.visualStateManager = new VisualStateManager(this, scheme, this.api);
|
||||||
|
this.visualStateManager.loadPreferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current color scheme
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_getScheme() {
|
||||||
|
if (!this.api) return null;
|
||||||
|
const mode = this.api.mode.get_id();
|
||||||
|
const theme = this.api.space.is_dark() ? 'dark' : 'light';
|
||||||
|
return colorSchemeRegistry.getScheme(mode, theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push a temporary visual state (for operations like paint)
|
||||||
|
*/
|
||||||
|
pushVisualState(context, state) {
|
||||||
|
this._initVisualStateManager();
|
||||||
|
if (this.visualStateManager) {
|
||||||
|
return this.visualStateManager.pushState(context, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pop a temporary visual state
|
||||||
|
*/
|
||||||
|
popVisualState(context) {
|
||||||
|
if (this.visualStateManager) {
|
||||||
|
this.visualStateManager.popState(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save widget geometry to catalog
|
||||||
|
*/
|
||||||
|
saveToCatalog(filename, overwrite) {
|
||||||
|
if (!filename) {
|
||||||
|
filename = this.meta.file;
|
||||||
|
}
|
||||||
|
if (this.grouped && !overwrite) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
const widget = this;
|
||||||
|
const mark = Date.now();
|
||||||
|
const vertices = widget.getGeoVertices({ unroll: true }).slice();
|
||||||
|
widget.meta.file = filename;
|
||||||
|
widget.meta.save = mark;
|
||||||
|
catalog().putFile(filename, vertices, () => {
|
||||||
|
console.log("saved mesh ["+(vertices.length/3)+"] time ["+(Date.now()-mark)+"]");
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save widget state to index
|
||||||
|
*/
|
||||||
|
saveState(ondone) {
|
||||||
|
if (!ondone) {
|
||||||
|
clearTimeout(this._save_timer);
|
||||||
|
this._save_timer = setTimeout(() => {
|
||||||
|
this._save_timer = undefined;
|
||||||
|
this.saveState(() => {});
|
||||||
|
}, 1500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const widget = this;
|
||||||
|
index().put('ws-save-'+this.id, {
|
||||||
|
anno: this.annotations(),
|
||||||
|
geo: widget.getGeoVertices({ unroll: false }).slice(),
|
||||||
|
group: this.group.id,
|
||||||
|
meta: this.meta,
|
||||||
|
track: widget.track
|
||||||
|
}, result => {
|
||||||
|
widget.meta.saved = Date.now();
|
||||||
|
if (ondone) ondone();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set mesh material color
|
||||||
|
*/
|
||||||
|
setColor(color, settings, save = true) {
|
||||||
|
if (settings) {
|
||||||
|
console.trace('legacy call with settings');
|
||||||
|
}
|
||||||
|
if (Array.isArray(color)) {
|
||||||
|
color = color[this.getExtruder() % color.length];
|
||||||
|
}
|
||||||
|
if (save) {
|
||||||
|
this.color = color;
|
||||||
|
}
|
||||||
|
let material = this.getMaterial();
|
||||||
|
material.color.set(this.meta.disabled ? avgc(0x888888, color, 3) : color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current visual state
|
||||||
|
*/
|
||||||
|
getVisualState() {
|
||||||
|
return {
|
||||||
|
edges: this.outline ? true : false,
|
||||||
|
wires: this.wire ? true : false,
|
||||||
|
opacity: this.getMaterial().opacity
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set visual state
|
||||||
|
*/
|
||||||
|
setVisualState({ edges, wires, opacity}) {
|
||||||
|
this.cache.vizstate = this.getVisualState();
|
||||||
|
this._setEdges(edges ?? false);
|
||||||
|
this._setWireframe(wires ?? false);
|
||||||
|
// Only set opacity if explicitly provided (don't default to 1)
|
||||||
|
if (opacity !== undefined) {
|
||||||
|
this.setOpacity(opacity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh visual state
|
||||||
|
*/
|
||||||
|
refreshVisualState() {
|
||||||
|
this.setVisualState(this.getVisualState());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore previous visual state
|
||||||
|
*/
|
||||||
|
restoreVisualState() {
|
||||||
|
if (this.cache.vizstate) {
|
||||||
|
this.setVisualState(this.cache.vizstate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply global visual state from localStorage to this widget.
|
||||||
|
* Reads opacity, edges, and wireframe settings from global config.
|
||||||
|
* This is the primary method for syncing widget appearance with global preferences.
|
||||||
|
*/
|
||||||
|
applyGlobalVisualState() {
|
||||||
|
if (!this.api) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const material = this.getMaterial();
|
||||||
|
|
||||||
|
// Apply global opacity
|
||||||
|
const opacity = this.api.local.getFloat('model.opacity') ?? 1.0;
|
||||||
|
material.opacity = opacity;
|
||||||
|
material.transparent = opacity < 1.0;
|
||||||
|
material.visible = opacity > 0.0;
|
||||||
|
|
||||||
|
// Apply global edges
|
||||||
|
const edges = this.api.local.getBoolean('model.edges', false);
|
||||||
|
this._setEdges(edges);
|
||||||
|
|
||||||
|
// Apply global wireframe
|
||||||
|
const wireframe = this.api.local.getBoolean('model.wireframe', false);
|
||||||
|
if (wireframe) {
|
||||||
|
const color = this.api.local.getInt('model.wireframe.color') || 0;
|
||||||
|
const wfOpacity = this.api.local.getFloat('model.wireframe.opacity') ?? 0.5;
|
||||||
|
this._setWireframe(true, color, wfOpacity);
|
||||||
|
} else {
|
||||||
|
this._setWireframe(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set material opacity
|
||||||
|
*/
|
||||||
|
setOpacity(value) {
|
||||||
|
const mat = this.getMaterial();
|
||||||
|
if (value <= 0.0) {
|
||||||
|
mat.transparent = solid_opacity < 1.0;
|
||||||
|
mat.opacity = solid_opacity;
|
||||||
|
mat.visible = false;
|
||||||
|
} else if (util.inRange(value, 0.0, solid_opacity)) {
|
||||||
|
mat.transparent = value < 1.0;
|
||||||
|
mat.opacity = value;
|
||||||
|
mat.visible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set edge rendering - public interface for core widget rotation/scale
|
||||||
|
* Delegates to internal _setEdges method
|
||||||
|
*/
|
||||||
|
setEdges(set) {
|
||||||
|
this._setEdges(set);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set edge rendering (internal method - use api.view.set_edges for global control)
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_setEdges(set) {
|
||||||
|
this.__setEdges(set);
|
||||||
|
}
|
||||||
|
|
||||||
|
__setEdges(set) {
|
||||||
|
if (!(this.api && this.api.conf)) {
|
||||||
|
// missing api features in engine mode
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const mesh = this.mesh;
|
||||||
|
|
||||||
|
// Handle toggle
|
||||||
|
if (set && set.toggle) {
|
||||||
|
set = this.outline ? false : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove outline (but keep cache)
|
||||||
|
if (this.outline) {
|
||||||
|
mesh.remove(this.outline);
|
||||||
|
this.outline = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!set) {
|
||||||
|
return; // Just hide, keep cache
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve current angle and color
|
||||||
|
const scheme = this._getScheme();
|
||||||
|
const angle = scheme?.edges?.angle ?? this.api.conf.get().controller.edgeangle ?? 20;
|
||||||
|
const color = scheme?.edges?.color ?? 0x888888;
|
||||||
|
|
||||||
|
// Initialize cache if needed
|
||||||
|
if (!this.cache.edges) {
|
||||||
|
this.cache.edges = { geometry: null, angle: null, version: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const cache = this.cache.edges;
|
||||||
|
let edgesGeometry;
|
||||||
|
|
||||||
|
// Check if we can reuse cached geometry
|
||||||
|
if (cache.geometry &&
|
||||||
|
cache.angle === angle &&
|
||||||
|
cache.version === this.geomVersion) {
|
||||||
|
// Reuse cached geometry
|
||||||
|
edgesGeometry = cache.geometry;
|
||||||
|
} else {
|
||||||
|
// Regenerate edges
|
||||||
|
edgesGeometry = new THREE.EdgesGeometry(mesh.geometry, angle);
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
cache.geometry = edgesGeometry;
|
||||||
|
cache.angle = angle;
|
||||||
|
cache.version = this.geomVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create outline with cached/new geometry
|
||||||
|
const material = new THREE.LineBasicMaterial({ color });
|
||||||
|
this.outline = new THREE.LineSegments(edgesGeometry, material);
|
||||||
|
this.outline.renderOrder = -20;
|
||||||
|
mesh.add(this.outline);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set wireframe rendering - public interface for core widget rotation/scale
|
||||||
|
* Delegates to internal _setWireframe method
|
||||||
|
*/
|
||||||
|
setWireframe(set, color, opacity) {
|
||||||
|
this._setWireframe(set, color, opacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set wireframe rendering (internal method - use api.view.set_wireframe for global control)
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_setWireframe(set, color, opacity) {
|
||||||
|
if (!(this.api && this.api.conf)) {
|
||||||
|
// missing api features in engine mode
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mesh = this.mesh,
|
||||||
|
widget = this;
|
||||||
|
// Save current opacity before removing wire
|
||||||
|
const currentOpacity = this.getMaterial().opacity;
|
||||||
|
if (this.wire) {
|
||||||
|
this.setOpacity(solid_opacity);
|
||||||
|
mesh.remove(this.wire);
|
||||||
|
this.wire = null;
|
||||||
|
}
|
||||||
|
if (set) {
|
||||||
|
const scheme = this._getScheme();
|
||||||
|
const wireColor = scheme?.wireframe?.color ?? 0xaaaaaa;
|
||||||
|
const wireOpacity = scheme?.wireframe?.opacity ?? 0.5;
|
||||||
|
const mat = new THREE.MeshBasicMaterial({
|
||||||
|
wireframe: true,
|
||||||
|
color: wireColor,
|
||||||
|
opacity: wireOpacity,
|
||||||
|
transparent: true
|
||||||
|
});
|
||||||
|
const wire = widget.wire = new THREE.Mesh(mesh.geometry.shallowClone(), mat);
|
||||||
|
mesh.add(wire);
|
||||||
|
}
|
||||||
|
if (this.api.view.is_arrange()) {
|
||||||
|
this.setColor(this.color);
|
||||||
|
} else {
|
||||||
|
this.setColor(0x888888,undefined,false);
|
||||||
|
}
|
||||||
|
if (opacity !== undefined) {
|
||||||
|
widget.setOpacity(opacity);
|
||||||
|
} else if (!set && this.wire === null) {
|
||||||
|
// Restore original opacity when removing wireframe without explicit opacity
|
||||||
|
widget.setOpacity(currentOpacity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load widget from catalog
|
||||||
|
*/
|
||||||
|
static loadFromCatalog(filename, ondone) {
|
||||||
|
catalog().getFile(filename, function(data) {
|
||||||
|
let widget = new Widget().loadVertices(data);
|
||||||
|
widget.meta.file = filename;
|
||||||
|
ondone(widget);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load widget from saved state
|
||||||
|
*/
|
||||||
|
static loadFromState(id, ondone, move) {
|
||||||
|
const Group = Widget.Groups;
|
||||||
|
index().get('ws-save-'+id, function(data) {
|
||||||
|
if (data) {
|
||||||
|
let vertices = data.geo || data,
|
||||||
|
track = data.track || undefined,
|
||||||
|
group = data.group || id,
|
||||||
|
anno = data.anno || undefined,
|
||||||
|
widget = new Widget(id, Group.forid(group)),
|
||||||
|
meta = data.meta || widget.meta,
|
||||||
|
ptr = widget.loadVertices(vertices);
|
||||||
|
widget.meta = meta;
|
||||||
|
widget.anno = anno || widget.anno;
|
||||||
|
// restore widget position if specified
|
||||||
|
if (move && track && track.pos) {
|
||||||
|
widget.track = track;
|
||||||
|
widget.move(track.pos.x, track.pos.y, track.pos.z, true);
|
||||||
|
}
|
||||||
|
ondone(ptr);
|
||||||
|
} else {
|
||||||
|
ondone(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete widget from saved state
|
||||||
|
*/
|
||||||
|
static deleteFromState(id,ondone) {
|
||||||
|
index().remove('ws-save-'+id, ondone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory function to create UI-enhanced widgets
|
||||||
|
*/
|
||||||
|
function newWidget(id, group) {
|
||||||
|
return new Widget(id, group);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Widget, newWidget };
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
import { $ } from '../../moto/webui.js';
|
import { $ } from '../../moto/webui.js';
|
||||||
import { api } from './api.js';
|
import { api } from '../app/api.js';
|
||||||
import { load } from '../../load/file.js';
|
import { load } from '../../load/file.js';
|
||||||
import { Widget, newWidget } from './widget.js';
|
import { Widget, newWidget } from '../app/widget.js';
|
||||||
|
|
||||||
let WIDGETS = [];
|
let WIDGETS = [];
|
||||||
|
|
||||||
|
|
@ -105,9 +105,9 @@ export const widgets = {
|
||||||
replace,
|
replace,
|
||||||
opacity(o) { setOpacity(o); console.trace('opacity() deprecated') },
|
opacity(o) { setOpacity(o); console.trace('opacity() deprecated') },
|
||||||
annotate,
|
annotate,
|
||||||
setIndexed,
|
|
||||||
setAxisIndex,
|
setAxisIndex,
|
||||||
setColor(c,s) { api.widgets.each(widget => widget.setColor(c,undefined,s) )},
|
setColor(c,s) { api.widgets.each(widget => widget.setColor(c,undefined,s) )},
|
||||||
|
setIndexed,
|
||||||
setOpacity,
|
setOpacity,
|
||||||
all() { return WIDGETS.slice() },
|
all() { return WIDGETS.slice() },
|
||||||
add(w) { return WIDGETS.push(w) },
|
add(w) { return WIDGETS.push(w) },
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
import { api } from './api.js';
|
import { api } from './api.js';
|
||||||
import { noop } from './utils.js';
|
import { noop } from '../core/utils.js';
|
||||||
import { codec } from './codec.js';
|
import { codec } from '../core/codec.js';
|
||||||
import { Widget } from './widget.js';
|
import { Widget } from './widget.js';
|
||||||
import { newPrint } from './print.js';
|
import { newPrint } from '../core/print.js';
|
||||||
|
|
||||||
|
const WORKPATH = "../lib/kiri/run/worker.js";
|
||||||
|
|
||||||
// this code runs in kiri's main loop
|
// this code runs in kiri's main loop
|
||||||
let debug = self.debug === true,
|
let debug = self.debug === true,
|
||||||
|
|
@ -19,7 +21,7 @@ let debug = self.debug === true,
|
||||||
poolpath;
|
poolpath;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Function} fn name of function in kiri.worker
|
* @param {Function} fn name of function in worker
|
||||||
* @param {Object} data to send to server
|
* @param {Object} data to send to server
|
||||||
* @param {Function} onreply function to call on reply messages
|
* @param {Function} onreply function to call on reply messages
|
||||||
* @param {Object[]} zerocopy array of objects to pass using zerocopy
|
* @param {Object[]} zerocopy array of objects to pass using zerocopy
|
||||||
|
|
@ -31,14 +33,17 @@ function send(fn, data, onreply, zerocopy) {
|
||||||
// track it only when we expect and can handle a reply
|
// track it only when we expect and can handle a reply
|
||||||
running[seq] = { fn:onreply };
|
running[seq] = { fn:onreply };
|
||||||
}
|
}
|
||||||
|
|
||||||
let msg = {
|
let msg = {
|
||||||
seq: seq,
|
seq: seq,
|
||||||
task: fn,
|
task: fn,
|
||||||
time: time(),
|
time: time(),
|
||||||
data: data
|
data: data
|
||||||
};
|
};
|
||||||
|
|
||||||
// console.log('client send', msg);
|
// console.log('client send', msg);
|
||||||
// if (!fn) { console.trace({empty_fn: data}) };
|
// if (!fn) { console.trace({empty_fn: data}) };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
worker.postMessage(msg, zerocopy);
|
worker.postMessage(msg, zerocopy);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -46,9 +51,18 @@ function send(fn, data, onreply, zerocopy) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Promise wrapper around send()
|
||||||
|
function sendp(fn, data, zerocopy) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
send(fn, data, resolve, zerocopy);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// code is running in the browser / client context
|
// code is running in the browser / client context
|
||||||
export const client = {
|
export const client = {
|
||||||
send: send,
|
send,
|
||||||
|
|
||||||
|
sendp,
|
||||||
|
|
||||||
setWorkPath(path) {
|
setWorkPath(path) {
|
||||||
workpath = path;
|
workpath = path;
|
||||||
|
|
@ -80,7 +94,7 @@ export const client = {
|
||||||
const blob = new Blob([ work ], { type: 'application/javascript' });
|
const blob = new Blob([ work ], { type: 'application/javascript' });
|
||||||
return new Worker(URL.createObjectURL(blob));
|
return new Worker(URL.createObjectURL(blob));
|
||||||
} else {
|
} else {
|
||||||
let worker = new Worker(workpath || api.const.PATHS.work, { type: 'module' });
|
let worker = new Worker(workpath || WORKPATH, { type: 'module' });
|
||||||
worker.onerror = (error) => {
|
worker.onerror = (error) => {
|
||||||
console.log({ WORKER_ERROR: error });
|
console.log({ WORKER_ERROR: error });
|
||||||
error.preventDefault();
|
error.preventDefault();
|
||||||
|
|
@ -186,11 +200,12 @@ export const client = {
|
||||||
let vertices = widget.getGeoVertices();
|
let vertices = widget.getGeoVertices();
|
||||||
send("sync", {
|
send("sync", {
|
||||||
id: widget.id,
|
id: widget.id,
|
||||||
meta: widget.meta,
|
anno: widget.annotations(),
|
||||||
group: widget.group.id,
|
group: widget.group.id,
|
||||||
|
meta: widget.meta,
|
||||||
|
position: widget.mesh.position,
|
||||||
track: widget.track,
|
track: widget.track,
|
||||||
vertices,
|
vertices,
|
||||||
position: widget.mesh.position,
|
|
||||||
}, done => {
|
}, done => {
|
||||||
widget.modified = false;
|
widget.modified = false;
|
||||||
});
|
});
|
||||||
|
|
@ -211,11 +226,17 @@ export const client = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
// called once at the beginning of slice operations
|
||||||
|
// setup any global shared state
|
||||||
|
slicePre(settings, callback) {
|
||||||
|
send("slicePre", { settings }, callback);
|
||||||
|
},
|
||||||
|
|
||||||
// called once for each widget
|
// called once for each widget
|
||||||
slice(settings, widget, callback) {
|
slice(settings, widget, callback) {
|
||||||
send("slice", {
|
send("slice", {
|
||||||
id: widget.id,
|
id: widget.id,
|
||||||
anno: widget.annotations(),
|
|
||||||
settings: settings
|
settings: settings
|
||||||
}, reply => {
|
}, reply => {
|
||||||
callback(reply);
|
callback(reply);
|
||||||
|
|
@ -230,8 +251,8 @@ export const client = {
|
||||||
|
|
||||||
// called once after each widget is sliced
|
// called once after each widget is sliced
|
||||||
// performs merged actions that consider all widget slice data
|
// performs merged actions that consider all widget slice data
|
||||||
sliceAll(settings, callback) {
|
slicePost(settings, callback) {
|
||||||
send("sliceAll", { settings }, callback);
|
send("slicePost", { settings }, callback);
|
||||||
},
|
},
|
||||||
|
|
||||||
// turn widget slices into unified path output
|
// turn widget slices into unified path output
|
||||||
|
|
@ -262,7 +283,7 @@ export const client = {
|
||||||
ondone(null, reply.error);
|
ondone(null, reply.error);
|
||||||
}
|
}
|
||||||
if (reply.debug) {
|
if (reply.debug) {
|
||||||
api.event.emit("export.debug", reply.debug);
|
console.log('worker debug', reply.debug);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
234
src/kiri/app/workspace.js
Normal file
234
src/kiri/app/workspace.js
Normal file
|
|
@ -0,0 +1,234 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
import { THREE } from '../../ext/three.js';
|
||||||
|
import { api } from './api.js';
|
||||||
|
import { local as SDB } from '../../data/local.js';
|
||||||
|
import { openFiles } from './files.js';
|
||||||
|
import { platform } from './platform.js';
|
||||||
|
import { selection } from './selected.js';
|
||||||
|
import { settings } from './conf/manager.js';
|
||||||
|
import { space as SPACE } from '../../moto/space.js';
|
||||||
|
import { utils } from '../core/utils.js';
|
||||||
|
import { Widget } from './widget.js';
|
||||||
|
import { Index } from '../../data/index.js';
|
||||||
|
|
||||||
|
const { o2js, js2o, ls2o, parseOpt } = utils;
|
||||||
|
const LOC = self.location;
|
||||||
|
const SETUP = parseOpt(LOC.search.substring(1));
|
||||||
|
const FILES = openFiles(new Index(SETUP.d ? SETUP.d[0] : 'kiri'));
|
||||||
|
|
||||||
|
/** Auto-save timer handle */
|
||||||
|
let autoSaveTimer = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload the application page.
|
||||||
|
* Emits 'reload' event and defers reload until busy count reaches zero.
|
||||||
|
*/
|
||||||
|
function reload() {
|
||||||
|
api.event.emit('reload');
|
||||||
|
do_reload(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform page reload after ensuring async operations complete.
|
||||||
|
* Checks busy state and defers reload until idle.
|
||||||
|
* Retries with exponential backoff (100ms -> 250ms).
|
||||||
|
* @param {number} [time=100] - Initial delay in milliseconds
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function do_reload(time) {
|
||||||
|
// allow time for async saves to complete and busy to to to zero
|
||||||
|
setTimeout(() => {
|
||||||
|
if (api.busy.val() === 0) {
|
||||||
|
LOC.reload();
|
||||||
|
} else {
|
||||||
|
console.log(`reload deferred on busy=${api.busy.val()}`);
|
||||||
|
do_reload(250);
|
||||||
|
}
|
||||||
|
}, time || 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger delayed auto-save if enabled.
|
||||||
|
* Debounces saves with 1-second delay.
|
||||||
|
* Only saves if controller.autoSave is enabled.
|
||||||
|
*/
|
||||||
|
function auto_save() {
|
||||||
|
if (!settings.ctrl().autoSave) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearTimeout(autoSaveTimer);
|
||||||
|
autoSaveTimer = setTimeout(() => {
|
||||||
|
api.space.save(true);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set camera focus on widgets or point.
|
||||||
|
* If point provided, focuses on that point.
|
||||||
|
* Otherwise calculates centroid of selection (or all widgets if sel undefined/empty).
|
||||||
|
* For CAM indexed mode, uses Z=0 focus height. Otherwise uses half of top_z.
|
||||||
|
* @param {Widget|Array<Widget>} [sel] - Widget(s) to focus on (defaults to all)
|
||||||
|
* @param {object} [point] - Optional point {x, y, z} to focus on directly
|
||||||
|
*/
|
||||||
|
function set_focus(sel, point) {
|
||||||
|
if (point) {
|
||||||
|
SPACE.platform.setCenter(point.x, point.z, point.y);
|
||||||
|
SPACE.view.setFocus(new THREE.Vector3(point.x, point.y, point.z));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sel === undefined) {
|
||||||
|
sel = api.widgets.all();
|
||||||
|
} else if (!Array.isArray) {
|
||||||
|
sel = [ sel ];
|
||||||
|
} else if (sel.length === 0) {
|
||||||
|
sel = api.widgets.all();
|
||||||
|
}
|
||||||
|
let pos = { x:0, y:0, z:0 };
|
||||||
|
for (let widget of sel) {
|
||||||
|
pos.x += widget.track.pos.x;
|
||||||
|
pos.y += widget.track.pos.y;
|
||||||
|
pos.z += widget.track.pos.z;
|
||||||
|
}
|
||||||
|
if (sel.length) {
|
||||||
|
pos.x /= sel.length;
|
||||||
|
pos.y /= sel.length;
|
||||||
|
pos.z /= sel.length;
|
||||||
|
}
|
||||||
|
let cam_index = api.conf.get().process.camStockIndexed || false;
|
||||||
|
let focus_z = cam_index ? 0 : platform.top_z() / 2;
|
||||||
|
SPACE.platform.setCenter(pos.x, -pos.y, focus_z);
|
||||||
|
SPACE.view.setFocus(new THREE.Vector3(pos.x, focus_z, -pos.y));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save workspace state to local storage.
|
||||||
|
* Saves:
|
||||||
|
* - Configuration settings
|
||||||
|
* - Widget list and metadata
|
||||||
|
* - Widget positions and states
|
||||||
|
* Removes stale widget data for deleted widgets.
|
||||||
|
* Shows alert unless quiet=true.
|
||||||
|
* @param {boolean} [quiet] - Suppress "workspace saved" alert
|
||||||
|
*/
|
||||||
|
function save(quiet) {
|
||||||
|
api.conf.save();
|
||||||
|
const newWidgets = [];
|
||||||
|
const oldWidgets = js2o(SDB.getItem('ws-widgets'), []);
|
||||||
|
api.widgets.each(widget => {
|
||||||
|
if (widget.synth) return;
|
||||||
|
newWidgets.push(widget.id);
|
||||||
|
oldWidgets.remove(widget.id);
|
||||||
|
widget.saveState();
|
||||||
|
let ann = api.widgets.annotate(widget.id);
|
||||||
|
ann.file = widget.meta.file;
|
||||||
|
ann.url = widget.meta.url;
|
||||||
|
});
|
||||||
|
SDB.setItem('ws-widgets', o2js(newWidgets));
|
||||||
|
oldWidgets.forEach(wid => {
|
||||||
|
Widget.deleteFromState(wid);
|
||||||
|
});
|
||||||
|
// eliminate dangling saved widgets
|
||||||
|
FILES.deleteFilter(key => newWidgets.indexOf(key.substring(8)) < 0, "ws-save-", "ws-savf");
|
||||||
|
if (!quiet) {
|
||||||
|
api.show.alert("workspace saved", 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore workspace from local storage.
|
||||||
|
* Restores:
|
||||||
|
* - Configuration settings
|
||||||
|
* - Camera view position
|
||||||
|
* - All saved widgets from storage
|
||||||
|
* Clears existing platform widgets unless skip_widget_load=true.
|
||||||
|
* Calls ondone callback when complete.
|
||||||
|
* @param {Function} [ondone] - Callback when restore completes
|
||||||
|
* @param {boolean} [skip_widget_load] - Skip loading widgets (config only)
|
||||||
|
* @returns {boolean} True if widgets were queued for loading
|
||||||
|
*/
|
||||||
|
function restore(ondone, skip_widget_load) {
|
||||||
|
let newset = api.conf.restore(false),
|
||||||
|
camera = newset.controller.view,
|
||||||
|
toload = ls2o('ws-widgets',[]),
|
||||||
|
loaded = 0,
|
||||||
|
position = true;
|
||||||
|
|
||||||
|
api.conf.update_fields();
|
||||||
|
platform.update_size();
|
||||||
|
|
||||||
|
SPACE.view.reset();
|
||||||
|
if (camera) {
|
||||||
|
SPACE.view.load(camera);
|
||||||
|
} else {
|
||||||
|
SPACE.view.home();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skip_widget_load) {
|
||||||
|
ondone && ondone();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove any widgets from platform
|
||||||
|
api.widgets.each(function(widget) {
|
||||||
|
platform.delete(widget);
|
||||||
|
});
|
||||||
|
|
||||||
|
// load any widget by name that was saved to the workspace
|
||||||
|
// after the last widget is loaded, call ondone()
|
||||||
|
for (let widgetid of toload) {
|
||||||
|
Widget.loadFromState(widgetid, (widget) => {
|
||||||
|
if (widget) {
|
||||||
|
platform.add(widget, 0, position, true);
|
||||||
|
let ann = api.widgets.annotate(widgetid);
|
||||||
|
widget.meta.file = ann.file;
|
||||||
|
widget.meta.url = ann.url;
|
||||||
|
}
|
||||||
|
if (++loaded === toload.length) {
|
||||||
|
platform.deselect();
|
||||||
|
if (ondone) {
|
||||||
|
ondone();
|
||||||
|
setTimeout(() => {
|
||||||
|
platform.update_bounds();
|
||||||
|
SPACE.update();
|
||||||
|
}, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, position);
|
||||||
|
};
|
||||||
|
|
||||||
|
// when there is nothing async to load, call ondone()
|
||||||
|
if (toload.length === 0 && ondone) {
|
||||||
|
ondone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all widgets from workspace.
|
||||||
|
* Clears worker cache/memory, selects all widgets, and deletes them.
|
||||||
|
*/
|
||||||
|
function clear() {
|
||||||
|
// free up worker cache/mem
|
||||||
|
api.client.clear();
|
||||||
|
platform.select_all();
|
||||||
|
platform.delete(selection.meshes());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if dark mode is enabled.
|
||||||
|
* @returns {boolean} True if dark mode active
|
||||||
|
*/
|
||||||
|
function is_dark() {
|
||||||
|
return settings.ctrl().dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const workspace = {
|
||||||
|
auto_save,
|
||||||
|
clear,
|
||||||
|
is_dark,
|
||||||
|
reload,
|
||||||
|
restore,
|
||||||
|
save,
|
||||||
|
set_focus,
|
||||||
|
update: SPACE.update,
|
||||||
|
};
|
||||||
|
|
@ -1,200 +0,0 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
|
||||||
|
|
||||||
import alerts from './alerts.js';
|
|
||||||
import settings from './settings.js'
|
|
||||||
import STACKS from './stacks.js';
|
|
||||||
|
|
||||||
import { broker } from '../../moto/broker.js';
|
|
||||||
import { client as work } from './client.js';
|
|
||||||
import { consts, COLOR as color, LISTS as lists } from './consts.js';
|
|
||||||
import { device, devices } from './devices.js';
|
|
||||||
import { catalog, dialog, event, group, help, hide, image } from './main.js';
|
|
||||||
import { modal, mode, process, show, space, util, view } from './main.js';
|
|
||||||
import { local as dataLocal } from '../../data/local.js';
|
|
||||||
import { noop, ajax, o2js, js2o } from './utils.js';
|
|
||||||
import { functions } from './function.js';
|
|
||||||
import { platform } from './platform.js';
|
|
||||||
import { selection } from './selection.js';
|
|
||||||
import { stats } from './stats.js';
|
|
||||||
import { newWidget } from './widget.js';
|
|
||||||
import { widgets } from './widgets.js';
|
|
||||||
import { updateTool } from '../mode/cam/tools.js';
|
|
||||||
import { beta, version } from '../../moto/license.js';
|
|
||||||
import { space as SPACE } from '../../moto/space.js';
|
|
||||||
import { types as load } from '../../load/file.js';
|
|
||||||
|
|
||||||
import { LANG } from './lang.js';
|
|
||||||
import { LOCAL, SETUP, SECURE } from './main.js';
|
|
||||||
import { UI } from './ui.js';
|
|
||||||
|
|
||||||
import web from '../../moto/webui.js';
|
|
||||||
|
|
||||||
let UC = UI.prefix('kiri').inputAction(settings.conf.update),
|
|
||||||
und = undefined,
|
|
||||||
clone = Object.clone,
|
|
||||||
isHover = false,
|
|
||||||
feature = {
|
|
||||||
seed: true, // seed profiles on first use
|
|
||||||
meta: true, // show selected widget metadata
|
|
||||||
frame: true, // receive frame events
|
|
||||||
alert_event: false, // emit alerts as events instead of display
|
|
||||||
controls: true, // show or not side menus
|
|
||||||
device_filter: und, // function to limit devices shown
|
|
||||||
drop_group: und, // optional array to group multi drop
|
|
||||||
drop_layout: true, // layout on new drop
|
|
||||||
hoverAdds: false, // when true only searches widget additions
|
|
||||||
on_key: und, // function override default key handlers
|
|
||||||
on_key2: [], // allows for multiple key handlers
|
|
||||||
on_load: und, // function override file drop loads
|
|
||||||
on_add_stl: und, // legacy override stl drop loads
|
|
||||||
on_mouse_up: und, // function intercepts mouse up select
|
|
||||||
on_mouse_down: und, // function intercepts mouse down
|
|
||||||
work_alerts: true, // allow disabling work progress alerts
|
|
||||||
pmode: consts.PMODES.SPEED, // preview modes
|
|
||||||
// hover: false, // when true fires mouse hover events
|
|
||||||
get hover() {
|
|
||||||
return isHover;
|
|
||||||
},
|
|
||||||
set hover(b) {
|
|
||||||
isHover = b;
|
|
||||||
broker.publish("feature.hover", b);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
busyVal = 0,
|
|
||||||
busy = {
|
|
||||||
val() { return busyVal },
|
|
||||||
inc() { api.event.emit("busy", ++busyVal) },
|
|
||||||
dec() { api.event.emit("busy", --busyVal) }
|
|
||||||
},
|
|
||||||
onkey = (fn) => {
|
|
||||||
api.feature.on_key2.push(fn);
|
|
||||||
},
|
|
||||||
doit = {
|
|
||||||
undo: noop, // do.js
|
|
||||||
redo: noop // do.js
|
|
||||||
},
|
|
||||||
devel = {
|
|
||||||
get enabled() {
|
|
||||||
return settings.ctrl().devel;
|
|
||||||
},
|
|
||||||
xray(layers, raw) {
|
|
||||||
let proc = api.conf.get().process,
|
|
||||||
size = proc.sliceHeight || proc.slaSlice || 1,
|
|
||||||
base = (proc.firstSliceHeight || size);
|
|
||||||
layers = Array.isArray(layers) ? layers : [layers];
|
|
||||||
proc.xray = layers.map(l => raw ? l : base + l * size - size / 2);
|
|
||||||
proc.xrayi = layers.slice();
|
|
||||||
api.function.slice();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
local = {
|
|
||||||
get: (key) => localGet(key),
|
|
||||||
getItem: (key) => localGet(key),
|
|
||||||
getInt: (key) => parseInt(localGet(key)),
|
|
||||||
getFloat: (key) => parseFloat(localGet(key)),
|
|
||||||
getBoolean: (key, def = true) => {
|
|
||||||
let val = localGet(key);
|
|
||||||
return val === true || val === 'true' || val === def;
|
|
||||||
},
|
|
||||||
toggle: (key, val, def) => localSet(key, val ?? !api.local.getBoolean(key, def)),
|
|
||||||
put: (key, val) => localSet(key, val),
|
|
||||||
set: (key, val) => localSet(key, val),
|
|
||||||
setItem: (key, val) => localSet(key, val),
|
|
||||||
removeItem: (key) => localRemove(key)
|
|
||||||
},
|
|
||||||
tweak = {
|
|
||||||
line_precision(v) { api.work.config({ base: { clipperClean: v } }) },
|
|
||||||
gcode_decimals(v) { api.work.config({ base: { gcode_decimals: v } }) }
|
|
||||||
};
|
|
||||||
|
|
||||||
function clip(text) {
|
|
||||||
navigator.clipboard
|
|
||||||
.writeText(text)
|
|
||||||
.catch(err => console.error('Clipboard Error:', err));
|
|
||||||
}
|
|
||||||
|
|
||||||
function localGet(key) {
|
|
||||||
let sloc = api.conf.get().local;
|
|
||||||
return sloc[key] || api.sdb[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
function localSet(key, val) {
|
|
||||||
let sloc = api.conf.get().local;
|
|
||||||
sloc[key] = api.sdb[key] = val;
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
|
|
||||||
function localRemove(key) {
|
|
||||||
let sloc = api.conf.get().local;
|
|
||||||
return delete sloc[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const api = {
|
|
||||||
ajax,
|
|
||||||
beta,
|
|
||||||
alerts,
|
|
||||||
busy,
|
|
||||||
catalog,
|
|
||||||
client: work,
|
|
||||||
clip,
|
|
||||||
clone,
|
|
||||||
color,
|
|
||||||
conf: settings.conf,
|
|
||||||
const: { LANG, LOCAL, SETUP, SECURE, STACKS, ...consts },
|
|
||||||
devel,
|
|
||||||
device,
|
|
||||||
devices,
|
|
||||||
dialog,
|
|
||||||
doit,
|
|
||||||
event,
|
|
||||||
electron: navigator.userAgent.includes('Electron'),
|
|
||||||
feature,
|
|
||||||
function: functions,
|
|
||||||
group,
|
|
||||||
help,
|
|
||||||
hide,
|
|
||||||
image,
|
|
||||||
js2o,
|
|
||||||
language: LANG,
|
|
||||||
lists,
|
|
||||||
load,
|
|
||||||
local,
|
|
||||||
modal,
|
|
||||||
mode,
|
|
||||||
new: {
|
|
||||||
widget: newWidget
|
|
||||||
},
|
|
||||||
noop,
|
|
||||||
o2js,
|
|
||||||
onkey,
|
|
||||||
platform,
|
|
||||||
process,
|
|
||||||
sdb: dataLocal,
|
|
||||||
selection,
|
|
||||||
settings,
|
|
||||||
show,
|
|
||||||
space,
|
|
||||||
SPACE,
|
|
||||||
stacks: STACKS,
|
|
||||||
stats,
|
|
||||||
tool: {
|
|
||||||
update: updateTool
|
|
||||||
},
|
|
||||||
tweak,
|
|
||||||
uc: UC,
|
|
||||||
ui: {},
|
|
||||||
util,
|
|
||||||
var: {
|
|
||||||
layer_lo: 0,
|
|
||||||
layer_hi: 0,
|
|
||||||
layer_max: 0
|
|
||||||
},
|
|
||||||
version,
|
|
||||||
view,
|
|
||||||
web,
|
|
||||||
widgets,
|
|
||||||
work,
|
|
||||||
};
|
|
||||||
|
|
||||||
// allow widget to straddle client / worker FOR NOW
|
|
||||||
self.kiri_api = api;
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
import { Layers } from './layers.js';
|
import { Layers } from '../app/layers.js';
|
||||||
import { newPoint } from '../../geo/point.js';
|
import { newPoint } from '../../geo/point.js';
|
||||||
import { Polygon, newPolygon } from '../../geo/polygon.js';
|
import { Polygon, newPolygon } from '../../geo/polygon.js';
|
||||||
import { Slice, newSlice, Top, newTop } from './slice.js';
|
import { Slice, newSlice, Top, newTop } from './slice.js';
|
||||||
import { Widget, newWidget } from './widget.js';
|
import { Widget, newWidget } from './widget.js';
|
||||||
|
|
||||||
const decoders = {};
|
const decoders = {};
|
||||||
|
const factories = {};
|
||||||
const freeMem = true;
|
const freeMem = true;
|
||||||
|
|
||||||
const TYPE = {
|
const TYPE = {
|
||||||
|
|
@ -213,6 +214,11 @@ function decodePointArray2D(array, z, fn) {
|
||||||
return points;
|
return points;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setFactory(type, factory) {
|
||||||
|
factories[type] = factory;
|
||||||
|
return codec;
|
||||||
|
}
|
||||||
|
|
||||||
// ----- Widget Codec -----
|
// ----- Widget Codec -----
|
||||||
|
|
||||||
Widget.prototype.encode = function(state) {
|
Widget.prototype.encode = function(state) {
|
||||||
|
|
@ -236,7 +242,8 @@ registerDecoder(TYPE.WIDGET, function(v, state) {
|
||||||
const id = v.id,
|
const id = v.id,
|
||||||
group = v.group || id,
|
group = v.group || id,
|
||||||
track = v.track || undefined,
|
track = v.track || undefined,
|
||||||
widget = newWidget(id, Widget.Groups.forid(group)),
|
factory = factories[TYPE.WIDGET] ?? newWidget,
|
||||||
|
widget = factory(id, Widget.Groups.forid(group)),
|
||||||
meta= v.meta || widget.meta,
|
meta= v.meta || widget.meta,
|
||||||
anno = v.anno || widget.anno;
|
anno = v.anno || widget.anno;
|
||||||
|
|
||||||
|
|
@ -480,6 +487,7 @@ registerDecoder(TYPE.LAYERS, function(v, state) {
|
||||||
});
|
});
|
||||||
|
|
||||||
export const codec = {
|
export const codec = {
|
||||||
|
TYPE,
|
||||||
encode,
|
encode,
|
||||||
decode,
|
decode,
|
||||||
registerDecoder,
|
registerDecoder,
|
||||||
|
|
@ -488,10 +496,12 @@ export const codec = {
|
||||||
decodePointArray,
|
decodePointArray,
|
||||||
encodePointArray2D,
|
encodePointArray2D,
|
||||||
decodePointArray2D,
|
decodePointArray2D,
|
||||||
|
setFactory,
|
||||||
toCodable
|
toCodable
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
TYPE,
|
||||||
encode,
|
encode,
|
||||||
decode,
|
decode,
|
||||||
registerDecoder,
|
registerDecoder,
|
||||||
|
|
@ -500,6 +510,7 @@ export {
|
||||||
decodePointArray,
|
decodePointArray,
|
||||||
encodePointArray2D,
|
encodePointArray2D,
|
||||||
decodePointArray2D,
|
decodePointArray2D,
|
||||||
|
setFactory,
|
||||||
toCodable
|
toCodable
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,918 +0,0 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
|
||||||
|
|
||||||
import './frame.js';
|
|
||||||
import '../../ext/base64.js';
|
|
||||||
|
|
||||||
import lang from './lang.js';
|
|
||||||
import STACKS from './stacks.js';
|
|
||||||
|
|
||||||
import { $ } from '../../moto/webui.js';
|
|
||||||
import { api } from './api.js';
|
|
||||||
import { broker } from '../../moto/broker.js';
|
|
||||||
import { consts } from './consts.js';
|
|
||||||
import { Index } from '../../data/index.js';
|
|
||||||
import { local as dataLocal } from '../../data/local.js';
|
|
||||||
import { openFiles } from './files.js';
|
|
||||||
import { platform } from './platform.js';
|
|
||||||
import { selection } from './selection.js';
|
|
||||||
import { settings } from './settings.js';
|
|
||||||
import { showDevices } from './devices.js';
|
|
||||||
import { space as SPACE } from '../../moto/space.js';
|
|
||||||
import { stats } from './stats.js';
|
|
||||||
import { noop, utils } from './utils.js';
|
|
||||||
import { version } from '../../moto/license.js';
|
|
||||||
import { Widget, newWidget } from './widget.js';
|
|
||||||
import { showTools } from '../mode/cam/tools.js';
|
|
||||||
|
|
||||||
let { parseOpt, o2js, js2o, ls2o } = utils,
|
|
||||||
{ COLOR, MODES, VIEWS } = consts,
|
|
||||||
LANG = lang.current,
|
|
||||||
WIN = self.window,
|
|
||||||
DOC = self.document,
|
|
||||||
LOC = self.location,
|
|
||||||
SETUP = parseOpt(LOC.search.substring(1)),
|
|
||||||
SECURE = isSecure(LOC.protocol),
|
|
||||||
LOCAL = self.debug && !SETUP.remote,
|
|
||||||
EVENT = broker,
|
|
||||||
SDB = dataLocal,
|
|
||||||
FILES = openFiles(new Index(SETUP.d ? SETUP.d[0] : 'kiri')),
|
|
||||||
clone = Object.clone,
|
|
||||||
MODE = MODES.FDM,
|
|
||||||
viewMode = VIEWS.ARRANGE,
|
|
||||||
autoSaveTimer = null,
|
|
||||||
inits = parseInt(SDB.getItem('kiri-init') || stats.get('init') || 0) + 1;
|
|
||||||
|
|
||||||
// update version and init count
|
|
||||||
SDB.setItem('kiri-init', inits);
|
|
||||||
stats.set('init', inits);
|
|
||||||
stats.set('kiri', version);
|
|
||||||
|
|
||||||
// allow widget to straddle client / worker FOR NOW
|
|
||||||
self.kiri_catalog = FILES;
|
|
||||||
|
|
||||||
export const dialog = {
|
|
||||||
show: showModal,
|
|
||||||
hide: hideModal,
|
|
||||||
update_process_list: updateProcessList
|
|
||||||
};
|
|
||||||
|
|
||||||
export const help = {
|
|
||||||
show: showHelp,
|
|
||||||
file: showHelpFile
|
|
||||||
};
|
|
||||||
|
|
||||||
export const event = {
|
|
||||||
on(t,l) { return EVENT.on(t,l) },
|
|
||||||
emit(t,m,o) { return EVENT.publish(t,m,o) },
|
|
||||||
bind(t,m,o) { return EVENT.bind(t,m,o) },
|
|
||||||
alerts(clr) { api.alerts.update(clr) },
|
|
||||||
import: loadFile,
|
|
||||||
settings: triggerSettingsEvent
|
|
||||||
};
|
|
||||||
|
|
||||||
export const group = {
|
|
||||||
merge: groupMerge,
|
|
||||||
split: groupSplit,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hide = {
|
|
||||||
alert(rec, recs) { api.alerts.hide(...arguments) },
|
|
||||||
import: noop,
|
|
||||||
slider: hideSlider
|
|
||||||
};
|
|
||||||
|
|
||||||
export const image = {
|
|
||||||
dialog: loadImageDialog,
|
|
||||||
convert: loadImageConvert
|
|
||||||
};
|
|
||||||
|
|
||||||
export const modal = {
|
|
||||||
show: showModal,
|
|
||||||
hide: hideModal,
|
|
||||||
visible: modalShowing
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mode = {
|
|
||||||
get_id() { return MODE },
|
|
||||||
get_lower: getModeLower,
|
|
||||||
get: getMode,
|
|
||||||
set: setMode,
|
|
||||||
switch: switchMode,
|
|
||||||
set_expert: noop,
|
|
||||||
is_fdm() { return MODE === MODES.FDM },
|
|
||||||
is_cam() { return MODE === MODES.CAM },
|
|
||||||
is_sla() { return MODE === MODES.SLA },
|
|
||||||
is_drag() { return MODE === MODES.DRAG },
|
|
||||||
is_wedm() { return MODE === MODES.WEDM },
|
|
||||||
is_wjet() { return MODE === MODES.WJET },
|
|
||||||
is_laser() { return MODE === MODES.LASER },
|
|
||||||
is_2d() { return false ||
|
|
||||||
api.mode.is_drag() ||
|
|
||||||
api.mode.is_wedm() ||
|
|
||||||
api.mode.is_wjet() ||
|
|
||||||
api.mode.is_laser()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const process = {
|
|
||||||
code: currentProcessCode,
|
|
||||||
get: currentProcessName
|
|
||||||
};
|
|
||||||
|
|
||||||
export const show = {
|
|
||||||
alert() { return api.alerts.show(...arguments) },
|
|
||||||
controls: setControlsVisible,
|
|
||||||
devices: showDevices,
|
|
||||||
import() { api.ui.import.style.display = '' },
|
|
||||||
layer: setVisibleLayer,
|
|
||||||
local: showLocal,
|
|
||||||
progress: setProgress,
|
|
||||||
slices: showSlices,
|
|
||||||
tools: showTools
|
|
||||||
};
|
|
||||||
|
|
||||||
export const space = {
|
|
||||||
reload,
|
|
||||||
auto_save,
|
|
||||||
restore: restoreWorkspace,
|
|
||||||
clear: clearWorkspace,
|
|
||||||
save: saveWorkspace,
|
|
||||||
set_focus: setFocus,
|
|
||||||
update: SPACE.update,
|
|
||||||
is_dark() { return settings.ctrl().dark }
|
|
||||||
};
|
|
||||||
|
|
||||||
export const util = {
|
|
||||||
isSecure,
|
|
||||||
download: downloadBlob,
|
|
||||||
ui2rec() { api.conf.update_from(...arguments) },
|
|
||||||
rec2ui() { api.conf.update_fields(...arguments) },
|
|
||||||
b64enc(obj) { return base64js.fromByteArray(new TextEncoder().encode(JSON.stringify(obj))) },
|
|
||||||
b64dec(obj) { return JSON.parse(new TextDecoder().decode(base64js.toByteArray(obj))) }
|
|
||||||
};
|
|
||||||
|
|
||||||
export const view = {
|
|
||||||
get() { return viewMode },
|
|
||||||
set() { setViewMode(...arguments) },
|
|
||||||
set_arrange() { api.view.set(VIEWS.ARRANGE) },
|
|
||||||
set_slice() { api.view.set(VIEWS.SLICE) },
|
|
||||||
set_preview() { api.view.set(VIEWS.PREVIEW) },
|
|
||||||
set_animate() { api.view.set(VIEWS.ANIMATE) },
|
|
||||||
is_arrange() { return viewMode === VIEWS.ARRANGE },
|
|
||||||
is_slice() { return viewMode === VIEWS.SLICE },
|
|
||||||
is_preview() { return viewMode === VIEWS.PREVIEW },
|
|
||||||
is_animate() { return viewMode === VIEWS.ANIMATE },
|
|
||||||
update_stack_labels: updateStackLabelState,
|
|
||||||
update_slider_max: updateSliderMax,
|
|
||||||
update_slider: updateSlider,
|
|
||||||
update_speeds: updateSpeeds,
|
|
||||||
hide_slices: hideSlices,
|
|
||||||
snapshot: null,
|
|
||||||
edges: setEdges,
|
|
||||||
unit_scale: unitScale,
|
|
||||||
wireframe: setWireframe
|
|
||||||
};
|
|
||||||
|
|
||||||
// add show() to catalog for API
|
|
||||||
FILES.show = showCatalog;
|
|
||||||
|
|
||||||
// patch broker for api backward compatibility
|
|
||||||
EVENT.on = (topic, listener) => {
|
|
||||||
EVENT.subscribe(topic, listener);
|
|
||||||
return EVENT;
|
|
||||||
};
|
|
||||||
|
|
||||||
function updateStackLabelState() {
|
|
||||||
const settings = api.conf.get();
|
|
||||||
// match label checkboxes to preference
|
|
||||||
for (let label of api.stacks.getLabels()) {
|
|
||||||
let check = `${settings.mode}-${api.view.get()}-${label}`;
|
|
||||||
api.stacks.setVisible(label, settings.labels[check] !== false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setFocus(sel, point) {
|
|
||||||
if (point) {
|
|
||||||
SPACE.platform.setCenter(point.x, point.z, point.y);
|
|
||||||
SPACE.view.setFocus(new THREE.Vector3(point.x, point.y, point.z));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (sel === undefined) {
|
|
||||||
sel = api.widgets.all();
|
|
||||||
} else if (!Array.isArray) {
|
|
||||||
sel = [ sel ];
|
|
||||||
} else if (sel.length === 0) {
|
|
||||||
sel = api.widgets.all();
|
|
||||||
}
|
|
||||||
let pos = { x:0, y:0, z:0 };
|
|
||||||
for (let widget of sel) {
|
|
||||||
pos.x += widget.track.pos.x;
|
|
||||||
pos.y += widget.track.pos.y;
|
|
||||||
pos.z += widget.track.pos.z;
|
|
||||||
}
|
|
||||||
if (sel.length) {
|
|
||||||
pos.x /= sel.length;
|
|
||||||
pos.y /= sel.length;
|
|
||||||
pos.z /= sel.length;
|
|
||||||
}
|
|
||||||
let cam_index = api.conf.get().process.camStockIndexed || false;
|
|
||||||
let focus_z = cam_index ? 0 : platform.top_z() / 2;
|
|
||||||
SPACE.platform.setCenter(pos.x, -pos.y, focus_z);
|
|
||||||
SPACE.view.setFocus(new THREE.Vector3(pos.x, focus_z, -pos.y));
|
|
||||||
}
|
|
||||||
|
|
||||||
function reload() {
|
|
||||||
api.event.emit('reload');
|
|
||||||
do_reload(100);
|
|
||||||
}
|
|
||||||
|
|
||||||
function do_reload(time) {
|
|
||||||
// allow time for async saves to complete and busy to to to zero
|
|
||||||
setTimeout(() => {
|
|
||||||
if (api.busy.val() === 0) {
|
|
||||||
LOC.reload();
|
|
||||||
} else {
|
|
||||||
console.log(`reload deferred on busy=${api.busy.val()}`);
|
|
||||||
do_reload(250);
|
|
||||||
}
|
|
||||||
}, time || 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
function auto_save() {
|
|
||||||
if (!settings.ctrl().autoSave) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
clearTimeout(autoSaveTimer);
|
|
||||||
autoSaveTimer = setTimeout(() => {
|
|
||||||
api.space.save(true);
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** ******************************************************************
|
|
||||||
* Utility Functions
|
|
||||||
******************************************************************* */
|
|
||||||
|
|
||||||
function unitScale() {
|
|
||||||
return api.mode.is_cam() && settings.ctrl().units === 'in' ? 25.4 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function triggerSettingsEvent() {
|
|
||||||
api.event.emit('settings', settings.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSecure(proto) {
|
|
||||||
return proto.toLowerCase().indexOf("https") === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setProgress(value = 0, msg) {
|
|
||||||
value = (value * 100).round(4);
|
|
||||||
api.ui.progress.width = value+'%';
|
|
||||||
if (self.debug) {
|
|
||||||
// console.log(msg, value.round(2));
|
|
||||||
api.ui.prostatus.style.display = 'flex';
|
|
||||||
if (msg) {
|
|
||||||
api.ui.prostatus.innerHTML = msg;
|
|
||||||
} else {
|
|
||||||
api.ui.prostatus.innerHTML = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function bound(v,min,max) {
|
|
||||||
return Math.max(min,Math.min(max,v));
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSpeeds(maxSpeed, minSpeed) {
|
|
||||||
const { ui } = api;
|
|
||||||
ui.speeds.style.display =
|
|
||||||
maxSpeed &&
|
|
||||||
settings.mode !== 'SLA' &&
|
|
||||||
settings.mode !== 'LASER' &&
|
|
||||||
viewMode === VIEWS.PREVIEW &&
|
|
||||||
ui.showSpeeds.checked ? 'block' : '';
|
|
||||||
if (maxSpeed) {
|
|
||||||
const colors = [];
|
|
||||||
for (let i = 0; i <= maxSpeed; i += maxSpeed / 20) {
|
|
||||||
colors.push(Math.round(Math.max(i, 1)));
|
|
||||||
}
|
|
||||||
api.client.colors(colors, maxSpeed, speedColors => {
|
|
||||||
const list = [];
|
|
||||||
Object.keys(speedColors).map(v => parseInt(v)).sort((a, b) => b - a).forEach(speed => {
|
|
||||||
const color = speedColors[speed];
|
|
||||||
const hex = color.toString(16).padStart(6, 0);
|
|
||||||
const r = (color >> 16) & 0xff;
|
|
||||||
const g = (color >> 8) & 0xff;
|
|
||||||
const b = (color >> 0) & 0xff;
|
|
||||||
const style = `background-color:#${hex}`;
|
|
||||||
list.push(`<label style="${style}">${speed}</label>`);
|
|
||||||
});
|
|
||||||
ui.speedbar.innerHTML = list.join('');
|
|
||||||
});
|
|
||||||
api.event.emit('preview.speeds', {
|
|
||||||
min: minSpeed,
|
|
||||||
max: maxSpeed
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSlider() {
|
|
||||||
api.event.emit("slider.set", {
|
|
||||||
start: (api.var.layer_lo / api.var.layer_max),
|
|
||||||
end: (api.var.layer_hi / api.var.layer_max)
|
|
||||||
});
|
|
||||||
api.conf.update_fields_from_range();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setVisibleLayer(h, l) {
|
|
||||||
h = h >= 0 ? h : api.var.layer_hi;
|
|
||||||
l = l >= 0 ? l : api.var.layer_lo;
|
|
||||||
api.var.layer_hi = bound(h, 0, api.var.layer_max);
|
|
||||||
api.var.layer_lo = bound(l, 0, h);
|
|
||||||
api.event.emit("slider.label");
|
|
||||||
updateSlider();
|
|
||||||
showSlices();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setWireframe(bool, color, opacity) {
|
|
||||||
api.widgets.each(function(w) { w.setWireframe(bool, color, opacity) });
|
|
||||||
SPACE.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setEdges(bool) {
|
|
||||||
if (bool && bool.toggle) {
|
|
||||||
api.local.toggle('model.edges');
|
|
||||||
} else {
|
|
||||||
api.local.set('model.edges', bool);
|
|
||||||
}
|
|
||||||
bool = api.local.getBoolean('model.edges');
|
|
||||||
api.widgets.each(w => w.setEdges(bool));
|
|
||||||
SPACE.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSliderMax(set) {
|
|
||||||
let max = STACKS.getRange().tallest - 1;
|
|
||||||
api.var.layer_max = api.ui.sliderMax.innerText = max;
|
|
||||||
if (set || max < api.var.layer_hi) {
|
|
||||||
api.var.layer_hi = api.var.layer_max;
|
|
||||||
api.event.emit("slider.label");
|
|
||||||
updateSlider();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideSlices() {
|
|
||||||
STACKS.clear();
|
|
||||||
api.widgets.setOpacity(COLOR.model_opacity);
|
|
||||||
api.widgets.each(function(widget) {
|
|
||||||
widget.setWireframe(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function setWidgetVisibility(bool) {
|
|
||||||
api.widgets.each(w => {
|
|
||||||
if (bool) {
|
|
||||||
w.show();
|
|
||||||
} else {
|
|
||||||
w.hide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* hide or show slice-layers and their sub-elements
|
|
||||||
*
|
|
||||||
* @param {number} [layer]
|
|
||||||
*/
|
|
||||||
function showSlices(layer) {
|
|
||||||
if (viewMode === VIEWS.ARRANGE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showSlider();
|
|
||||||
|
|
||||||
if (typeof(layer) === 'string' || typeof(layer) === 'number') {
|
|
||||||
layer = parseInt(layer);
|
|
||||||
} else {
|
|
||||||
layer = api.var.layer_hi;
|
|
||||||
}
|
|
||||||
|
|
||||||
layer = bound(layer, 0, api.var.layer_max);
|
|
||||||
if (layer < api.var.layer_lo) api.var.layer_lo = layer;
|
|
||||||
api.var.layer_hi = layer;
|
|
||||||
api.event.emit("slider.label");
|
|
||||||
|
|
||||||
updateSlider();
|
|
||||||
STACKS.setRange(api.var.layer_lo, api.var.layer_hi);
|
|
||||||
|
|
||||||
SPACE.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
function showSlider() {
|
|
||||||
api.ui.layers.style.display = 'flex';
|
|
||||||
api.ui.slider.style.display = 'flex';
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideSlider() {
|
|
||||||
api.ui.layers.style.display = 'none';
|
|
||||||
api.ui.slider.style.display = 'none';
|
|
||||||
api.ui.speeds.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadImageDialog(image, name, force) {
|
|
||||||
if (!force && image.byteLength > 2500000) {
|
|
||||||
return api.uc.confirm("Large images may fail to import<br>Consider resizing under 1000 x 1000<br>Proceed with import?").then(ok => {
|
|
||||||
if (ok) {
|
|
||||||
loadImageDialog(image, name, true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const opt = {pre: [
|
|
||||||
"<div class='f-col a-center'>",
|
|
||||||
" <h3>Image Conversion</h3>",
|
|
||||||
" <p class='t-just' style='width:300px;line-height:1.5em'>",
|
|
||||||
" This will create a 3D model from a 2D PNG image. Photos must",
|
|
||||||
" be blurred to be usable. Values from 0=off to 50=high are suggested.",
|
|
||||||
" Higher values incur more processing time.",
|
|
||||||
" </p>",
|
|
||||||
" <div class='f-row t-right'><table>",
|
|
||||||
" <tr><th>blur value</th><td><input id='png-blur' value='0' size='3'></td>",
|
|
||||||
" <th> invert image</th><td><input id='png-inv' type='checkbox'></td></tr>",
|
|
||||||
" <tr><th>base size</th><td><input id='png-base' value='0' size='3'></td>",
|
|
||||||
" <th> invert alpha</th><td><input id='alpha-inv' type='checkbox'></td></tr>",
|
|
||||||
" <tr><th>border size</th><td><input id='png-border' value='0' size='3'></td>",
|
|
||||||
" <th></th><td></td></tr>",
|
|
||||||
" </table></div>",
|
|
||||||
"</div>"
|
|
||||||
]};
|
|
||||||
api.uc.confirm(undefined, {convert:true, cancel:false}, undefined, opt).then((ok) => {
|
|
||||||
if (ok) {
|
|
||||||
loadImage(image, {
|
|
||||||
file: name,
|
|
||||||
blur: parseInt($('png-blur').value) || 0,
|
|
||||||
base: parseInt($('png-base').value) || 0,
|
|
||||||
border: parseInt($('png-border').value) || 0,
|
|
||||||
inv_image: $('png-inv').checked,
|
|
||||||
inv_alpha: $('alpha-inv').checked
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadImage(image, opt = {}) {
|
|
||||||
const info = Object.assign({settings: settings.get(), png:image}, opt);
|
|
||||||
api.client.image2mesh(info, progress => {
|
|
||||||
api.show.progress(progress, "converting");
|
|
||||||
}, vertices => {
|
|
||||||
api.show.progress(0);
|
|
||||||
const widget = newWidget().loadVertices(vertices);
|
|
||||||
widget.meta.file = opt.file;
|
|
||||||
platform.add(widget);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** ******************************************************************
|
|
||||||
* Selection Functions
|
|
||||||
******************************************************************* */
|
|
||||||
|
|
||||||
function groupMerge() {
|
|
||||||
Widget.Groups.merge(selection.widgets(true));
|
|
||||||
}
|
|
||||||
|
|
||||||
function groupSplit() {
|
|
||||||
Widget.Groups.split(selection.widgets(false));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** ******************************************************************
|
|
||||||
* Settings Functions
|
|
||||||
******************************************************************* */
|
|
||||||
|
|
||||||
// convert any image type to png
|
|
||||||
function loadImageConvert(res, name) {
|
|
||||||
let url = URL.createObjectURL(new Blob([res]));
|
|
||||||
|
|
||||||
$('mod-any').innerHTML = `<img id="xsrc" src="${url}"><canvas id="xdst"></canvas>`;
|
|
||||||
|
|
||||||
let img = $('xsrc');
|
|
||||||
let can = $('xdst');
|
|
||||||
|
|
||||||
img.onload = () => {
|
|
||||||
can.width = img.width;
|
|
||||||
can.height = img.height;
|
|
||||||
let ctx = can.getContext('2d');
|
|
||||||
ctx.drawImage(img, 0, 0);
|
|
||||||
fetch(can.toDataURL()).then(r => r.arrayBuffer()).then(data => {
|
|
||||||
loadImageDialog(data, name);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadFile(ev) {
|
|
||||||
// use modern Filesystem api when available
|
|
||||||
if (false && window.showOpenFilePicker) {
|
|
||||||
window.showOpenFilePicker().then(files => {
|
|
||||||
return Promise.all(files.map(fh => fh.getFile()))
|
|
||||||
}).then(files => {
|
|
||||||
if (files.length) {
|
|
||||||
api.platform.load_files(files);
|
|
||||||
}
|
|
||||||
}).catch(e => { /* ignore cancel */ });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
api.ui.load.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveWorkspace(quiet) {
|
|
||||||
api.conf.save();
|
|
||||||
const newWidgets = [];
|
|
||||||
const oldWidgets = js2o(SDB.getItem('ws-widgets'), []);
|
|
||||||
api.widgets.each(function(widget) {
|
|
||||||
if (widget.synth) return;
|
|
||||||
newWidgets.push(widget.id);
|
|
||||||
oldWidgets.remove(widget.id);
|
|
||||||
widget.saveState();
|
|
||||||
let ann = api.widgets.annotate(widget.id);
|
|
||||||
ann.file = widget.meta.file;
|
|
||||||
ann.url = widget.meta.url;
|
|
||||||
});
|
|
||||||
SDB.setItem('ws-widgets', o2js(newWidgets));
|
|
||||||
oldWidgets.forEach(wid => {
|
|
||||||
Widget.deleteFromState(wid);
|
|
||||||
});
|
|
||||||
// eliminate dangling saved widgets
|
|
||||||
FILES.deleteFilter(key => newWidgets.indexOf(key.substring(8)) < 0, "ws-save-", "ws-savf");
|
|
||||||
if (!quiet) {
|
|
||||||
api.show.alert("workspace saved", 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function restoreWorkspace(ondone, skip_widget_load) {
|
|
||||||
let newset = api.conf.restore(false),
|
|
||||||
camera = newset.controller.view,
|
|
||||||
toload = ls2o('ws-widgets',[]),
|
|
||||||
loaded = 0,
|
|
||||||
position = true;
|
|
||||||
|
|
||||||
api.conf.update_fields();
|
|
||||||
platform.update_size();
|
|
||||||
|
|
||||||
SPACE.view.reset();
|
|
||||||
if (camera) {
|
|
||||||
SPACE.view.load(camera);
|
|
||||||
} else {
|
|
||||||
SPACE.view.home();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (skip_widget_load) {
|
|
||||||
if (ondone) {
|
|
||||||
ondone();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove any widgets from platform
|
|
||||||
api.widgets.each(function(widget) {
|
|
||||||
platform.delete(widget);
|
|
||||||
});
|
|
||||||
|
|
||||||
// load any widget by name that was saved to the workspace
|
|
||||||
toload.forEach(function(widgetid) {
|
|
||||||
Widget.loadFromState(widgetid, function(widget) {
|
|
||||||
if (widget) {
|
|
||||||
platform.add(widget, 0, position, true);
|
|
||||||
let ann = api.widgets.annotate(widgetid);
|
|
||||||
widget.meta.file = ann.file;
|
|
||||||
widget.meta.url = ann.url;
|
|
||||||
}
|
|
||||||
if (++loaded === toload.length) {
|
|
||||||
platform.deselect();
|
|
||||||
if (ondone) {
|
|
||||||
ondone();
|
|
||||||
setTimeout(() => {
|
|
||||||
platform.update_bounds();
|
|
||||||
SPACE.update();
|
|
||||||
}, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, position);
|
|
||||||
});
|
|
||||||
|
|
||||||
return toload.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearWorkspace() {
|
|
||||||
// free up worker cache/mem
|
|
||||||
api.client.clear();
|
|
||||||
platform.select_all();
|
|
||||||
platform.delete(selection.meshes());
|
|
||||||
}
|
|
||||||
|
|
||||||
function modalShowing() {
|
|
||||||
return api.ui.modal.style.display === 'flex';
|
|
||||||
}
|
|
||||||
|
|
||||||
function showModal(which) {
|
|
||||||
let mod = api.ui.modal,
|
|
||||||
style = mod.style,
|
|
||||||
visible = modalShowing(),
|
|
||||||
info = { pct: 0 };
|
|
||||||
|
|
||||||
// hide all modals befroe showing another
|
|
||||||
Object.keys(api.ui.modals).forEach(name => {
|
|
||||||
api.ui.modals[name].style.display = name === which ? 'flex' : '';
|
|
||||||
});
|
|
||||||
|
|
||||||
function ondone() {
|
|
||||||
api.event.emit('modal.show', which);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (visible) {
|
|
||||||
return ondone();
|
|
||||||
}
|
|
||||||
|
|
||||||
style.height = '0';
|
|
||||||
style.display = 'flex';
|
|
||||||
|
|
||||||
new TWEEN.Tween(info).
|
|
||||||
easing(TWEEN.Easing.Quadratic.InOut).
|
|
||||||
to({ pct: 100 }, 100).
|
|
||||||
onUpdate(() => { style.height = `${info.pct}%` }).
|
|
||||||
onComplete(ondone).
|
|
||||||
start();
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideModal() {
|
|
||||||
if (!modalShowing()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let mod = api.ui.modal, style = mod.style, info={pct:100};
|
|
||||||
new TWEEN.Tween(info).
|
|
||||||
easing(TWEEN.Easing.Quadratic.InOut).
|
|
||||||
to({pct:0}, 100).
|
|
||||||
onUpdate(() => { style.height = `${info.pct}%` }).
|
|
||||||
onComplete(() => {
|
|
||||||
style.display = '';
|
|
||||||
api.event.emit('modal.hide');
|
|
||||||
}).
|
|
||||||
start();
|
|
||||||
}
|
|
||||||
|
|
||||||
function showCatalog() {
|
|
||||||
showModal("files");
|
|
||||||
}
|
|
||||||
|
|
||||||
function editSettings(e) {
|
|
||||||
let current = settings.get(),
|
|
||||||
mode = getMode(),
|
|
||||||
name = e.target.getAttribute("name"),
|
|
||||||
load = current.sproc[mode][name],
|
|
||||||
loadstr = JSON.stringify(load,null,4).split('\n');
|
|
||||||
api.uc.prompt(`settings for "${name}"`, loadstr).then(edit => {
|
|
||||||
if (edit) {
|
|
||||||
try {
|
|
||||||
current.sproc[mode][name] = JSON.parse(edit);
|
|
||||||
if (name === settings.proc().processName) {
|
|
||||||
api.conf.load(null, name);
|
|
||||||
}
|
|
||||||
api.conf.save();
|
|
||||||
api.settings.sync.put();
|
|
||||||
} catch (e) {
|
|
||||||
console.log({ malformed_settings: e });
|
|
||||||
api.uc.alert('malformed settings object');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function exportSettings(e) {
|
|
||||||
let current = settings.get(),
|
|
||||||
mode = getMode(),
|
|
||||||
name = e.target.getAttribute("name"),
|
|
||||||
data = api.util.b64enc({
|
|
||||||
process: current.sproc[mode][name],
|
|
||||||
version,
|
|
||||||
moto: moto.id,
|
|
||||||
time: Date.now(),
|
|
||||||
mode,
|
|
||||||
name
|
|
||||||
});
|
|
||||||
api.uc.prompt("Export Process Filename", name).then(name => {
|
|
||||||
if (name) {
|
|
||||||
api.util.download(data, `${name}.km`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteSettings(e) {
|
|
||||||
let current = settings.get();
|
|
||||||
let name = e.target.getAttribute("del");
|
|
||||||
delete current.sproc[getMode()][name];
|
|
||||||
api.settings.sync.put();
|
|
||||||
updateProcessList();
|
|
||||||
api.conf.save();
|
|
||||||
triggerSettingsEvent();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateProcessList() {
|
|
||||||
let current = settings.get();
|
|
||||||
let list = [], s = current, sp = s.sproc[getMode()] || {}, table = api.ui.settingsList;
|
|
||||||
table.innerHTML = '';
|
|
||||||
for (let k in sp) {
|
|
||||||
if (sp.hasOwnProperty(k)) list.push(k);
|
|
||||||
}
|
|
||||||
list.filter(n => n !=='default').sort().forEach(function(sk) {
|
|
||||||
let row = DOC.createElement('div'),
|
|
||||||
load = DOC.createElement('button'),
|
|
||||||
edit = DOC.createElement('button'),
|
|
||||||
xprt = DOC.createElement('button'),
|
|
||||||
del = DOC.createElement('button'),
|
|
||||||
name = sk;
|
|
||||||
|
|
||||||
load.setAttribute('load', sk);
|
|
||||||
load.onclick = (ev) => {
|
|
||||||
api.conf.load(undefined, sk);
|
|
||||||
updateProcessList();
|
|
||||||
hideModal();
|
|
||||||
}
|
|
||||||
load.appendChild(DOC.createTextNode(sk));
|
|
||||||
if (sk == settings.proc().processName) {
|
|
||||||
load.setAttribute('class', 'selected')
|
|
||||||
}
|
|
||||||
api.ui.settingsName.value = settings.proc().processName;
|
|
||||||
|
|
||||||
del.setAttribute('del', sk);
|
|
||||||
del.setAttribute('title', "remove '"+sk+"'");
|
|
||||||
del.innerHTML = '<i class="far fa-trash-alt"></i>';
|
|
||||||
del.onclick = deleteSettings;
|
|
||||||
|
|
||||||
edit.setAttribute('name', sk);
|
|
||||||
edit.setAttribute('title', 'edit');
|
|
||||||
edit.innerHTML = '<i class="far fa-edit"></i>';
|
|
||||||
edit.onclick = editSettings;
|
|
||||||
|
|
||||||
xprt.setAttribute('name', sk);
|
|
||||||
xprt.setAttribute('title', 'export');
|
|
||||||
xprt.innerHTML = '<i class="fas fa-download"></i>';
|
|
||||||
xprt.onclick = exportSettings;
|
|
||||||
|
|
||||||
row.setAttribute("class", "flow-row");
|
|
||||||
row.appendChild(edit);
|
|
||||||
row.appendChild(load);
|
|
||||||
row.appendChild(xprt);
|
|
||||||
row.appendChild(del);
|
|
||||||
table.appendChild(row);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function showHelp() {
|
|
||||||
showHelpFile(`local`,() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
function showHelpFile(local,then) {
|
|
||||||
if (!local) {
|
|
||||||
WIN.open("//docs.grid.space/", "_help");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const LANG = api.language.current;
|
|
||||||
$('kiri-version').innerHTML = `${LANG.version} ${version}`;
|
|
||||||
showModal('help');
|
|
||||||
api.event.emit('help.show', local);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showLocal() {
|
|
||||||
// deprecated
|
|
||||||
}
|
|
||||||
|
|
||||||
function setViewMode(mode) {
|
|
||||||
const isCAM = settings.mode() === 'CAM';
|
|
||||||
viewMode = mode;
|
|
||||||
platform.deselect();
|
|
||||||
selection.update_info();
|
|
||||||
// clear any bounds forced by, for example, WireEDM previews
|
|
||||||
api.platform.set_bounds();
|
|
||||||
// disable clear in non-arrange modes
|
|
||||||
['view-arrange','act-slice','act-preview','act-animate'].forEach(el => {
|
|
||||||
$(el).classList.remove('selected')
|
|
||||||
});
|
|
||||||
$('render-tools').classList.add('hide');
|
|
||||||
switch (mode) {
|
|
||||||
case VIEWS.ARRANGE:
|
|
||||||
$('view-arrange').classList.add('selected');
|
|
||||||
$('render-tools').classList.remove('hide');
|
|
||||||
api.function.clear_progress();
|
|
||||||
api.client.clear();
|
|
||||||
STACKS.clear();
|
|
||||||
hideSlider();
|
|
||||||
updateSpeeds();
|
|
||||||
setVisibleLayer();
|
|
||||||
setWidgetVisibility(true);
|
|
||||||
api.widgets.setOpacity(1);
|
|
||||||
api.view.edges(api.local.getBoolean('model.edges'));
|
|
||||||
break;
|
|
||||||
case VIEWS.SLICE:
|
|
||||||
$('act-slice').classList.add('selected');
|
|
||||||
updateSpeeds();
|
|
||||||
updateSliderMax();
|
|
||||||
setWidgetVisibility(true);
|
|
||||||
!isCAM && api.view.edges(false);
|
|
||||||
break;
|
|
||||||
case VIEWS.PREVIEW:
|
|
||||||
$('act-preview').classList.add('selected');
|
|
||||||
setWidgetVisibility(true);
|
|
||||||
!isCAM && api.view.edges(false);
|
|
||||||
break;
|
|
||||||
case VIEWS.ANIMATE:
|
|
||||||
$('act-animate').classList.add('selected');
|
|
||||||
!isCAM && api.view.edges(false);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.log("invalid view mode: "+mode);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
api.event.emit('view.set', mode);
|
|
||||||
DOC.activeElement.blur();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMode() {
|
|
||||||
return settings.mode();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getModeLower() {
|
|
||||||
return getMode().toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
function switchMode(mode) {
|
|
||||||
setMode(mode, null, platform.update_size);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setMode(mode, lock, then) {
|
|
||||||
if (!MODES[mode]) {
|
|
||||||
console.log("invalid mode: "+mode);
|
|
||||||
mode = 'FDM';
|
|
||||||
}
|
|
||||||
const current = settings.get();
|
|
||||||
// change mode constants
|
|
||||||
current.mode = mode;
|
|
||||||
MODE = MODES[mode];
|
|
||||||
// gcode edit area for any non-SLA mode
|
|
||||||
api.uc.setVisible($('gcode-edit'), mode !== 'SLA');
|
|
||||||
// highlight selected mode menu item
|
|
||||||
["FDM","CAM","SLA","LASER","DRAG","WJET","WEDM"].forEach(sm => {
|
|
||||||
const cl = $(`mode-${sm.toLowerCase()}`).classList;
|
|
||||||
if (sm === mode) {
|
|
||||||
cl.add('selected');
|
|
||||||
} else {
|
|
||||||
cl.remove('selected');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// restore cached device profile for this mode
|
|
||||||
if (current.cdev[mode]) {
|
|
||||||
current.device = clone(current.cdev[mode]);
|
|
||||||
api.event.emit('device.select', api.device.get());
|
|
||||||
}
|
|
||||||
// hide/show
|
|
||||||
api.uc.setVisible($('set-tools'), mode === 'CAM');
|
|
||||||
// updates right-hand menu by enabling/disabling fields
|
|
||||||
setViewMode(VIEWS.ARRANGE);
|
|
||||||
api.uc.setMode(MODE);
|
|
||||||
// sanitize and persist settings
|
|
||||||
api.conf.load();
|
|
||||||
api.conf.save();
|
|
||||||
// other housekeeping
|
|
||||||
triggerSettingsEvent();
|
|
||||||
platform.update_selected();
|
|
||||||
selection.update_bounds(api.widgets.all());
|
|
||||||
api.conf.update_fields();
|
|
||||||
// because device dialog, if showing, needs to be updated
|
|
||||||
if (modalShowing()) {
|
|
||||||
api.show.devices();
|
|
||||||
}
|
|
||||||
api.space.restore(null, true);
|
|
||||||
api.event.emit("mode.set", mode);
|
|
||||||
if (then) {
|
|
||||||
then();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function currentProcessName() {
|
|
||||||
return settings.get().cproc[getMode()];
|
|
||||||
}
|
|
||||||
|
|
||||||
function currentProcessCode() {
|
|
||||||
return settings.get().sproc[getMode()][currentProcessName()];
|
|
||||||
}
|
|
||||||
|
|
||||||
function setControlsVisible(show) {
|
|
||||||
// TODO fix
|
|
||||||
// $('mid-left').style.display = show ? 'flex' : 'none';
|
|
||||||
// $('mid-right').style.display = show ? 'flex' : 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadBlob(data, filename) {
|
|
||||||
let url = WIN.URL.createObjectURL(new Blob([data], {type: "octet/stream"}));
|
|
||||||
$('mod-any').innerHTML = `<a id="_dexport_" href="${url}" download="${filename}">x</a>`;
|
|
||||||
$('_dexport_').click();
|
|
||||||
}
|
|
||||||
|
|
||||||
// prevent safari from exiting full screen mode
|
|
||||||
DOC.onkeydown = function (evt) { if (evt.keyCode == 27) evt.preventDefault() }
|
|
||||||
|
|
||||||
export { FILES as catalog, LOCAL, SETUP, SECURE };
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
import { arcToPath } from '../../geo/paths.js';
|
import { arcToPath } from '../../geo/paths.js';
|
||||||
import { consts } from './consts.js';
|
import { createVM } from '../../moto/quickjs.js';
|
||||||
import { newPoint } from '../../geo/point.js';
|
import { newPoint } from '../../geo/point.js';
|
||||||
|
import { newPolygon } from '../../geo/polygon.js';
|
||||||
import { util } from '../../geo/base.js';
|
import { util } from '../../geo/base.js';
|
||||||
|
|
||||||
const { numOrDefault } = util;
|
const { numOrDefault } = util;
|
||||||
const { beltfact } = consts;
|
|
||||||
|
|
||||||
const XAXIS = new THREE.Vector3(1,0,0);
|
const BELTFACT = Math.cos(Math.PI / 4);
|
||||||
const DEG2RAD = Math.PI / 180;
|
const DEG2RAD = Math.PI / 180;
|
||||||
|
|
||||||
class Print {
|
class Print {
|
||||||
|
|
@ -24,6 +24,7 @@ class Print {
|
||||||
this.tools = {};
|
this.tools = {};
|
||||||
// set to 1 to enable flow rate analysis (console)
|
// set to 1 to enable flow rate analysis (console)
|
||||||
this.debugE = settings ? (settings.controller.devel ? 1 : 0) : 0;
|
this.debugE = settings ? (settings.controller.devel ? 1 : 0) : 0;
|
||||||
|
this._ready = this.createSafeEval();
|
||||||
}
|
}
|
||||||
|
|
||||||
setType(type) {
|
setType(type) {
|
||||||
|
|
@ -40,7 +41,7 @@ class Print {
|
||||||
* addOutput - add a new point to the output gcode array
|
* addOutput - add a new point to the output gcode array
|
||||||
* @param {any[]} array - the output gcode array
|
* @param {any[]} array - the output gcode array
|
||||||
* @param {Point} point - the new point
|
* @param {Point} point - the new point
|
||||||
* @param {number} emit - the extrusion value
|
* @param {number} emit - the extrusion value (G? value for cnc)
|
||||||
* @param {number} speed - the feed rate
|
* @param {number} speed - the feed rate
|
||||||
* @param {string} tool - the tool id
|
* @param {string} tool - the tool id
|
||||||
* @param {"lerp"|string} opts.type - the output type
|
* @param {"lerp"|string} opts.type - the output type
|
||||||
|
|
@ -50,7 +51,7 @@ class Print {
|
||||||
* @return {Output} - the new output object
|
* @return {Output} - the new output object
|
||||||
*/
|
*/
|
||||||
addOutput(array, point, emit, speed, tool, opts) {
|
addOutput(array, point, emit, speed, tool, opts) {
|
||||||
const { type, retract, center, arcPoints} = opts ?? {};
|
const { type, retract, center, arcPoints, clockwise, distance } = opts ?? {};
|
||||||
let { lastPoint, lastEmit, lastOut } = this;
|
let { lastPoint, lastEmit, lastOut } = this;
|
||||||
let arc = emit == 2 || emit == 3;
|
let arc = emit == 2 || emit == 3;
|
||||||
// drop duplicates (usually intruced by FDM bisections)
|
// drop duplicates (usually intruced by FDM bisections)
|
||||||
|
|
@ -67,7 +68,9 @@ class Print {
|
||||||
this.lastOut = lastOut = new Output(point, emit, speed, tool, {
|
this.lastOut = lastOut = new Output(point, emit, speed, tool, {
|
||||||
type: type ?? this.nextType,
|
type: type ?? this.nextType,
|
||||||
center,
|
center,
|
||||||
|
clockwise,
|
||||||
arcPoints,
|
arcPoints,
|
||||||
|
distance
|
||||||
});
|
});
|
||||||
if (tool !== undefined) {
|
if (tool !== undefined) {
|
||||||
this.tools[tool] = true;
|
this.tools[tool] = true;
|
||||||
|
|
@ -128,10 +131,9 @@ class Print {
|
||||||
perimeter = poly.perimeter(),
|
perimeter = poly.perimeter(),
|
||||||
close = !options.open,
|
close = !options.open,
|
||||||
scarf = !poly.open ? (options.scarf ?? 0) : false,
|
scarf = !poly.open ? (options.scarf ?? 0) : false,
|
||||||
tool = options.tool,
|
|
||||||
zmax = options.zmax,
|
|
||||||
last = startPoint,
|
last = startPoint,
|
||||||
first = true;
|
first = true,
|
||||||
|
{ arcOpts, tool, zmax, onfirst, onfirstout } = options;
|
||||||
|
|
||||||
// if short, use calculated print speed based on sliding scale
|
// if short, use calculated print speed based on sliding scale
|
||||||
if (perimeter < process.outputShortPoly) {
|
if (perimeter < process.outputShortPoly) {
|
||||||
|
|
@ -167,7 +169,7 @@ class Print {
|
||||||
if (scarf) {
|
if (scarf) {
|
||||||
let epz = Math.max(...poly.points.map(p => p.z));
|
let epz = Math.max(...poly.points.map(p => p.z));
|
||||||
let spz = startPoint.z;
|
let spz = startPoint.z;
|
||||||
poly = poly.segment(options.nozzleSize ?? 0.4, false, false, scarf * 2);
|
poly = poly.segment(nozzleSize ?? 0.4, false, false, scarf * 2);
|
||||||
pp = poly.points;
|
pp = poly.points;
|
||||||
let lp, sp = [];
|
let lp, sp = [];
|
||||||
for (let p of pp) {
|
for (let p of pp) {
|
||||||
|
|
@ -191,6 +193,10 @@ class Print {
|
||||||
}
|
}
|
||||||
pp.push(...esp);
|
pp.push(...esp);
|
||||||
scarf = true;
|
scarf = true;
|
||||||
|
} else if (arcOpts) {
|
||||||
|
// annotate arc points
|
||||||
|
pp = newPolygon(pp).detectArcs(arcOpts).points;
|
||||||
|
// console.log(arcOpts, pp.map(p => p.arc?.skip).filter(v => v));
|
||||||
}
|
}
|
||||||
|
|
||||||
// scarf manages its own close point
|
// scarf manages its own close point
|
||||||
|
|
@ -199,19 +205,44 @@ class Print {
|
||||||
}
|
}
|
||||||
|
|
||||||
let lpo;
|
let lpo;
|
||||||
|
let arcInfo;
|
||||||
|
let arcDist = 0;
|
||||||
for (let point of pp) {
|
for (let point of pp) {
|
||||||
|
let { arc } = point;
|
||||||
|
if (arc) {
|
||||||
|
arcInfo = arc;
|
||||||
|
if (first && onfirst) onfirst(point, output);
|
||||||
|
let out = scope.addOutput(output, point, shellMult, moveSpeed, tool);
|
||||||
|
if (first && onfirstout) onfirstout(out);
|
||||||
|
// arc center is relative to first point
|
||||||
|
arcInfo.center.move({ x: -point.x, y: -point.y, z: 0 });
|
||||||
|
last = point;
|
||||||
|
if (first) first = false;
|
||||||
|
continue;
|
||||||
|
} else if (arcInfo) {
|
||||||
|
// accumulate and pass to last point to calc FDM E value
|
||||||
|
arcDist += last.distTo2D(point);
|
||||||
|
if (--arcInfo.skip === 0) {
|
||||||
|
arcInfo.distance = arcDist;
|
||||||
|
scope.addOutput(output, point, shellMult, moveSpeed, tool, arcInfo);
|
||||||
|
arcInfo = undefined;
|
||||||
|
arcDist = 0;
|
||||||
|
} else {
|
||||||
|
// non-emit arc points get -1 for rendering
|
||||||
|
scope.addOutput(output, point, -1, moveSpeed, tool);
|
||||||
|
}
|
||||||
|
last = point;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (point.skip && lpo?.skip) {
|
if (point.skip && lpo?.skip) {
|
||||||
|
// basic thin wall skips are converted to moves
|
||||||
scope.addOutput(output, point, 0, moveSpeed, tool);
|
scope.addOutput(output, point, 0, moveSpeed, tool);
|
||||||
} else if (first) {
|
} else if (first) {
|
||||||
// if (point.skip) console.log({ skip: point });
|
// if (point.skip) console.log({ skip: point });
|
||||||
if (options.onfirst) {
|
onfirst && onfirst(point, output);
|
||||||
options.onfirst(point, output);
|
|
||||||
}
|
|
||||||
// move to first output point on poly
|
// move to first output point on poly
|
||||||
let out = scope.addOutput(output, point, 0, moveSpeed, tool);
|
let out = scope.addOutput(output, point, 0, moveSpeed, tool);
|
||||||
if (options.onfirstout) {
|
onfirstout && onfirstout(out);
|
||||||
options.onfirstout(out);
|
|
||||||
}
|
|
||||||
first = false;
|
first = false;
|
||||||
} else {
|
} else {
|
||||||
let seglen = last.distTo2D(point);
|
let seglen = last.distTo2D(point);
|
||||||
|
|
@ -237,7 +268,26 @@ class Print {
|
||||||
return output.last().point;
|
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) {
|
constReplace(str, consts, start, pad, short) {
|
||||||
|
let safeEval = this.safeEval;
|
||||||
|
if (safeEval) {
|
||||||
|
safeEval.setContext(consts);
|
||||||
|
}
|
||||||
function tryeval(str) {
|
function tryeval(str) {
|
||||||
try {
|
try {
|
||||||
return eval(`{ ${str} }`)
|
return eval(`{ ${str} }`)
|
||||||
|
|
@ -269,7 +319,7 @@ class Print {
|
||||||
}
|
}
|
||||||
eva.push(`function range(a,b) { return (a + (layer / layers) * (b-a)).round(4) }`);
|
eva.push(`function range(a,b) { return (a + (layer / layers) * (b-a)).round(4) }`);
|
||||||
eva.push(`try {( ${tok} )} catch (e) {console.log(e);0}`);
|
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;
|
nutok = evl;
|
||||||
if (pad === 666) {
|
if (pad === 666) {
|
||||||
return evl;
|
return evl;
|
||||||
|
|
@ -538,8 +588,8 @@ class Print {
|
||||||
let { point, prevPoint } = processLine(line, axes);
|
let { point, prevPoint } = processLine(line, axes);
|
||||||
|
|
||||||
if (morph && belt) {
|
if (morph && belt) {
|
||||||
point.y -= point.z * beltfact;
|
point.y -= point.z * BELTFACT;
|
||||||
point.z *= beltfact;
|
point.z *= BELTFACT;
|
||||||
}
|
}
|
||||||
|
|
||||||
const retract = (fdm && pos.E < 0) || undefined;
|
const retract = (fdm && pos.E < 0) || undefined;
|
||||||
|
|
@ -725,15 +775,16 @@ class Output {
|
||||||
*/
|
*/
|
||||||
constructor(point, emit, speed, tool, options) {
|
constructor(point, emit, speed, tool, options) {
|
||||||
|
|
||||||
const { type, center, arcPoints } = (options ?? {});
|
const { type, center, arcPoints, clockwise, distance } = (options ?? {});
|
||||||
//speed, tool, type, center, arcPoints
|
|
||||||
this.point = point;
|
this.point = point;
|
||||||
this.emit = Number(emit); //convert bools into 0/1
|
this.emit = Number(emit); //convert bools into 0/1
|
||||||
this.speed = speed;
|
this.speed = speed;
|
||||||
this.tool = tool;
|
this.tool = tool;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.center = center;
|
this.center = center;
|
||||||
this.arcPoints = arcPoints;
|
this.clockwise = clockwise;
|
||||||
|
this.distance = distance;
|
||||||
// this.where = new Error().stack.split("\n");
|
// this.where = new Error().stack.split("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
import { Layers } from './layers.js';
|
import { Layers } from '../app/layers.js';
|
||||||
import { newPoint } from '../../geo/point.js';
|
import { newPoint } from '../../geo/point.js';
|
||||||
import { newPolygon } from '../../geo/polygon.js';
|
import { newPolygon } from '../../geo/polygon.js';
|
||||||
import { newSlopeFromAngle } from '../../geo/slope.js';
|
import { newSlopeFromAngle } from '../../geo/slope.js';
|
||||||
|
|
|
||||||
|
|
@ -1,354 +0,0 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
|
||||||
|
|
||||||
import { api } from './api.js';
|
|
||||||
import { space } from '../../moto/space.js';
|
|
||||||
import { THREE } from '../../ext/three.js';
|
|
||||||
import { tool as MeshTool } from '../../mesh/tool.js';
|
|
||||||
|
|
||||||
const selectedMeshes = [];
|
|
||||||
|
|
||||||
function for_groups(fn) {
|
|
||||||
let groups = widgets(true).map(w => w.group).uniq();
|
|
||||||
for (let group of groups) {
|
|
||||||
fn(group[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function for_widgets(fn, noauto) {
|
|
||||||
let m = selectedMeshes;
|
|
||||||
let w = api.widgets.all();
|
|
||||||
if (m.length === 0 && w.length) {
|
|
||||||
m = noauto ? [] : w.map(w => w.mesh);
|
|
||||||
}
|
|
||||||
m.slice().forEach(mesh => { fn(mesh.widget) });
|
|
||||||
}
|
|
||||||
|
|
||||||
function for_status(fn) {
|
|
||||||
api.widgets.all().forEach(w => {
|
|
||||||
fn(w, selectedMeshes.contains(w.mesh));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function for_meshes(fn) {
|
|
||||||
selectedMeshes.slice().forEach(mesh => { fn(mesh) });
|
|
||||||
}
|
|
||||||
|
|
||||||
function move(x, y, z, abs) {
|
|
||||||
if (!api.view.is_arrange()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for_groups(w => w.move(x, y, z, abs));
|
|
||||||
update_bounds();
|
|
||||||
api.platform.update_bounds();
|
|
||||||
api.event.emit('selection.move', {x, y, z, abs});
|
|
||||||
space.update();
|
|
||||||
api.space.auto_save();
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_info() {
|
|
||||||
const ui = api.ui;
|
|
||||||
let bounds = new THREE.Box3(), track;
|
|
||||||
for_meshes(mesh => {
|
|
||||||
let box = mesh.getBoundingBox().clone();
|
|
||||||
track = mesh.widget.track;
|
|
||||||
let {pos} = track
|
|
||||||
box.min.add(pos);
|
|
||||||
box.max.add(pos);
|
|
||||||
bounds = bounds.union(box);
|
|
||||||
});
|
|
||||||
if (bounds.min.x === Infinity) {
|
|
||||||
if (selectedMeshes.length === 0) {
|
|
||||||
ui.sizeX.value = 0;
|
|
||||||
ui.sizeY.value = 0;
|
|
||||||
ui.sizeZ.value = 0;
|
|
||||||
ui.scaleX.value = 1;
|
|
||||||
ui.scaleY.value = 1;
|
|
||||||
ui.scaleZ.value = 1;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let dx = bounds.max.x - bounds.min.x,
|
|
||||||
dy = bounds.max.y - bounds.min.y,
|
|
||||||
dz = bounds.max.z - bounds.min.z,
|
|
||||||
scale = api.view.unit_scale();
|
|
||||||
ui.sizeX.value = ui.sizeX.was = (dx / scale).round(2)
|
|
||||||
ui.sizeY.value = ui.sizeY.was = (dy / scale).round(2)
|
|
||||||
ui.sizeZ.value = ui.sizeZ.was = (dz / scale).round(2)
|
|
||||||
ui.scaleX.value = ui.scaleX.was = track.scale.x.round(2);
|
|
||||||
ui.scaleY.value = ui.scaleY.was = track.scale.y.round(2);
|
|
||||||
ui.scaleZ.value = ui.scaleZ.was = track.scale.z.round(2);
|
|
||||||
update_bounds();
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_bounds(widgets) {
|
|
||||||
const settings = api.conf.get();
|
|
||||||
// update bounds on selection for drag limiting
|
|
||||||
const isBelt = settings.device.bedBelt;
|
|
||||||
if (isBelt) {
|
|
||||||
if (api.platform.fit()) {
|
|
||||||
api.platform.update_origin();
|
|
||||||
space.update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const dvy = settings.device.bedDepth;
|
|
||||||
const dvx = settings.device.bedWidth;
|
|
||||||
const bounds_sel = new THREE.Box3();
|
|
||||||
if (!widgets) {
|
|
||||||
widgets = selectedMeshes.map(m => m.widget);
|
|
||||||
}
|
|
||||||
for (let widget of widgets) {
|
|
||||||
const wp = widget.track.pos;
|
|
||||||
const bx = widget.track.box;
|
|
||||||
const miny = wp.y - bx.h / 2 + dvy / 2;
|
|
||||||
const maxy = wp.y + bx.h / 2 + dvy / 2;
|
|
||||||
const minx = wp.x - bx.w / 2 + dvx / 2;
|
|
||||||
const maxx = wp.x + bx.w / 2 + dvx / 2;
|
|
||||||
// keep widget in bounds when rotated or scaled
|
|
||||||
const ylo = miny < 0;
|
|
||||||
const yhi = !isBelt && maxy > dvy
|
|
||||||
if (ylo && !yhi) {
|
|
||||||
widget.move(0, -miny, 0);
|
|
||||||
} else if (yhi && !ylo) {
|
|
||||||
widget.move(0, dvy - maxy, 0);
|
|
||||||
}
|
|
||||||
const xlo = minx < 0;
|
|
||||||
const xhi = maxx > dvx;
|
|
||||||
if (xlo && !xhi) {
|
|
||||||
widget.move(-minx, 0, 0);
|
|
||||||
} else if (xhi && !xlo) {
|
|
||||||
widget.move(dvx - maxx, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const wb = widget.mesh.getBoundingBox().clone();
|
|
||||||
wb.min.x += wp.x;
|
|
||||||
wb.max.x += wp.x;
|
|
||||||
wb.min.y += wp.y;
|
|
||||||
wb.max.y += wp.y;
|
|
||||||
bounds_sel.union(wb);
|
|
||||||
}
|
|
||||||
settings.bounds_sel = bounds_sel;
|
|
||||||
}
|
|
||||||
|
|
||||||
function duplicate() {
|
|
||||||
if (!api.view.is_arrange()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for_widgets(widget => {
|
|
||||||
const mesh = widget.mesh;
|
|
||||||
const bb = mesh.getBoundingBox();
|
|
||||||
const ow = widget;
|
|
||||||
const nw = api.widgets.new().loadGeometry(mesh.geometry.clone());
|
|
||||||
nw.meta.file = ow.meta.file;
|
|
||||||
nw.meta.vertices = ow.meta.vertices;
|
|
||||||
nw.anno = ow.annotations();
|
|
||||||
nw.move(bb.max.x - bb.min.x + 1, 0, 0);
|
|
||||||
api.platform.add(nw, true);
|
|
||||||
api.event.emit("widget.duplicate", nw, ow);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function mirror() {
|
|
||||||
if (!api.view.is_arrange()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for_widgets(widget => {
|
|
||||||
widget.mirror();
|
|
||||||
});
|
|
||||||
space.update();
|
|
||||||
api.space.auto_save();
|
|
||||||
}
|
|
||||||
|
|
||||||
function scale() {
|
|
||||||
if (!api.view.is_arrange()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let args = [...arguments];
|
|
||||||
for_groups(w => w.scale(...args));
|
|
||||||
selection.update_bounds();
|
|
||||||
api.platform.update_bounds();
|
|
||||||
api.event.emit('selection.scale', args);
|
|
||||||
// skip update if last argument is strictly 'false'
|
|
||||||
if (args.last() === false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
api.space.auto_save();
|
|
||||||
selection.update_info();
|
|
||||||
space.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
function rotate(x, y, z) {
|
|
||||||
if (!api.view.is_arrange()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for_groups(w => w.rotate(x, y, z));
|
|
||||||
selection.update_bounds();
|
|
||||||
api.platform.update_bounds();
|
|
||||||
api.event.emit('selection.rotate', {x, y, z});
|
|
||||||
api.space.auto_save();
|
|
||||||
selection.update_info();
|
|
||||||
space.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
function merge({ deleteMerged = true }) {
|
|
||||||
let sel = widgets();
|
|
||||||
if (sel.length === 0) {
|
|
||||||
sel = api.widgets.all();
|
|
||||||
}
|
|
||||||
let obj = [];
|
|
||||||
sel.forEach(widget => {
|
|
||||||
let {x, y, z} = widget.track.pos || { x:0, y:0, z:0 };
|
|
||||||
let pos = widget.mesh.geometry.attributes.position;
|
|
||||||
let pvals = pos.array;
|
|
||||||
for (let i=0, il=pos.count; i<il; i += 3) {
|
|
||||||
let pi = i * pos.itemSize;
|
|
||||||
obj.push(pvals[pi++] + x, pvals[pi++] + y, pvals[pi++] + z);
|
|
||||||
obj.push(pvals[pi++] + x, pvals[pi++] + y, pvals[pi++] + z);
|
|
||||||
obj.push(pvals[pi++] + x, pvals[pi++] + y, pvals[pi++] + z);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let w = api.platform.load_verts([], obj, "merged");
|
|
||||||
if (deleteMerged) api.platform.delete(sel);
|
|
||||||
api.platform.select(w);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Isolates the bodies within the selected widgets. If no widgets are selected,
|
|
||||||
* it processes all available widgets. For each widget, it checks if a tool is
|
|
||||||
* associated; if not, it creates a new MeshTool and indexes the vertices.
|
|
||||||
* The function identifies isolated bodies within each widget. If a widget has
|
|
||||||
* a single body, it remains selected. Otherwise, it creates new widgets for
|
|
||||||
* each isolated body and selects them while deleting the original widget.
|
|
||||||
*/
|
|
||||||
function isolateBodies(){
|
|
||||||
let sel = widgets();
|
|
||||||
console.log('isolate', sel);
|
|
||||||
if (sel.length === 0) {
|
|
||||||
sel = api.widgets.all();
|
|
||||||
}
|
|
||||||
for(let widget of sel){
|
|
||||||
if (!widget.tool) {
|
|
||||||
let tool = widget.tool = new MeshTool();
|
|
||||||
tool.index(widget.getGeoVertices({ unroll: true, translate:true }));
|
|
||||||
}
|
|
||||||
let bodies = widget.tool.isolateBodies()
|
|
||||||
if(bodies.length ==1){
|
|
||||||
api.platform.select(widget);
|
|
||||||
}else{
|
|
||||||
// console.log(bodies,Array.from(bodies.entries()))
|
|
||||||
for(let [i,verts] of bodies.entries()){
|
|
||||||
let w = api.platform.load_verts([], verts, `isolate ${i} of ${ widget.anno.file}`);
|
|
||||||
api.platform.select(w)
|
|
||||||
}
|
|
||||||
api.platform.delete(widget);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function exportWidgets(format = "stl") {
|
|
||||||
let sel = widgets();
|
|
||||||
if (sel.length === 0) {
|
|
||||||
sel = api.widgets.all();
|
|
||||||
}
|
|
||||||
let facets = 0;
|
|
||||||
let outs = [];
|
|
||||||
sel.forEach(widget => {
|
|
||||||
let mesh = widget.mesh;
|
|
||||||
let geo = mesh.geometry;
|
|
||||||
outs.push({geo, widget});
|
|
||||||
facets += geo.attributes.position.count;
|
|
||||||
});
|
|
||||||
if (format === "obj") {
|
|
||||||
let obj = [];
|
|
||||||
let vpad = 0;
|
|
||||||
for (let out of outs) {
|
|
||||||
let meta = out.widget.meta;
|
|
||||||
let name = meta.file || 'unnamed';
|
|
||||||
obj.push(`g ${name}`);
|
|
||||||
let { position } = out.geo.attributes;
|
|
||||||
let pvals = position.array;
|
|
||||||
for (let i=0, il=position.count; i<il; i += 3) {
|
|
||||||
let pi = i * position.itemSize;
|
|
||||||
obj.push(`v ${pvals[pi++]} ${pvals[pi++]} ${pvals[pi++]}`);
|
|
||||||
obj.push(`v ${pvals[pi++]} ${pvals[pi++]} ${pvals[pi++]}`);
|
|
||||||
obj.push(`v ${pvals[pi++]} ${pvals[pi++]} ${pvals[pi++]}`);
|
|
||||||
obj.push(`f ${i+1+vpad} ${i+2+vpad} ${i+3+vpad}`);
|
|
||||||
}
|
|
||||||
vpad += position.count;
|
|
||||||
}
|
|
||||||
return obj.join('\n');
|
|
||||||
}
|
|
||||||
let stl = new Uint8Array(80 + 4 + facets/3 * 50);
|
|
||||||
let dat = new DataView(stl.buffer);
|
|
||||||
let pos = 84;
|
|
||||||
dat.setInt32(80, facets/3, true);
|
|
||||||
for (let out of outs) {
|
|
||||||
let { position } = out.geo.attributes;
|
|
||||||
let pvals = position.array;
|
|
||||||
for (let i=0, il=position.count; i<il; i += 3) {
|
|
||||||
let pi = i * position.itemSize;
|
|
||||||
let p0 = new THREE.Vector3(pvals[pi++], pvals[pi++], pvals[pi++]);
|
|
||||||
let p1 = new THREE.Vector3(pvals[pi++], pvals[pi++], pvals[pi++]);
|
|
||||||
let p2 = new THREE.Vector3(pvals[pi++], pvals[pi++], pvals[pi++]);
|
|
||||||
let norm = THREE.computeFaceNormal(p0, p1, p2);
|
|
||||||
let xo = 0, yo = 0, zo = 0;
|
|
||||||
if (outs.length > 1) {
|
|
||||||
let {x, y, z} = out.widget.track.pos;
|
|
||||||
xo = x;
|
|
||||||
yo = y;
|
|
||||||
zo = z;
|
|
||||||
}
|
|
||||||
dat.setFloat32(pos + 0, norm.x, true);
|
|
||||||
dat.setFloat32(pos + 4, norm.y, true);
|
|
||||||
dat.setFloat32(pos + 8, norm.z, true);
|
|
||||||
dat.setFloat32(pos + 12, p0.x + xo, true);
|
|
||||||
dat.setFloat32(pos + 16, p0.y + yo, true);
|
|
||||||
dat.setFloat32(pos + 20, p0.z + zo, true);
|
|
||||||
dat.setFloat32(pos + 24, p1.x + xo, true);
|
|
||||||
dat.setFloat32(pos + 28, p1.y + yo, true);
|
|
||||||
dat.setFloat32(pos + 32, p1.z + zo, true);
|
|
||||||
dat.setFloat32(pos + 36, p2.x + xo, true);
|
|
||||||
dat.setFloat32(pos + 40, p2.y + yo, true);
|
|
||||||
dat.setFloat32(pos + 44, p2.z + zo, true);
|
|
||||||
pos += 50;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return stl;
|
|
||||||
}
|
|
||||||
|
|
||||||
function widgets(orall) {
|
|
||||||
let sel = selectedMeshes.slice().map(m => m.widget);
|
|
||||||
return sel.length ? sel : orall ? api.widgets.all() : []
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDisabled(bool) {
|
|
||||||
for_widgets(w => w.meta.disabled = bool);
|
|
||||||
api.platform.update_selected();
|
|
||||||
}
|
|
||||||
|
|
||||||
// extend API (api.selection)
|
|
||||||
export const selection = {
|
|
||||||
move,
|
|
||||||
merge,
|
|
||||||
isolateBodies,
|
|
||||||
scale,
|
|
||||||
rotate,
|
|
||||||
mirror,
|
|
||||||
duplicate,
|
|
||||||
for_groups,
|
|
||||||
for_meshes,
|
|
||||||
for_status,
|
|
||||||
for_widgets,
|
|
||||||
update_bounds,
|
|
||||||
update_info,
|
|
||||||
widgets,
|
|
||||||
export: exportWidgets,
|
|
||||||
add(w) { selectedMeshes.addOnce(w.mesh) },
|
|
||||||
delete() { api.platform.delete(widgets()) },
|
|
||||||
remove(w) { return selectedMeshes.remove(w.mesh) },
|
|
||||||
count() { return selectedMeshes.length },
|
|
||||||
contains(w) { return selectedMeshes.indexOf(w.mesh) >= 0 },
|
|
||||||
enable() { setDisabled(false) },
|
|
||||||
disable() { setDisabled(true) },
|
|
||||||
opacity() { api.widgets.opacity(...arguments) },
|
|
||||||
meshes() { return selectedMeshes.slice() },
|
|
||||||
};
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
import { Layers } from './layers.js';
|
import { Layers } from '../app/layers.js';
|
||||||
import { newPoint } from '../../geo/point.js';
|
import { newPoint } from '../../geo/point.js';
|
||||||
import { polygons as POLY } from '../../geo/polygons.js';
|
import { polygons as POLY } from '../../geo/polygons.js';
|
||||||
|
|
||||||
|
|
@ -123,9 +123,9 @@ class Slice {
|
||||||
return slice ? POLY.fingerprintCompare(this.fingerprint(), slice.fingerprint()) : false;
|
return slice ? POLY.fingerprintCompare(this.fingerprint(), slice.fingerprint()) : false;
|
||||||
};
|
};
|
||||||
|
|
||||||
addTops(polys) {
|
addTops(polys, opt) {
|
||||||
polys.forEach(p => {
|
polys.forEach(p => {
|
||||||
this.addTop(p);
|
this.addTop(p, opt);
|
||||||
});
|
});
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
@ -134,16 +134,23 @@ class Slice {
|
||||||
* @param {Object | Polygon} data
|
* @param {Object | Polygon} data
|
||||||
* @returns Slice.Top
|
* @returns Slice.Top
|
||||||
*/
|
*/
|
||||||
addTop(data) {
|
addTop(data, opt = {}) {
|
||||||
|
let minArea = opt.minArea ?? 0;
|
||||||
if (data.length) {
|
if (data.length) {
|
||||||
// standard legacy polygon
|
// standard legacy polygon
|
||||||
let top = new Top(data);
|
let top = new Top(data);
|
||||||
|
if (top.area() < minArea) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
this.tops.push(top);
|
this.tops.push(top);
|
||||||
top.simple = data;
|
top.simple = data;
|
||||||
return top;
|
return top;
|
||||||
} else {
|
} else {
|
||||||
// create top object from object bundle passed back by slicePost()
|
// create top object from object bundle passed back by slicePost()
|
||||||
let top = new Top(data.poly);
|
let top = new Top(data.poly);
|
||||||
|
if (top.area() < minArea) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
top.thin_fill = data.thin_fill?.map(p => newPoint(p.x,p.y,p.z));
|
top.thin_fill = data.thin_fill?.map(p => newPoint(p.x,p.y,p.z));
|
||||||
top.thin_wall = data.thin_wall;
|
top.thin_wall = data.thin_wall;
|
||||||
top.thin_sort = data.thin_sort;
|
top.thin_sort = data.thin_sort;
|
||||||
|
|
@ -152,7 +159,7 @@ class Slice {
|
||||||
top.fill_off = data.fill_off;
|
top.fill_off = data.fill_off;
|
||||||
top.last = data.last;
|
top.last = data.last;
|
||||||
top.gaps = data.gaps;
|
top.gaps = data.gaps;
|
||||||
top.shells = data.shells;
|
top.shells = data.shells.filter(p => p.area() > minArea);
|
||||||
top.simple = data.simple;
|
top.simple = data.simple;
|
||||||
this.tops.push(top);
|
this.tops.push(top);
|
||||||
return top;
|
return top;
|
||||||
|
|
@ -208,6 +215,10 @@ class Top {
|
||||||
this.poly = polygon; // outline poly
|
this.poly = polygon; // outline poly
|
||||||
}
|
}
|
||||||
|
|
||||||
|
area() {
|
||||||
|
return this.poly.area();
|
||||||
|
}
|
||||||
|
|
||||||
clone(deep) {
|
clone(deep) {
|
||||||
let top = new Top(this.poly.clone(deep));
|
let top = new Top(this.poly.clone(deep));
|
||||||
return top;
|
return top;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
import { base } from '../../geo/base.js';
|
import { decode } from '../core/codec.js';
|
||||||
import { avgc } from './utils.js';
|
|
||||||
import { util as mesh_util } from '../../mesh/util.js';
|
import { util as mesh_util } from '../../mesh/util.js';
|
||||||
import { tool as mesh_tool } from '../../mesh/tool.js';
|
import { tool as mesh_tool } from '../../mesh/tool.js';
|
||||||
import { verticesToPoints } from '../../geo/points.js';
|
import { verticesToPoints } from '../../geo/points.js';
|
||||||
|
|
@ -10,27 +9,21 @@ import { newPolygon } from '../../geo/polygon.js';
|
||||||
import { polygons as POLY } from '../../geo/polygons.js';
|
import { polygons as POLY } from '../../geo/polygons.js';
|
||||||
import { checkOverUnderOn, intersectPoints } from '../../geo/slicer.js';
|
import { checkOverUnderOn, intersectPoints } from '../../geo/slicer.js';
|
||||||
|
|
||||||
const { inRange, time } = base.util;
|
|
||||||
const solid_opacity = 1.0;
|
|
||||||
const groups = [];
|
|
||||||
|
|
||||||
const sharedArrayClass = self.SharedArrayBuffer || undefined;
|
const sharedArrayClass = self.SharedArrayBuffer || undefined;
|
||||||
const hasSharedArrays = sharedArrayClass ? true : false;
|
const hasSharedArrays = sharedArrayClass ? true : false;
|
||||||
|
const solid_opacity = 1.0;
|
||||||
|
const groups = [];
|
||||||
|
const debug_shadow = false;
|
||||||
|
|
||||||
let nextId = 0;
|
let nextId = 0;
|
||||||
|
|
||||||
function newWidget(id,group) { return new Widget(id,group) }
|
|
||||||
|
|
||||||
function catalog() { return self.kiri_catalog };
|
|
||||||
|
|
||||||
function index() { return self.kiri_catalog.index }
|
|
||||||
|
|
||||||
class Widget {
|
class Widget {
|
||||||
constructor(id, group) {
|
constructor(id, group) {
|
||||||
this.api = self.kiri_api;
|
this.api = self.kiri_api;
|
||||||
|
|
||||||
this.id = id || Date.now().toString(36)+(nextId++);
|
this.id = id || Date.now().toString(36)+(nextId++);
|
||||||
this.grouped = group ? true : false;
|
this.grouped = group ? true : false;
|
||||||
|
// persisted
|
||||||
this.group = group || [];
|
this.group = group || [];
|
||||||
this.group.push(this);
|
this.group.push(this);
|
||||||
if (!this.group.id) {
|
if (!this.group.id) {
|
||||||
|
|
@ -41,10 +34,8 @@ class Widget {
|
||||||
}
|
}
|
||||||
// rotation stack (for undo)
|
// rotation stack (for undo)
|
||||||
this.roto = [];
|
this.roto = [];
|
||||||
// added meshes (supports, tabs, etc)
|
// overlay meshes (supports, tabs, etc)
|
||||||
this.adds = [];
|
this.adds = [];
|
||||||
// persisted client annotations (cam tabs, fdm supports)
|
|
||||||
this.anno = {};
|
|
||||||
// THREE Mesh and points
|
// THREE Mesh and points
|
||||||
this.mesh = null;
|
this.mesh = null;
|
||||||
this.points = null;
|
this.points = null;
|
||||||
|
|
@ -56,6 +47,26 @@ class Widget {
|
||||||
this.settings = null; // used??
|
this.settings = null; // used??
|
||||||
this.modified = true;
|
this.modified = true;
|
||||||
this.boundingBoxNeedsUpdate = true
|
this.boundingBoxNeedsUpdate = true
|
||||||
|
// cache shadow geo
|
||||||
|
this.cache = {};
|
||||||
|
// geometry version for edge cache invalidation
|
||||||
|
this.geomVersion = 0;
|
||||||
|
this.stats = {
|
||||||
|
slice_time: 0,
|
||||||
|
load_time: 0,
|
||||||
|
progress: 0
|
||||||
|
};
|
||||||
|
// if this is a synthesized support widget
|
||||||
|
this.support = false;
|
||||||
|
// persisted: client annotations (cam tabs, fdm supports)
|
||||||
|
this.anno = {};
|
||||||
|
// persisted: file meta-data
|
||||||
|
this.meta = {
|
||||||
|
url: null,
|
||||||
|
file: null,
|
||||||
|
saved: false
|
||||||
|
};
|
||||||
|
// persisted: location state
|
||||||
this.track = {
|
this.track = {
|
||||||
// box size for packer
|
// box size for packer
|
||||||
box: {
|
box: {
|
||||||
|
|
@ -80,62 +91,9 @@ class Widget {
|
||||||
},
|
},
|
||||||
top: 0, // z top
|
top: 0, // z top
|
||||||
mirror: false,
|
mirror: false,
|
||||||
indexed: false
|
indexed: false,
|
||||||
},
|
indexRad: 0
|
||||||
// used to cache shadow geo
|
|
||||||
this.cache = {};
|
|
||||||
this.stats = {
|
|
||||||
slice_time: 0,
|
|
||||||
load_time: 0,
|
|
||||||
progress: 0
|
|
||||||
};
|
};
|
||||||
this.meta = {
|
|
||||||
url: null,
|
|
||||||
file: null,
|
|
||||||
saved: false
|
|
||||||
};
|
|
||||||
// if this is a synthesized support widget
|
|
||||||
this.support = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
saveToCatalog(filename, overwrite) {
|
|
||||||
if (!filename) {
|
|
||||||
filename = this.meta.file;
|
|
||||||
}
|
|
||||||
if (this.grouped && !overwrite) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
const widget = this;
|
|
||||||
const mark = time();
|
|
||||||
const vertices = widget.getGeoVertices({ unroll: true }).slice();
|
|
||||||
widget.meta.file = filename;
|
|
||||||
widget.meta.save = mark;
|
|
||||||
catalog().putFile(filename, vertices, () => {
|
|
||||||
console.log("saved mesh ["+(vertices.length/3)+"] time ["+(time()-mark)+"]");
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
saveState(ondone) {
|
|
||||||
if (!ondone) {
|
|
||||||
clearTimeout(this._save_timer);
|
|
||||||
this._save_timer = setTimeout(() => {
|
|
||||||
this._save_timer = undefined;
|
|
||||||
this.saveState(() => {});
|
|
||||||
}, 1500);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const widget = this;
|
|
||||||
index().put('ws-save-'+this.id, {
|
|
||||||
geo: widget.getGeoVertices({ unroll: false }).slice(),
|
|
||||||
track: widget.track,
|
|
||||||
group: this.group.id,
|
|
||||||
meta: this.meta,
|
|
||||||
anno: this.annotations()
|
|
||||||
}, result => {
|
|
||||||
widget.meta.saved = time();
|
|
||||||
if (ondone) ondone();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
annotations() {
|
annotations() {
|
||||||
|
|
@ -231,9 +189,14 @@ class Widget {
|
||||||
return this.loadVertices(...arguments);
|
return this.loadVertices(...arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
setModified() {
|
setModified(reason) {
|
||||||
this.modified = true;
|
this.modified = true;
|
||||||
this.boundingBoxNeedsUpdate = true;
|
this.boundingBoxNeedsUpdate = true;
|
||||||
|
this.clearShadows();
|
||||||
|
if (reason !== 'axis') {
|
||||||
|
this.cache.geo = undefined;
|
||||||
|
this.geomVersion++; // Increment version to invalidate edge cache
|
||||||
|
}
|
||||||
if (this.mesh && this.mesh.geometry) {
|
if (this.mesh && this.mesh.geometry) {
|
||||||
// this fixes ray intersections after the mesh is modified
|
// this fixes ray intersections after the mesh is modified
|
||||||
this.mesh.geometry.boundingSphere = null;
|
this.mesh.geometry.boundingSphere = null;
|
||||||
|
|
@ -274,6 +237,8 @@ class Widget {
|
||||||
]);
|
]);
|
||||||
mesh.renderOrder = 1;
|
mesh.renderOrder = 1;
|
||||||
// geometry.computeVertexNormals();
|
// geometry.computeVertexNormals();
|
||||||
|
// Clear existing groups (e.g., from cloned geometry) before adding new one
|
||||||
|
geometry.clearGroups();
|
||||||
geometry.addGroup(0, Infinity, 0);
|
geometry.addGroup(0, Infinity, 0);
|
||||||
// mesh.castShadow = true;
|
// mesh.castShadow = true;
|
||||||
// mesh.receiveShadow = true;
|
// mesh.receiveShadow = true;
|
||||||
|
|
@ -311,23 +276,6 @@ class Widget {
|
||||||
this.slices = null;
|
this.slices = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {number} color
|
|
||||||
*/
|
|
||||||
setColor(color, settings, save = true) {
|
|
||||||
if (settings) {
|
|
||||||
console.trace('legacy call with settings');
|
|
||||||
}
|
|
||||||
if (Array.isArray(color)) {
|
|
||||||
color = color[this.getExtruder() % color.length];
|
|
||||||
}
|
|
||||||
if (save) {
|
|
||||||
this.color = color;
|
|
||||||
}
|
|
||||||
let material = this.getMaterial();
|
|
||||||
material.color.set(this.meta.disabled ? avgc(0x888888, color, 3) : color);
|
|
||||||
}
|
|
||||||
|
|
||||||
getColor() {
|
getColor() {
|
||||||
return this.color;
|
return this.color;
|
||||||
}
|
}
|
||||||
|
|
@ -362,47 +310,6 @@ class Widget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getVisualState() {
|
|
||||||
return {
|
|
||||||
edges: this.outline ? true : false,
|
|
||||||
wires: this.wire ? true : false,
|
|
||||||
opacity: this.getMaterial().opacity
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
setVisualState({ edges, wires, opacity}) {
|
|
||||||
this.cache.vizstate = this.getVisualState();
|
|
||||||
this.setEdges(edges ?? false);
|
|
||||||
this.setWireframe(wires ?? false);
|
|
||||||
this.setOpacity(opacity ?? 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshVisualState() {
|
|
||||||
this.setVisualState(this.getVisualState());
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreVisualState() {
|
|
||||||
if (this.cache.vizstate) {
|
|
||||||
this.setVisualState(this.cache.vizstate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {number} value
|
|
||||||
*/
|
|
||||||
setOpacity(value) {
|
|
||||||
const mesh = this.mesh;
|
|
||||||
const mat = this.getMaterial();
|
|
||||||
if (value <= 0.0) {
|
|
||||||
mat.transparent = solid_opacity < 1.0;
|
|
||||||
mat.opacity = solid_opacity;
|
|
||||||
mat.visible = false;
|
|
||||||
} else if (inRange(value, 0.0, solid_opacity)) {
|
|
||||||
mat.transparent = value < 1.0;
|
|
||||||
mat.opacity = value;
|
|
||||||
mat.visible = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleVisibility(bool) {
|
toggleVisibility(bool) {
|
||||||
const mat = this.getMaterial();
|
const mat = this.getMaterial();
|
||||||
|
|
@ -463,7 +370,7 @@ class Widget {
|
||||||
// invalidate cached points
|
// invalidate cached points
|
||||||
if (x || y || z) {
|
if (x || y || z) {
|
||||||
this.points = null;
|
this.points = null;
|
||||||
this.setModified();
|
this.setModified('moveMesh');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -480,11 +387,11 @@ class Widget {
|
||||||
}
|
}
|
||||||
|
|
||||||
setAxisIndex(deg) {
|
setAxisIndex(deg) {
|
||||||
|
// console.trace(deg);
|
||||||
let rad = deg * (Math.PI / 180);
|
let rad = deg * (Math.PI / 180);
|
||||||
if (rad !== this.track.indexRad) {
|
if (rad !== this.track.indexRad) {
|
||||||
this.track.indexRad = rad;
|
this.track.indexRad = rad;
|
||||||
this.clearShadows();
|
this.setModified('axis');
|
||||||
this.setModified();
|
|
||||||
this._updateMeshPosition();
|
this._updateMeshPosition();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -537,27 +444,35 @@ class Widget {
|
||||||
pos.z += (z || 0);
|
pos.z += (z || 0);
|
||||||
}
|
}
|
||||||
if (x || y || z) {
|
if (x || y || z) {
|
||||||
this.setModified();
|
this.setModified('_move');
|
||||||
this._updateMeshPosition();
|
this._updateMeshPosition();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateMeshPosition() {
|
_updateMeshPosition() {
|
||||||
let mesh = this.mesh,
|
let { cache, mesh, track } = this,
|
||||||
track = this.track,
|
{ top, tzoff } = track,
|
||||||
tzoff = track.tzoff,
|
{ x, y, z } = track.pos,
|
||||||
top = track.top,
|
tz = -tzoff;
|
||||||
tz = -tzoff,
|
|
||||||
{ x, y, z } = track.pos;
|
|
||||||
if (track.indexed) {
|
if (track.indexed) {
|
||||||
let rad = track.indexRad;
|
this.mesh.rotation.x = -track.indexRad;
|
||||||
this.mesh.rotation.x = -rad;
|
|
||||||
z = top;
|
z = top;
|
||||||
} else {
|
} else {
|
||||||
this.mesh.rotation.x = 0;
|
this.mesh.rotation.x = 0;
|
||||||
z += tz;
|
z += tz;
|
||||||
}
|
}
|
||||||
mesh.position.set(x, y, z);
|
let { pos } = cache;
|
||||||
|
if (!pos || pos.x !== x || pos.y !==y || pos.z !== z) {
|
||||||
|
mesh.position.set(x, y, z);
|
||||||
|
this._updateEdges();
|
||||||
|
cache.pos = { x, y, z };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateEdges() {
|
||||||
|
if (this.outline && this.setEdges) {
|
||||||
|
this.setEdges(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scale(x, y, z) {
|
scale(x, y, z) {
|
||||||
|
|
@ -574,16 +489,14 @@ class Widget {
|
||||||
let mesh = this.mesh,
|
let mesh = this.mesh,
|
||||||
scale = this.track.scale;
|
scale = this.track.scale;
|
||||||
this.bounds = null;
|
this.bounds = null;
|
||||||
this.setWireframe(false);
|
if (this.setWireframe) this.setWireframe(false);
|
||||||
this.clearSlices();
|
this.clearSlices();
|
||||||
mesh.geometry.applyMatrix4(new THREE.Matrix4().makeScale(x, y, z));
|
mesh.geometry.applyMatrix4(new THREE.Matrix4().makeScale(x, y, z));
|
||||||
if (this.outline) {
|
this._updateEdges();
|
||||||
this.setEdges(true);
|
|
||||||
}
|
|
||||||
scale.x *= (x || 1.0);
|
scale.x *= (x || 1.0);
|
||||||
scale.y *= (y || 1.0);
|
scale.y *= (y || 1.0);
|
||||||
scale.z *= (z || 1.0);
|
scale.z *= (z || 1.0);
|
||||||
this.setModified();
|
this.setModified('_scale');
|
||||||
}
|
}
|
||||||
|
|
||||||
rotate(x, y, z, temp, center = true) {
|
rotate(x, y, z, temp, center = true) {
|
||||||
|
|
@ -593,9 +506,7 @@ class Widget {
|
||||||
if (center) {
|
if (center) {
|
||||||
this.center(false);
|
this.center(false);
|
||||||
}
|
}
|
||||||
if (this.outline) {
|
this._updateEdges();
|
||||||
this.setEdges(true);
|
|
||||||
}
|
|
||||||
if ((x || y || z) && this.api && this.api.event) {
|
if ((x || y || z) && this.api && this.api.event) {
|
||||||
this.api.event.emit('widget.rotate', {widget: this, x, y, z});
|
this.api.event.emit('widget.rotate', {widget: this, x, y, z});
|
||||||
}
|
}
|
||||||
|
|
@ -604,7 +515,7 @@ class Widget {
|
||||||
_rotate(x, y, z, temp) {
|
_rotate(x, y, z, temp) {
|
||||||
if (!temp) {
|
if (!temp) {
|
||||||
this.bounds = null;
|
this.bounds = null;
|
||||||
this.setWireframe(false);
|
if (this.setWireframe) this.setWireframe(false);
|
||||||
this.clearSlices();
|
this.clearSlices();
|
||||||
}
|
}
|
||||||
let m4 = new THREE.Matrix4();
|
let m4 = new THREE.Matrix4();
|
||||||
|
|
@ -622,7 +533,7 @@ class Widget {
|
||||||
rot.y += (y || 0);
|
rot.y += (y || 0);
|
||||||
rot.z += (z || 0);
|
rot.z += (z || 0);
|
||||||
}
|
}
|
||||||
this.setModified();
|
this.setModified('_rotate');
|
||||||
}
|
}
|
||||||
|
|
||||||
// undo all accumulated rotations
|
// undo all accumulated rotations
|
||||||
|
|
@ -632,8 +543,8 @@ class Widget {
|
||||||
});
|
});
|
||||||
this.roto = [];
|
this.roto = [];
|
||||||
this.center();
|
this.center();
|
||||||
this.setModified();
|
this.setModified('unrotate');
|
||||||
this.refreshVisualState();
|
if (this.refreshVisualState) this.refreshVisualState();
|
||||||
}
|
}
|
||||||
|
|
||||||
mirror() {
|
mirror() {
|
||||||
|
|
@ -648,7 +559,7 @@ class Widget {
|
||||||
|
|
||||||
_mirror() {
|
_mirror() {
|
||||||
this.clearSlices();
|
this.clearSlices();
|
||||||
this.setWireframe(false);
|
if (this.setWireframe) this.setWireframe(false);
|
||||||
let geo = this.mesh.geometry, ot = this.track;
|
let geo = this.mesh.geometry, ot = this.track;
|
||||||
let pos = geo.attributes.position;
|
let pos = geo.attributes.position;
|
||||||
let arr = pos.array;
|
let arr = pos.array;
|
||||||
|
|
@ -671,7 +582,7 @@ class Widget {
|
||||||
}
|
}
|
||||||
pos.needsUpdate = true;
|
pos.needsUpdate = true;
|
||||||
ot.mirror = !ot.mirror;
|
ot.mirror = !ot.mirror;
|
||||||
this.setModified();
|
this.setModified('_mirror');
|
||||||
this.points = null;
|
this.points = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -694,6 +605,17 @@ class Widget {
|
||||||
|
|
||||||
getGeoVertices(opt = {}) {
|
getGeoVertices(opt = {}) {
|
||||||
const { unroll, translate } = opt;
|
const { unroll, translate } = opt;
|
||||||
|
let cacheKey = [
|
||||||
|
unroll ? 1 : 0,
|
||||||
|
translate ? 1 : 0,
|
||||||
|
((this.track.indexRad ?? 0) * 100000) | 0
|
||||||
|
].join(',');
|
||||||
|
let marked = Date.now();
|
||||||
|
let geoCache = this.cache.geo = (this.cache.geo || {});
|
||||||
|
let cached = geoCache[cacheKey];
|
||||||
|
if (cached) {
|
||||||
|
return cached.pos;
|
||||||
|
}
|
||||||
let geo = this.mesh.geometry;
|
let geo = this.mesh.geometry;
|
||||||
let pos = geo.getAttribute('position');
|
let pos = geo.getAttribute('position');
|
||||||
// if indexed, return points rotated about X and then offset
|
// if indexed, return points rotated about X and then offset
|
||||||
|
|
@ -703,6 +625,7 @@ class Widget {
|
||||||
.applyMatrix4( new THREE.Matrix4().makeRotationX(-track.indexRad) );
|
.applyMatrix4( new THREE.Matrix4().makeRotationX(-track.indexRad) );
|
||||||
}
|
}
|
||||||
pos = pos.array;
|
pos = pos.array;
|
||||||
|
// unroll indexed geometry
|
||||||
if (geo.index && unroll !== false) {
|
if (geo.index && unroll !== false) {
|
||||||
let idx = geo.index.array;
|
let idx = geo.index.array;
|
||||||
let len = idx.length;
|
let len = idx.length;
|
||||||
|
|
@ -716,12 +639,8 @@ class Widget {
|
||||||
pp2[inc++] = pos[ip];
|
pp2[inc++] = pos[ip];
|
||||||
}
|
}
|
||||||
pos = pp2;
|
pos = pp2;
|
||||||
} else if (translate) {
|
|
||||||
pos = pos.slice();
|
|
||||||
}
|
}
|
||||||
// used by CAM.shadowAt()
|
geoCache[cacheKey] = { pos, marked };
|
||||||
this.cache.geo = pos;
|
|
||||||
delete this.cache.shadow;
|
|
||||||
return pos;
|
return pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -739,7 +658,7 @@ class Widget {
|
||||||
if (!this.bounds || refresh || this.boundingBoxNeedsUpdate) {
|
if (!this.bounds || refresh || this.boundingBoxNeedsUpdate) {
|
||||||
this.bounds = new THREE.Box3().setFromArray(this.getGeoVertices({
|
this.bounds = new THREE.Box3().setFromArray(this.getGeoVertices({
|
||||||
translate: true,
|
translate: true,
|
||||||
unroll: false
|
unroll: true
|
||||||
}));
|
}));
|
||||||
this.boundingBoxNeedsUpdate = false;
|
this.boundingBoxNeedsUpdate = false;
|
||||||
}
|
}
|
||||||
|
|
@ -782,65 +701,6 @@ class Widget {
|
||||||
return Date.now() - mark;
|
return Date.now() - mark;
|
||||||
}
|
}
|
||||||
|
|
||||||
setEdges(set) {
|
|
||||||
if (!(this.api && this.api.conf)) {
|
|
||||||
// missing api features in engine mode
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let mesh = this.mesh;
|
|
||||||
if (set && set.toggle) {
|
|
||||||
set = this.outline ? false : true;
|
|
||||||
}
|
|
||||||
if (this.outline) {
|
|
||||||
mesh.remove(this.outline);
|
|
||||||
this.outline = null;
|
|
||||||
|
|
||||||
}
|
|
||||||
if (set) {
|
|
||||||
let dark = this.api.space.is_dark();
|
|
||||||
let cam = this.api.mode.is_cam();
|
|
||||||
let color = dark ? 0x444444 : 0x888888;
|
|
||||||
let angle = this.api.conf.get().controller.edgeangle || 20;
|
|
||||||
let edges = new THREE.EdgesGeometry(mesh.geometry, angle);
|
|
||||||
let material = new THREE.LineBasicMaterial({ color });
|
|
||||||
this.outline = new THREE.LineSegments(edges, material);
|
|
||||||
this.outline.renderOrder = -20;
|
|
||||||
mesh.add(this.outline);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setWireframe(set, color, opacity) {
|
|
||||||
if (!(this.api && this.api.conf)) {
|
|
||||||
// missing api features in engine mode
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let mesh = this.mesh,
|
|
||||||
widget = this;
|
|
||||||
if (this.wire) {
|
|
||||||
this.setOpacity(solid_opacity);
|
|
||||||
mesh.remove(this.wire);
|
|
||||||
this.wire = null;
|
|
||||||
}
|
|
||||||
if (set) {
|
|
||||||
let dark = this.api.space.is_dark();
|
|
||||||
let mat = new THREE.MeshBasicMaterial({
|
|
||||||
wireframe: true,
|
|
||||||
color: dark ? 0xaaaaaa : 0,
|
|
||||||
opacity: 0.5,
|
|
||||||
transparent: true
|
|
||||||
})
|
|
||||||
let wire = widget.wire = new THREE.Mesh(mesh.geometry.shallowClone(), mat);
|
|
||||||
mesh.add(wire);
|
|
||||||
}
|
|
||||||
if (this.api.view.is_arrange()) {
|
|
||||||
this.setColor(this.color);
|
|
||||||
} else {
|
|
||||||
this.setColor(0x888888,undefined,false);
|
|
||||||
}
|
|
||||||
if (opacity !== undefined) {
|
|
||||||
widget.setOpacity(opacity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
show() {
|
show() {
|
||||||
this.mesh.visible = true;
|
this.mesh.visible = true;
|
||||||
|
|
@ -856,7 +716,12 @@ class Widget {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
async shadowAt(z) {
|
/**
|
||||||
|
* @param {number} z height for shadow computation
|
||||||
|
* @param {number | undefined} pocket normal value to match faces
|
||||||
|
* @returns {Polygon[]}
|
||||||
|
*/
|
||||||
|
async shadowAt(z, pocket) {
|
||||||
let shadows = this.cache.shadows;
|
let shadows = this.cache.shadows;
|
||||||
if (!shadows) {
|
if (!shadows) {
|
||||||
shadows = this.cache.shadows = {};
|
shadows = this.cache.shadows = {};
|
||||||
|
|
@ -868,20 +733,34 @@ class Widget {
|
||||||
// find closest shadow above and use to speed up delta shadow gen
|
// find closest shadow above and use to speed up delta shadow gen
|
||||||
let zover = Object.keys(shadows).map(v => parseFloat(v)).filter(v => v > z);
|
let zover = Object.keys(shadows).map(v => parseFloat(v)).filter(v => v > z);
|
||||||
let minZabove = Math.min(Infinity, ...zover);
|
let minZabove = Math.min(Infinity, ...zover);
|
||||||
let shadow = this.#computeShadowAt(z - 0.005, minZabove);
|
// shift shadow probeline down a fraction to capture z flats with FP noise
|
||||||
|
let shadow = this.#computeShadowAt(z - 0.005, minZabove, undefined, pocket);
|
||||||
if (minZabove < Infinity) {
|
if (minZabove < Infinity) {
|
||||||
shadow = POLY.union([...shadow, ...shadows[minZabove]], 0.001, true, { wasm: false });
|
shadow = POLY.union([...shadow, ...shadows[minZabove]], 0, true, { wasm: false });
|
||||||
|
// cull interior sliver voids
|
||||||
|
if (!pocket)
|
||||||
|
for (let poly of shadow) {
|
||||||
|
if (poly.inner) {
|
||||||
|
poly.inner = poly.inner.filter(inr => {
|
||||||
|
let A = inr.area();
|
||||||
|
let P = inr.perimeter();
|
||||||
|
let R = (2 * A / P);
|
||||||
|
return R > 0.05;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return shadows[z] = POLY.setZ(shadow, z);
|
return shadows[z] = POLY.setZ(shadow, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
// create a stack of faces in 1mm increments
|
// create a stack of faces in 1mm increments
|
||||||
// the stacks are then used to produce shadowlines
|
// the stacks are then used to produce shadowlines
|
||||||
#ensureShadowCache() {
|
#ensureShadowCache(pocket) {
|
||||||
if (this.cache.shadow) {
|
if (this.cache.shadow) {
|
||||||
return;
|
return this.cache.shadow;
|
||||||
}
|
}
|
||||||
const geo = this.cache.geo;
|
if (debug_shadow) console.time('shadow buckets');
|
||||||
|
const geo = this.getGeoVertices({ unroll: true, translate: true });
|
||||||
const length = geo.length;
|
const length = geo.length;
|
||||||
const bounds = this.getBoundingBox();
|
const bounds = this.getBoundingBox();
|
||||||
const stack = {};
|
const stack = {};
|
||||||
|
|
@ -893,7 +772,8 @@ class Widget {
|
||||||
const b = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]);
|
const b = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]);
|
||||||
const c = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]);
|
const c = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]);
|
||||||
const n = THREE.computeFaceNormal(a, b, c);
|
const n = THREE.computeFaceNormal(a, b, c);
|
||||||
if (n.z < 0.001) {
|
// todo: use pocket to match normal values when set
|
||||||
|
if ((pocket && n.z > -pocket) || (!pocket && n.z < 0.001)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const minZ = Math.floor(Math.min(a.z, b.z, c.z));
|
const minZ = Math.floor(Math.min(a.z, b.z, c.z));
|
||||||
|
|
@ -902,15 +782,55 @@ class Widget {
|
||||||
stack[z].push(a, b, c);
|
stack[z].push(a, b, c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.cache.shadow = stack;
|
if (debug_shadow) console.timeEnd('shadow buckets');
|
||||||
|
return this.cache.shadow = stack;
|
||||||
|
}
|
||||||
|
|
||||||
|
async computeShadowStack(zlist, progress, pocket) {
|
||||||
|
let shadow_stack = this.cache.shadow_stack;
|
||||||
|
if (!shadow_stack) {
|
||||||
|
shadow_stack = this.cache.shadow_stack = {};
|
||||||
|
}
|
||||||
|
let work = self.kiri_worker;
|
||||||
|
let stack = this.#ensureShadowCache(pocket);
|
||||||
|
let plist = [];
|
||||||
|
if (debug_shadow) console.time('seed minion buckets');
|
||||||
|
work.minions.broadcast('cam_shadow_stack', stack);
|
||||||
|
if (debug_shadow) console.timeEnd('seed minion buckets');
|
||||||
|
let pinc = 1 / zlist.length;
|
||||||
|
let pval = 0;
|
||||||
|
for (let z of zlist) {
|
||||||
|
let p = work.minions.queueAsync({
|
||||||
|
cmd: 'cam_shadow_z',
|
||||||
|
z: z - 0.005,
|
||||||
|
t: z + 1
|
||||||
|
}).then(reply => {
|
||||||
|
shadow_stack[z - 0.005] = decode(reply.data);
|
||||||
|
pval += pinc;
|
||||||
|
progress(pval);
|
||||||
|
});
|
||||||
|
plist.push(p);
|
||||||
|
}
|
||||||
|
await Promise.all(plist);
|
||||||
|
work.minions.broadcast('cam_shadow_stack', { clear: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// called from minion
|
||||||
|
computeShadowAtZ(z, ztop, cached, pocket) {
|
||||||
|
return this.#computeShadowAt(z, ztop, cached, pocket);
|
||||||
}
|
}
|
||||||
|
|
||||||
// union triangles > z (opt cap < ztop) into polygon(s)
|
// union triangles > z (opt cap < ztop) into polygon(s)
|
||||||
// slice the triangle stack matchingg z then union the results
|
// slice the triangle stack matchingg z then union the results
|
||||||
#computeShadowAt(z, ztop) {
|
#computeShadowAt(z, ztop, cached, pocket) {
|
||||||
this.#ensureShadowCache();
|
let shadow_stack = this.cache.shadow_stack;
|
||||||
|
if (shadow_stack && shadow_stack[z]) {
|
||||||
|
return shadow_stack[z];
|
||||||
|
}
|
||||||
|
let label = `compute shadow ${z.round(2)}`;
|
||||||
|
if (debug_shadow) console.time(label);
|
||||||
const found = [];
|
const found = [];
|
||||||
const stack = this.cache.shadow;
|
const stack = cached ?? this.#ensureShadowCache(pocket);
|
||||||
let minZ = Math.floor(z);
|
let minZ = Math.floor(z);
|
||||||
let maxZ = Math.ceil(z);
|
let maxZ = Math.ceil(z);
|
||||||
let slices = [];
|
let slices = [];
|
||||||
|
|
@ -965,84 +885,18 @@ class Widget {
|
||||||
});
|
});
|
||||||
|
|
||||||
// recursively merge grid constrained subsets of polygons
|
// recursively merge grid constrained subsets of polygons
|
||||||
polys = unionTris(polys);
|
polys = POLY.unionFaces(polys);
|
||||||
|
|
||||||
// for a more perfect union, pump shadows to merge very close lines
|
// for a more perfect union, pump shadows to merge very close lines
|
||||||
// todo: create clipper only version that avoids round trip thru geo classes
|
// todo: create clipper only version that avoids round trip thru geo classes
|
||||||
polys = POLY.offset(polys, 0.01);
|
polys = POLY.offset(polys, 0.01);
|
||||||
polys = POLY.offset(polys, -0.01);
|
polys = POLY.offset(polys, -0.01);
|
||||||
|
|
||||||
|
if (debug_shadow) console.timeEnd(label);
|
||||||
return polys;
|
return polys;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* recursively process triangle soup from mesh by bucketing them
|
|
||||||
* by grid cell locality with max depth. this minimizes the number
|
|
||||||
* of progressive small unions. results in a few unions of larger polys.
|
|
||||||
*/
|
|
||||||
function unionTris(polys, depth = 0) {
|
|
||||||
if (depth < 5 && polys.length > 1000) {
|
|
||||||
// Calculate bounding box of all triangles
|
|
||||||
let minX = Infinity, maxX = -Infinity;
|
|
||||||
let minY = Infinity, maxY = -Infinity;
|
|
||||||
for (let tri of polys) {
|
|
||||||
for (let pt of tri.points) {
|
|
||||||
minX = Math.min(minX, pt.x);
|
|
||||||
maxX = Math.max(maxX, pt.x);
|
|
||||||
minY = Math.min(minY, pt.y);
|
|
||||||
maxY = Math.max(maxY, pt.y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create NxN grid
|
|
||||||
const gridSize = 3;
|
|
||||||
const cellWidth = (maxX - minX) / gridSize;
|
|
||||||
const cellHeight = (maxY - minY) / gridSize;
|
|
||||||
|
|
||||||
// Partition polygons into grid cells
|
|
||||||
const grid = Array(gridSize).fill(0).map(() => Array(gridSize).fill(0).map(() => []));
|
|
||||||
|
|
||||||
for (let i = 0; i < polys.length; i++) {
|
|
||||||
const poly = polys[i];
|
|
||||||
// Use polygon centroid to assign to cell
|
|
||||||
const bounds = poly.bounds;
|
|
||||||
const cx = (bounds.minx + bounds.maxx) / 2;
|
|
||||||
const cy = (bounds.miny + bounds.maxy) / 2;
|
|
||||||
|
|
||||||
// Clamp to grid bounds
|
|
||||||
let gx = Math.floor((cx - minX) / cellWidth);
|
|
||||||
let gy = Math.floor((cy - minY) / cellHeight);
|
|
||||||
gx = Math.max(0, Math.min(gridSize - 1, gx));
|
|
||||||
gy = Math.max(0, Math.min(gridSize - 1, gy));
|
|
||||||
|
|
||||||
grid[gx][gy].push(poly);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Union within each cell
|
|
||||||
const cellResults = [];
|
|
||||||
let totalInCell = 0;
|
|
||||||
for (let gx = 0; gx < gridSize; gx++) {
|
|
||||||
for (let gy = 0; gy < gridSize; gy++) {
|
|
||||||
const cellPolys = grid[gx][gy];
|
|
||||||
if (cellPolys.length > 0) {
|
|
||||||
totalInCell += cellPolys.length;
|
|
||||||
const cellUnion = unionTris(cellPolys, depth + 1);
|
|
||||||
cellResults.push(...cellUnion);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Union the cell results
|
|
||||||
polys = unionTris(cellResults, depth + 1);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
polys = POLY.union(polys, 0, true, { wasm: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
return polys;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Widget Grouping API
|
// Widget Grouping API
|
||||||
const Group = Widget.Groups = {
|
const Group = Widget.Groups = {
|
||||||
list() {
|
list() {
|
||||||
|
|
@ -1143,40 +997,8 @@ const Group = Widget.Groups = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Widget.loadFromCatalog = function(filename, ondone) {
|
function newWidget(id,group) {
|
||||||
catalog().getFile(filename, function(data) {
|
return new Widget(id,group);
|
||||||
let widget = newWidget().loadVertices(data);
|
}
|
||||||
widget.meta.file = filename;
|
|
||||||
ondone(widget);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Widget.loadFromState = function(id, ondone, move) {
|
|
||||||
index().get('ws-save-'+id, function(data) {
|
|
||||||
if (data) {
|
|
||||||
let vertices = data.geo || data,
|
|
||||||
track = data.track || undefined,
|
|
||||||
group = data.group || id,
|
|
||||||
anno = data.anno || undefined,
|
|
||||||
widget = newWidget(id, Group.forid(group)),
|
|
||||||
meta = data.meta || widget.meta,
|
|
||||||
ptr = widget.loadVertices(vertices);
|
|
||||||
widget.meta = meta;
|
|
||||||
widget.anno = anno || widget.anno;
|
|
||||||
// restore widget position if specified
|
|
||||||
if (move && track && track.pos) {
|
|
||||||
widget.track = track;
|
|
||||||
widget.move(track.pos.x, track.pos.y, track.pos.z, true);
|
|
||||||
}
|
|
||||||
ondone(ptr);
|
|
||||||
} else {
|
|
||||||
ondone(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Widget.deleteFromState = function(id,ondone) {
|
|
||||||
index().remove('ws-save-'+id, ondone);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { Widget, newWidget };
|
export { Widget, newWidget };
|
||||||
|
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
{
|
|
||||||
"pre":[
|
|
||||||
"M117 Heating ; announce heating",
|
|
||||||
"M107 ; turn off filament cooling fan",
|
|
||||||
"M104 S{temp} T0 ; set extruder temperature",
|
|
||||||
"M140 S{bed_temp} T0 ; set bed temperature",
|
|
||||||
"G90 ; set absolute positioning mode",
|
|
||||||
"M83 ; set relative positioning for extruder",
|
|
||||||
"G28 ; home axes",
|
|
||||||
"G1 F3000 ; set default move speed",
|
|
||||||
"M190 S{bed_temp} T0 ; wait for bed to reach target temp",
|
|
||||||
"M109 S{temp} T0 ; wait for extruder to reach target temp",
|
|
||||||
"M117 Purge ; announce purge",
|
|
||||||
"G92 E0 ; set extruder position to 0",
|
|
||||||
"G1 E20 F250 ; purge 20mm from extruder",
|
|
||||||
"G1 X0 Y0 Z0 E1 F3000 ; move to origin / wipe",
|
|
||||||
"M117 Start Print ; announce start of print"
|
|
||||||
],
|
|
||||||
"post":[
|
|
||||||
"M117 Done ; announce done",
|
|
||||||
"M107 ; turn off filament cooling fan",
|
|
||||||
"M104 S0 T0 ; turn off extruder",
|
|
||||||
"M140 S0 T0 ; turn off bed",
|
|
||||||
"G91 ; relative movement",
|
|
||||||
"G1 Z10 F1200 ; drop bed 10mm",
|
|
||||||
"G90 ; absolute positioning",
|
|
||||||
"G28 X0 Y0 ; home XY axes",
|
|
||||||
"M84 ; disable stepper motors"
|
|
||||||
],
|
|
||||||
"cmd":{
|
|
||||||
"fan_power": "M106 S{fan_speed}",
|
|
||||||
"progress": "M117 {progress}% layer {layer} / {layers}"
|
|
||||||
},
|
|
||||||
"settings":{
|
|
||||||
"image_url": "/decal/gridbot.png",
|
|
||||||
"image_anchor": 0,
|
|
||||||
"image_scale": 1,
|
|
||||||
"origin_center": false,
|
|
||||||
"bed_width": 300,
|
|
||||||
"bed_depth": 300,
|
|
||||||
"build_height": 300
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
{
|
|
||||||
"pre":[
|
|
||||||
"M117 Heating ; announce heating",
|
|
||||||
"M107 ; turn off filament cooling fan",
|
|
||||||
"M104 S{temp} T0 ; set extruder temperature",
|
|
||||||
"M140 S{bed_temp} T0 ; set bed temperature",
|
|
||||||
"G90 ; set absolute positioning mode",
|
|
||||||
"M83 ; set relative positioning for extruder",
|
|
||||||
"G28 ; home axes",
|
|
||||||
"G1 F3000 ; set default move speed",
|
|
||||||
"M190 S{bed_temp} T0 ; wait for bed to reach target temp",
|
|
||||||
"M109 S{temp} T0 ; wait for extruder to reach target temp",
|
|
||||||
"M117 Purge ; announce purge",
|
|
||||||
"G92 E0 ; set extruder position to 0",
|
|
||||||
"G1 E20 F250 ; purge 20mm from extruder",
|
|
||||||
"G1 X0 Y0 Z0 E1 F3000 ; move to origin / wipe",
|
|
||||||
"M117 Start Print ; announce start of print"
|
|
||||||
],
|
|
||||||
"post":[
|
|
||||||
"M117 Done ; announce done",
|
|
||||||
"M107 ; turn off filament cooling fan",
|
|
||||||
"M104 S0 T0 ; turn off extruder",
|
|
||||||
"M140 S0 T0 ; turn off bed",
|
|
||||||
"G91 ; relative movement",
|
|
||||||
"G1 Z10 F1200 ; drop bed 10mm",
|
|
||||||
"G90 ; absolute positioning",
|
|
||||||
"G28 X0 Y0 ; home XY axes",
|
|
||||||
"M84 ; disable stepper motors"
|
|
||||||
],
|
|
||||||
"cmd":{
|
|
||||||
"fan_power": "M106 S{fan_speed}",
|
|
||||||
"progress": "M117 {progress}% layer {layer} / {layers}"
|
|
||||||
},
|
|
||||||
"settings":{
|
|
||||||
"image_url": "/decal/gridbot.png",
|
|
||||||
"image_anchor": 0,
|
|
||||||
"image_scale": 1,
|
|
||||||
"origin_center": false,
|
|
||||||
"bed_width": 300,
|
|
||||||
"bed_depth": 300,
|
|
||||||
"build_height": 300
|
|
||||||
}
|
|
||||||
}
|
|
||||||
400
src/kiri/dev/fdm/Snapmaker.U1.json
Normal file
400
src/kiri/dev/fdm/Snapmaker.U1.json
Normal file
|
|
@ -0,0 +1,400 @@
|
||||||
|
{
|
||||||
|
"mode": "FDM",
|
||||||
|
"internal": 0,
|
||||||
|
"bedHeight": 2.5,
|
||||||
|
"bedWidth": 270,
|
||||||
|
"bedDepth": 270,
|
||||||
|
"bedRound": false,
|
||||||
|
"maxHeight": 270,
|
||||||
|
"originCenter": false,
|
||||||
|
"gcodeFan": [
|
||||||
|
";; >-----> FAN >------------------------>",
|
||||||
|
"M106 P1 S{fan_speed} ; set fan speed",
|
||||||
|
";; <-----< FAN <------------------------<"
|
||||||
|
],
|
||||||
|
"gcodeTrack": [
|
||||||
|
";; >-----> PROGRESS >------------------->",
|
||||||
|
"M73 P{progress} R{remain_time} ; report progress % and remaining time",
|
||||||
|
";; <-----< PROGRESS <-------------------<"
|
||||||
|
],
|
||||||
|
"gcodeLayer": [
|
||||||
|
";; >-----> LAYER >---------------------->",
|
||||||
|
";BEFORE_LAYER_CHANGE",
|
||||||
|
";[layer_z]",
|
||||||
|
"G92 E0",
|
||||||
|
"TIMELAPSE_TAKE_FRAME",
|
||||||
|
"DEFECT_DETECTION_DETECT",
|
||||||
|
";AFTER_LAYER_CHANGE",
|
||||||
|
";[layer_z]",
|
||||||
|
"SET_PRINT_STATS_INFO TOTAL_LAYER={layers}",
|
||||||
|
"SET_PRINT_STATS_INFO CURRENT_LAYER={layer}",
|
||||||
|
";; <-----< LAYER <----------------------<"
|
||||||
|
],
|
||||||
|
"gcodePre": [
|
||||||
|
";; >-----> HEADER >--------------------->",
|
||||||
|
";; PREAMBLE POST",
|
||||||
|
"; HEADER_BLOCK_START",
|
||||||
|
"; generated by Kiri:Moto",
|
||||||
|
"; total layer number: {layers}",
|
||||||
|
"; filament_density: 1.24,1.24,1.24,1.24",
|
||||||
|
"; filament_diameter: 1.75,1.75,1.75,1.75",
|
||||||
|
"; max_z_height: {z_max}",
|
||||||
|
"; HEADER_BLOCK_END",
|
||||||
|
"; EXECUTABLE_BLOCK_START",
|
||||||
|
"M73 P0 R1 ; set print progress to 0%, remaining time 1 min",
|
||||||
|
"M106 S0 ; turn off primary cooling fan",
|
||||||
|
"M106 P2 S0 ; turn off auxiliary/part fan (fan index 2)",
|
||||||
|
"PRINT_START ; firmware macro: begin print sequence",
|
||||||
|
"DEFECT_DETECTION_START ; start bed surface defect detection system",
|
||||||
|
"SET_PRINT_STATS_INFO TOTAL_LAYER=3 ; set total layer count to 3",
|
||||||
|
"SET_PRINT_STATS_INFO CURRENT_LAYER=0 ; initialize current layer to 0",
|
||||||
|
"TIMELAPSE_START ; start timelapse capture",
|
||||||
|
"M140 S65 ; set bed temperature to 65C (no wait)",
|
||||||
|
"M104 T{tool} S140 ; set tool 0 nozzle temp to 140C (no wait)",
|
||||||
|
"M204 S10000 ; set default acceleration",
|
||||||
|
"G28 X Y ; home X and Y axes",
|
||||||
|
";===== Bed surface foreign-object detection ========",
|
||||||
|
"T{tool} ; select tool 0",
|
||||||
|
"G90 ; absolute positioning",
|
||||||
|
"DEFECT_DETECTION_DETECT_BED ; scan bed for debris or foreign objects",
|
||||||
|
";===== Tool pickup / toolhead detection =================",
|
||||||
|
"SM_PRINT_CHECK_SWITCH_EXTRUDER ; verify extruder/tool switch state",
|
||||||
|
";===== Auto feed, flow calibration & pre-extrusion ======================",
|
||||||
|
"SM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140 ; preheat extruder 1",
|
||||||
|
"SM_PRINT_AUTO_FEED EXTRUDER=0 ; auto-load filament for extruder 0",
|
||||||
|
"SM_PRINT_FLOW_CALIBRATE EXTRUDER=0 ; calibrate flow for extruder 0",
|
||||||
|
"SM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140 ; preheat extruder 2",
|
||||||
|
"SM_PRINT_AUTO_FEED EXTRUDER=1 ; auto-load filament for extruder 1",
|
||||||
|
"SM_PRINT_FLOW_CALIBRATE EXTRUDER=1 ; calibrate flow for extruder 1",
|
||||||
|
"SM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140 ; preheat extruder 3",
|
||||||
|
"SM_PRINT_AUTO_FEED EXTRUDER=2 ; auto-load filament for extruder 2",
|
||||||
|
"SM_PRINT_FLOW_CALIBRATE EXTRUDER=2 ; calibrate flow for extruder 2",
|
||||||
|
"SM_PRINT_AUTO_FEED EXTRUDER=3 ; auto-load filament for extruder 3",
|
||||||
|
"SM_PRINT_FLOW_CALIBRATE EXTRUDER=3 ; calibrate flow for extruder 3",
|
||||||
|
"M104 S0 T0 A0 ; disable heater on tool 0",
|
||||||
|
"M104 S0 T1 A0 ; disable heater on tool 1",
|
||||||
|
"M104 S0 T2 A0 ; disable heater on tool 2",
|
||||||
|
"M104 S0 T3 A0 ; disable heater on tool 3",
|
||||||
|
"M104 T{tool} S130 ; set first tool nozzle temp to 130C",
|
||||||
|
";===== Coarse homing =================",
|
||||||
|
"T{tool} ; select first tool",
|
||||||
|
"M106 S255 ; turn primary fan to full speed",
|
||||||
|
"M106 P2 S0 ; ensure auxiliary fan is off",
|
||||||
|
"MOVE_TO_DISCARD_FILAMENT_POSITION ; move to filament discard / purge location",
|
||||||
|
"M109 T{tool} S130 ; wait for nozzle to reach 130C",
|
||||||
|
"ROUGHLY_CLEAN_NOZZLE_WITH_DISCARD ; coarse nozzle cleaning with discard",
|
||||||
|
"MOVE_TO_XY_IDLE_POSITION_EXTRUDER ; move toolhead to idle XY position",
|
||||||
|
"G28 Z I140 J140 ; home Z axis with specified probe offsets",
|
||||||
|
";===== Build plate detection =================",
|
||||||
|
"DETECT_BED_PLATE ; detect presence/type of build plate",
|
||||||
|
";===== Deep nozzle cleaning =================",
|
||||||
|
"G90 ; absolute positioning",
|
||||||
|
"G0 Z5 F10000 ; raise Z to safe height",
|
||||||
|
"MOVE_TO_DISCARD_FILAMENT_POSITION ; move to discard position",
|
||||||
|
"M109 S170 ; heat nozzle to deep-clean temperature",
|
||||||
|
"ROUGHLY_CLEAN_NOZZLE ; coarse nozzle cleaning",
|
||||||
|
"MOVE_TO_XY_IDLE_POSITION_EXTRUDER ; move to idle position",
|
||||||
|
"FINELY_CLEAN_NOZZLE_STAGE_1 ; fine nozzle cleaning stage 1",
|
||||||
|
"M104 S130 ; lower nozzle temperature",
|
||||||
|
"G0 Z5 F10000 ; raise Z",
|
||||||
|
"MOVE_TO_DISCARD_FILAMENT_POSITION ; move to discard position again",
|
||||||
|
"ROUGHLY_CLEAN_NOZZLE ; repeat coarse cleaning",
|
||||||
|
"MOVE_TO_XY_IDLE_POSITION_EXTRUDER ; move to idle position",
|
||||||
|
"FINELY_CLEAN_NOZZLE_STAGE_2 ; fine nozzle cleaning stage 2",
|
||||||
|
";===== Precision homing =================",
|
||||||
|
"M106 S255 ; set primary fan to full speed",
|
||||||
|
"M109 S130 ; wait for nozzle at 130C",
|
||||||
|
"M190 S65 ; wait for bed to reach 65C",
|
||||||
|
"M107 P2 ; turn off auxiliary fan",
|
||||||
|
"G90 ; absolute positioning",
|
||||||
|
"G0 Z5 F10000 ; lift Z before homing",
|
||||||
|
"G28 Z ; precise Z homing",
|
||||||
|
";===== Heated bed leveling =================",
|
||||||
|
"BED_MESH_CALIBRATE PROBE_COUNT=11,11 ; probe bed mesh with 11x11 grid",
|
||||||
|
";===== Draw purge / start line =================",
|
||||||
|
"G90 ; absolute positioning",
|
||||||
|
"G1 Z1.5 ; move nozzle near bed",
|
||||||
|
"G0 X10 Y3 Z2 F18000 ; move to purge start position",
|
||||||
|
"M109 S220 ; heat nozzle to print temperature",
|
||||||
|
"G1 Z0.2 ; move to purge height",
|
||||||
|
"M83 ; relative extrusion mode",
|
||||||
|
"G1 X110 E15 F360 ; extrude purge line",
|
||||||
|
"G1 Z1.5 ; lift nozzle",
|
||||||
|
"M73 P20 R1 ; update progress to 20%",
|
||||||
|
"G90 ; absolute positioning",
|
||||||
|
"M106 S0 ; ensure primary fan off",
|
||||||
|
"G90 ; absolute positioning (redundant safety)",
|
||||||
|
"G21 ; set units to millimeters",
|
||||||
|
"M83 ; relative extrusion mode",
|
||||||
|
"; filament start gcode",
|
||||||
|
"SET_PRESSURE_ADVANCE ADVANCE=0.02 ; override pressure advance value",
|
||||||
|
"T{tool} ; select first tool",
|
||||||
|
"M106 S0 ; ensure primary fan off",
|
||||||
|
"M106 P2 S0 ; ensure auxiliary fan off",
|
||||||
|
";; <-----< HEADER <---------------------<"
|
||||||
|
],
|
||||||
|
"gcodePost": [
|
||||||
|
";; >-----> FOOTER >--------------------->",
|
||||||
|
"M106 S0 ; turn off primary cooling fan",
|
||||||
|
"M106 P2 S0 ; turn off auxiliary/part fan (fan index 2)",
|
||||||
|
"PRINT_END",
|
||||||
|
"TIMELAPSE_STOP",
|
||||||
|
"M73 P100 R0 ; report progress complete",
|
||||||
|
";; <-----< FOOTER <---------------------<"
|
||||||
|
],
|
||||||
|
"gcodeProc": "",
|
||||||
|
"gcodeFExt": "gcode",
|
||||||
|
"extruders": [
|
||||||
|
{
|
||||||
|
"extFilament": 1.75,
|
||||||
|
"extNozzle": 0.4,
|
||||||
|
"extSelect": [
|
||||||
|
"; BEGIN tool change from {last_tool} to {tool}",
|
||||||
|
"M620 S{tool}A ; prepare tool change",
|
||||||
|
"M204 S9000 ; Set acceleration to 9000 mm/s^2",
|
||||||
|
"M620.11 S0 ; Reset filament retraction state",
|
||||||
|
"M400 ; wait for moves to complete",
|
||||||
|
"M620.1 E F187 T240 ; set purge rate 187 temp 240",
|
||||||
|
"T{tool} ; initiate tool change",
|
||||||
|
"M620.1 E F374 T240 ; set purge rate 374 temp 240",
|
||||||
|
"M620.11 S0 ; Reset filament retraction state",
|
||||||
|
"G92 E0 ; reset extruder position",
|
||||||
|
"M400 ; wait for moves to complete",
|
||||||
|
" ; -- PURGE --",
|
||||||
|
"M109 S240 ; Set nozzle temperature to 240°C",
|
||||||
|
"G1 E23.7 F187 ; Extrude 23.7mm of filament at flowrate 187",
|
||||||
|
"G1 E0.914611 F50 ; Pulsatile extrusion",
|
||||||
|
"G1 E10.518 F187 ; Continue extrusion at flowrate 187",
|
||||||
|
"G1 E0.914611 F50 ; Pulsatile extrusion",
|
||||||
|
"G1 E10.518 F374 ; Continue extrusion at flowrate 374",
|
||||||
|
"G1 E0.914611 F50 ; Pulsatile extrusion",
|
||||||
|
"G1 E10.518 F374 ; Continue extrusion at flowrate 374",
|
||||||
|
"G1 E0.914611 F50 ; Pulsatile extrusion",
|
||||||
|
"G1 E10.518 F374 ; Continue extrusion at flowrate 374",
|
||||||
|
"G1 E-2 F1800 ; Retract filament by 2mm",
|
||||||
|
"G1 E2 F300 ; Recover retraction slowly",
|
||||||
|
"M400 ; Wait for all movements to complete",
|
||||||
|
"M109 S{temp} ; Set nozzle temperature back to print temp",
|
||||||
|
" ; -- SHAKE --",
|
||||||
|
"M106 P1 S255 ; Turn on nozzle cooling fan to full speed",
|
||||||
|
"M400 S3 ; Wait for all movements to complete",
|
||||||
|
"G1 X70 F5000 ; Move to X70",
|
||||||
|
"G1 X90 F3000 ; Move to X90",
|
||||||
|
"G1 Y255 F4000 ; Move to Y255",
|
||||||
|
"G1 X105 F5000 ; Move to X105",
|
||||||
|
"G1 Y265 ; Move to Y265",
|
||||||
|
"G1 X70 F10000 ; Rapid move to X70",
|
||||||
|
"G1 X100 F5000 ; Move to X100",
|
||||||
|
"G1 X70 F10000 ; Rapid move back to X70",
|
||||||
|
"G1 X100 F5000 ; Move to X100",
|
||||||
|
"G1 X70 F10000 ; Shake filament residue",
|
||||||
|
"G1 X80 F15000",
|
||||||
|
"G1 X60",
|
||||||
|
"G1 X80",
|
||||||
|
"G1 X60",
|
||||||
|
"G1 X80 ; Shake to put down garbage",
|
||||||
|
"G1 X100 F5000 ; Move to X100",
|
||||||
|
"G1 X165 F15000 ; Wipe and shake",
|
||||||
|
"G1 Y256 ; Move Y aside to prevent collision",
|
||||||
|
"M400 ; Wait for all movements to complete",
|
||||||
|
"M204 S10000 ; Set acceleration to 10000 mm/s^2",
|
||||||
|
"M621 S{tool}A ; complete tool change",
|
||||||
|
"; END tool change"
|
||||||
|
],
|
||||||
|
"extOffsetX": 0,
|
||||||
|
"extOffsetY": 0,
|
||||||
|
"extDeselect": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"extFilament": 1.75,
|
||||||
|
"extNozzle": 0.4,
|
||||||
|
"extSelect": [],
|
||||||
|
"extOffsetX": 0,
|
||||||
|
"extOffsetY": 0,
|
||||||
|
"extDeselect": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"extFilament": 1.75,
|
||||||
|
"extNozzle": 0.4,
|
||||||
|
"extSelect": [],
|
||||||
|
"extOffsetX": 0,
|
||||||
|
"extOffsetY": 0,
|
||||||
|
"extDeselect": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"extFilament": 1.75,
|
||||||
|
"extNozzle": 0.4,
|
||||||
|
"extSelect": [],
|
||||||
|
"extOffsetX": 0,
|
||||||
|
"extOffsetY": 0,
|
||||||
|
"extDeselect": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"new": false,
|
||||||
|
"deviceName": "My Snapmaker U1",
|
||||||
|
"bedBelt": false,
|
||||||
|
"fwRetract": false,
|
||||||
|
"filamentSource": "direct",
|
||||||
|
"extras": {
|
||||||
|
"bbl": {
|
||||||
|
"luke": {
|
||||||
|
"host": "10.20.20.75",
|
||||||
|
"code": "20709223",
|
||||||
|
"serial": "01P09C482401767",
|
||||||
|
"modified": true,
|
||||||
|
"selected": false,
|
||||||
|
"type": "C12"
|
||||||
|
},
|
||||||
|
"leia": {
|
||||||
|
"host": "10.20.20.76",
|
||||||
|
"code": "35129782",
|
||||||
|
"serial": "01P09C461603301",
|
||||||
|
"modified": true,
|
||||||
|
"selected": false,
|
||||||
|
"type": "C12"
|
||||||
|
},
|
||||||
|
"vader": {
|
||||||
|
"host": "10.20.20.77",
|
||||||
|
"code": "17711811",
|
||||||
|
"serial": "01P00A413100516",
|
||||||
|
"modified": true,
|
||||||
|
"selected": false,
|
||||||
|
"type": "C12"
|
||||||
|
},
|
||||||
|
"Scrappy": {
|
||||||
|
"host": "10.10.2.5",
|
||||||
|
"serial": "01S00A292500224",
|
||||||
|
"code": "38373335",
|
||||||
|
"modified": true,
|
||||||
|
"type": "C11",
|
||||||
|
"selected": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"imageURL": "",
|
||||||
|
"deviceZMax": 0,
|
||||||
|
"gcodeTime": 1,
|
||||||
|
"gcodeFeature": [],
|
||||||
|
"profiles": [
|
||||||
|
{
|
||||||
|
"processName": "U1 PLA",
|
||||||
|
"sliceHeight": 0.2,
|
||||||
|
"sliceShells": 3,
|
||||||
|
"sliceShellOrder": "out-in",
|
||||||
|
"sliceLayerStart": "last",
|
||||||
|
"sliceFillAngle": 45,
|
||||||
|
"sliceFillOverlap": 0.4,
|
||||||
|
"sliceFillSparse": 0.2,
|
||||||
|
"sliceFillType": "linear",
|
||||||
|
"sliceAdaptive": false,
|
||||||
|
"sliceMinHeight": 0,
|
||||||
|
"sliceSupportDensity": 0.2,
|
||||||
|
"sliceSupportOffset": 0.2,
|
||||||
|
"sliceSupportGap": 1,
|
||||||
|
"sliceSupportExtra": 0,
|
||||||
|
"sliceSupportAngle": 50,
|
||||||
|
"sliceSupportNozzle": 0,
|
||||||
|
"sliceSolidMinArea": 1,
|
||||||
|
"sliceBottomLayers": 2,
|
||||||
|
"sliceTopLayers": 3,
|
||||||
|
"firstLayerRate": 20,
|
||||||
|
"firstLayerYOffset": 0,
|
||||||
|
"firstLayerBrim": 0,
|
||||||
|
"firstLayerBeltLead": 0,
|
||||||
|
"firstLayerFanSpeed": 0,
|
||||||
|
"outputTemp": 220,
|
||||||
|
"outputBedTemp": 65,
|
||||||
|
"outputFanSpeed": 255,
|
||||||
|
"outputFeedrate": 100,
|
||||||
|
"outputFinishrate": 90,
|
||||||
|
"outputSeekrate": 200,
|
||||||
|
"outputShellMult": 1.2,
|
||||||
|
"outputFillMult": 1.18,
|
||||||
|
"outputSparseMult": 1.25,
|
||||||
|
"outputRetractDist": 1.3,
|
||||||
|
"outputRetractSpeed": 30,
|
||||||
|
"outputRetractWipe": 0,
|
||||||
|
"outputRetractDwell": 5,
|
||||||
|
"outputShortPoly": 0,
|
||||||
|
"outputMinSpeed": 1,
|
||||||
|
"outputCoastDist": 0,
|
||||||
|
"outputLayerRetract": false,
|
||||||
|
"zHopDistance": 0,
|
||||||
|
"antiBacklash": 0,
|
||||||
|
"sliceFillWidth": 1,
|
||||||
|
"firstSliceHeight": 0.2,
|
||||||
|
"firstLayerFillRate": 40,
|
||||||
|
"firstLayerNozzleTemp": 220,
|
||||||
|
"firstLayerBedTemp": 65,
|
||||||
|
"firstLayerBrimTrig": 0,
|
||||||
|
"firstLayerBrimGap": 0,
|
||||||
|
"outputRaft": false,
|
||||||
|
"outputRaftSpacing": 0.2,
|
||||||
|
"outputBrimCount": 0,
|
||||||
|
"outputBrimOffset": 1,
|
||||||
|
"outputPurgeTower": 0,
|
||||||
|
"outputInvertX": false,
|
||||||
|
"outputInvertY": false,
|
||||||
|
"ranges": [],
|
||||||
|
"sliceLineWidth": 0,
|
||||||
|
"sliceFillRepeat": 2,
|
||||||
|
"firstLayerBrimIn": 0,
|
||||||
|
"firstLayerBeltBump": 0,
|
||||||
|
"outputBeltFirst": false,
|
||||||
|
"outputLoops": 0,
|
||||||
|
"sliceFillGrow": 0.2,
|
||||||
|
"sliceSupportSpan": 5,
|
||||||
|
"sliceSupportOutline": true,
|
||||||
|
"firstLayerFlatten": 0,
|
||||||
|
"outputDraftShield": false,
|
||||||
|
"outputAvoidGaps": true,
|
||||||
|
"sliceDetectThin": "off",
|
||||||
|
"outputAlternating": false,
|
||||||
|
"sliceLayerStartX": 0,
|
||||||
|
"sliceLayerStartY": 0,
|
||||||
|
"outputFanLayer": 1,
|
||||||
|
"sliceAngle": 45,
|
||||||
|
"sliceZInterleave": false,
|
||||||
|
"outputScarfLength": 0,
|
||||||
|
"outputMinLayerTime": 9,
|
||||||
|
"sliceCompInner": 0,
|
||||||
|
"sliceCompOuter": 0,
|
||||||
|
"outputMaxFlowrate": 15,
|
||||||
|
"firstLayerBeltFact": 1,
|
||||||
|
"sliceSolidify": false,
|
||||||
|
"fdmArcEnabled": false,
|
||||||
|
"fdmArcResolution": 1,
|
||||||
|
"fdmArcTolerance": 0.005,
|
||||||
|
"sliceSupportType": "manual",
|
||||||
|
"sliceSupportTree": false,
|
||||||
|
"sliceSolidMinThick": 0.4
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gcodeChange": [
|
||||||
|
";; >-----> TOOL >----------------------->",
|
||||||
|
"; Change Tool{last_tool} -> Tool{tool} (layer {layer}) ; tool change",
|
||||||
|
"M104 S70 T{last_tool} ; cooldown previous tool",
|
||||||
|
"G91 ; set relative positioning",
|
||||||
|
"G1 Z1.5 ; lift nozzle 1.5 mm to avoid collision",
|
||||||
|
"G90 ; return to absolute positioning",
|
||||||
|
"G1 F21000 ; set high travel feedrate",
|
||||||
|
"M104 S{temp} T{tool} ; heat new tool and wait",
|
||||||
|
"M400 ; wait for all queued moves to finish",
|
||||||
|
"T{tool} ; select tool {tool}",
|
||||||
|
"SM_PRINT_PREEXTRUDE_FILAMENT INDEX={tool} ; Snapmaker macro: pre-extrude tool filament",
|
||||||
|
"G90 ; ensure absolute positioning",
|
||||||
|
"M106 P2 S178 ; set auxiliary/part fan to ~70%",
|
||||||
|
"M106 S255 ; set primary cooling fan to full speed",
|
||||||
|
"M109 S{temp} T{tool} ; set new tool nozzle temperature",
|
||||||
|
"SET_PRESSURE_ADVANCE ADVANCE=0.02 ; override pressure advance value for tool 1",
|
||||||
|
"G1 F30000 ; set printing feedrate",
|
||||||
|
";ADD WIPE TOWER HERE",
|
||||||
|
"M220 R ; reset feedrate override to default (100%) / restore",
|
||||||
|
"G1 F30000 ; set travel feedrate to 30000 mm/min",
|
||||||
|
"G4 S0 ; dwell 0s (no-op / sync point)",
|
||||||
|
"G92 E0 ; zero extruder position after purge (prevents huge E values)",
|
||||||
|
"G1 X{pos_x} Y{pos_y} ; restore XY position",
|
||||||
|
"G1 Z{pos_z} ; restore Z position",
|
||||||
|
";; <-----< TOOL <-----------------------<"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
// UI FRONT END ANIMATION CODE for 2D
|
// UI FRONT END ANIMATION CODE for 2D
|
||||||
|
|
||||||
import { api } from '../../core/api.js';
|
import { api } from '../../../app/api.js';
|
||||||
import { space } from '../../../moto/space.js';
|
import { space } from '../../../../moto/space.js';
|
||||||
|
|
||||||
const { client } = api;
|
const { client } = api;
|
||||||
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
// UI FRONT END ANIMATION CODE for 3D (indexed)
|
// UI FRONT END ANIMATION CODE for 3D (indexed)
|
||||||
|
|
||||||
import { api } from '../../core/api.js';
|
import { api } from '../../../app/api.js';
|
||||||
import { space } from '../../../moto/space.js';
|
import { space } from '../../../../moto/space.js';
|
||||||
|
|
||||||
const { client } = api;
|
const { client } = api;
|
||||||
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
import { api } from '../../core/api.js';
|
import { api } from '../../../app/api.js';
|
||||||
import { env, opAdd, opRender } from './client.js';
|
import { env, opAdd, opRender } from './init-ui.js';
|
||||||
import { CAM } from './driver-fe.js';
|
import { CAM } from './dispatch.js';
|
||||||
import { clearTabs, restoreTabs } from './cl-tab.js';
|
import { clearTabs, restoreTabs } from './cl-tab.js';
|
||||||
import { updateStock } from './cl-stock.js';
|
import { updateStock } from './cl-stock.js';
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { api } from "../../core/api.js";
|
import { api } from '../../../app/api.js';
|
||||||
import { CAM } from "./driver-fe.js";
|
import { CAM } from "./dispatch.js";
|
||||||
import { clearPops, env } from "./client.js";
|
import { clearPops, env } from "./init-ui.js";
|
||||||
|
|
||||||
let alert, lastWidget;
|
let alert, lastWidget;
|
||||||
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
import { api } from '../../core/api.js';
|
import { api } from '../../../app/api.js';
|
||||||
import { env, clearPops } from './client.js';
|
import { env, clearPops } from './init-ui.js';
|
||||||
import { CAM } from './driver-fe.js';
|
import { CAM } from './dispatch.js';
|
||||||
|
|
||||||
export let holeSelOn = false;
|
export let holeSelOn = false;
|
||||||
export let lastSelHoles;
|
export let lastSelHoles;
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
import { $, h } from '../../../moto/webui.js';
|
import { $, h } from '../../../../moto/webui.js';
|
||||||
import { api } from '../../core/api.js';
|
import { api } from '../../../app/api.js';
|
||||||
import { env, opRender } from './client.js';
|
import { env, opRender } from './init-ui.js';
|
||||||
import { Tool } from './tool.js';
|
import { Tool } from '../core/tool.js';
|
||||||
import { opFlip } from './cl-flip.js';
|
import { opFlip } from './cl-flip.js';
|
||||||
import { selectHoles } from './cl-hole.js';
|
import { selectHoles } from './cl-hole.js';
|
||||||
import { selectHelical } from './cl-helical.js';
|
import { selectHelical } from './cl-helical.js';
|
||||||
import { surfaceAdd } from './cl-surface.js';
|
import { surfaceAdd } from './cl-surface.js';
|
||||||
import { traceAdd } from './cl-trace.js';
|
import { traceAdd, traceClear, traceLoad } from './cl-trace.js';
|
||||||
|
import { startFaceUp } from '../../../app/face-tool.js';
|
||||||
|
import { showZBottom, showZTop, updateStock } from './cl-stock.js';
|
||||||
|
|
||||||
const { MODES } = api.const;
|
const { MODES } = api.const;
|
||||||
const { uc: UC } = api;
|
const { uc: UC } = api;
|
||||||
|
|
@ -27,6 +29,10 @@ function hasSpindle() {
|
||||||
return env.current.device.spindleMax > 0;
|
return env.current.device.spindleMax > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isWebGPU() {
|
||||||
|
return api.conf.get().controller.webGPU;
|
||||||
|
}
|
||||||
|
|
||||||
function zTop() {
|
function zTop() {
|
||||||
return api.conf.get().process.camZTop > 0;
|
return api.conf.get().process.camZTop > 0;
|
||||||
}
|
}
|
||||||
|
|
@ -131,10 +137,21 @@ export function createPopOp(type, map) {
|
||||||
return rec;
|
return rec;
|
||||||
},
|
},
|
||||||
hideshow: () => {
|
hideshow: () => {
|
||||||
|
let conf = api.conf.get();
|
||||||
for (let inp of Object.values(op.inputs)) {
|
for (let inp of Object.values(op.inputs)) {
|
||||||
let parent = inp.parentElement;
|
let parent = inp.parentElement;
|
||||||
if (parent && parent.setVisible && parent.__opt.show) {
|
if (parent && parent.setVisible && parent.__opt.show) {
|
||||||
parent.setVisible(parent.__opt.show(op, api.conf.get()));
|
// for normal inputs
|
||||||
|
parent.setVisible(parent.__opt.show(op, conf));
|
||||||
|
} else if (inp.__opt?.show) {
|
||||||
|
// for blanks
|
||||||
|
inp.setVisible(inp.__opt.show(op, conf));
|
||||||
|
} else if (inp.children) {
|
||||||
|
for (let c of inp.children) {
|
||||||
|
if (c.__opt?.show) {
|
||||||
|
c.setVisible(c.__opt.show(op, conf));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -194,6 +211,82 @@ export function createPopOp(type, map) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createPopOps() {
|
export function createPopOps() {
|
||||||
|
|
||||||
|
const bottom = true;
|
||||||
|
const open = true;
|
||||||
|
const top = true;
|
||||||
|
|
||||||
|
function selector(type, { input, event, opt }) {
|
||||||
|
let which = opt?.top ? 'top' : opt?.bottom ? 'bottom' : 'unknown';
|
||||||
|
let value = parseFloat(input.value);
|
||||||
|
switch (type) {
|
||||||
|
case 'click':
|
||||||
|
let rec = env.poppedRec;
|
||||||
|
let { field } = opt;
|
||||||
|
api.event.emit('cam.zplane.select', { which, onselect(value) {
|
||||||
|
rec[field] = value;
|
||||||
|
input.focus();
|
||||||
|
input.value = value;
|
||||||
|
input.blur();
|
||||||
|
} } );
|
||||||
|
break;
|
||||||
|
case 'enter':
|
||||||
|
api.event.emit('cam.zplane.show', { which, value });
|
||||||
|
break;
|
||||||
|
case 'leave':
|
||||||
|
api.event.emit('cam.zplane.hide', { which });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isClear() {
|
||||||
|
return env.poppedRec.mode === 'clear';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTrace() {
|
||||||
|
return env.poppedRec.mode === 'trace';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isShadow() {
|
||||||
|
return env.poppedRec.shadow;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSurface() {
|
||||||
|
return env.poppedRec.mode === 'surface';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSurfaceLinear() {
|
||||||
|
return env.poppedRec.mode === 'surface' && env.poppedRec.sr_type === 'linear';
|
||||||
|
}
|
||||||
|
|
||||||
|
function canDogBones() {
|
||||||
|
if (!env.poppedRec) return false;
|
||||||
|
return env.poppedRec.mode === 'follow';
|
||||||
|
}
|
||||||
|
|
||||||
|
function canDogBonesRev() {
|
||||||
|
return canDogBones() && env.poppedRec.dogbone;
|
||||||
|
}
|
||||||
|
|
||||||
|
function zDogSep() {
|
||||||
|
return canDogBones() || zBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
const ov_topz = { title: LANG.ou_ztop_l, convert: toFloat, units, selector, top, field: "ov_topz" };
|
||||||
|
|
||||||
|
const ov_botz = { title: LANG.ou_zbot_l, convert: toFloat, units, selector, bottom, field: "ov_botz" };
|
||||||
|
|
||||||
|
const ov_hover = {
|
||||||
|
hover(ie) {
|
||||||
|
if (ie === 'enter') {
|
||||||
|
showZTop(env.poppedRec.ov_topz);
|
||||||
|
showZBottom(env.poppedRec.ov_botz);
|
||||||
|
} else {
|
||||||
|
updateStock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
createPopOp('level', {
|
createPopOp('level', {
|
||||||
tool: 'camLevelTool',
|
tool: 'camLevelTool',
|
||||||
spindle: 'camLevelSpindle',
|
spindle: 'camLevelSpindle',
|
||||||
|
|
@ -236,10 +329,6 @@ export function createPopOps() {
|
||||||
tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
|
tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
|
||||||
direction: UC.newSelect(LANG.ou_dire_s, { title: LANG.ou_dire_l }, "direction"),
|
direction: UC.newSelect(LANG.ou_dire_s, { title: LANG.ou_dire_l }, "direction"),
|
||||||
sep: UC.newBlank({ class: "pop-sep" }),
|
sep: UC.newBlank({ class: "pop-sep" }),
|
||||||
spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: toInt, show: hasSpindle }),
|
|
||||||
rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: toInt, units }),
|
|
||||||
plunge: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: toInt, units }),
|
|
||||||
sep: UC.newBlank({ class: "pop-sep" }),
|
|
||||||
step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: toFloat, bound: UC.bound(0.01, 1.0) }),
|
step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: toFloat, bound: UC.bound(0.01, 1.0) }),
|
||||||
down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: toFloat, units }),
|
down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: toFloat, units }),
|
||||||
leave: UC.newInput(LANG.cr_lsto_s, { title: LANG.cr_lsto_l, convert: toFloat, units }),
|
leave: UC.newInput(LANG.cr_lsto_s, { title: LANG.cr_lsto_l, convert: toFloat, units }),
|
||||||
|
|
@ -250,9 +339,14 @@ export function createPopOps() {
|
||||||
inside: UC.newBoolean(LANG.cr_olin_s, undefined, { title: LANG.cr_olin_l, show: () => !env.poppedRec.all || env.poppedRec.inside }),
|
inside: UC.newBoolean(LANG.cr_olin_s, undefined, { title: LANG.cr_olin_l, show: () => !env.poppedRec.all || env.poppedRec.inside }),
|
||||||
omitthru: UC.newBoolean(LANG.co_omit_s, undefined, { title: LANG.co_omit_l }),
|
omitthru: UC.newBoolean(LANG.co_omit_s, undefined, { title: LANG.co_omit_l }),
|
||||||
sep: UC.newBlank({ class: "pop-sep" }),
|
sep: UC.newBlank({ class: "pop-sep" }),
|
||||||
exp: UC.newExpand("overrides"),
|
exp: UC.newExpand("feeds & speeds", { }),
|
||||||
ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: toFloat, units }),
|
spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: toInt, show: hasSpindle }),
|
||||||
ov_botz: UC.newInput(LANG.ou_zbot_s, { title: LANG.ou_zbot_l, convert: toFloat, units }),
|
rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: toInt, units }),
|
||||||
|
plunge: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: toInt, units }),
|
||||||
|
exp_end: UC.endExpand(),
|
||||||
|
exp: UC.newExpand("top & bottom", ov_hover),
|
||||||
|
ov_topz: UC.newInput(LANG.ou_ztop_s, ov_topz),
|
||||||
|
ov_botz: UC.newInput(LANG.ou_zbot_s, ov_botz),
|
||||||
exp_end: UC.endExpand(),
|
exp_end: UC.endExpand(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -278,9 +372,6 @@ export function createPopOps() {
|
||||||
tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
|
tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
|
||||||
direction: UC.newSelect(LANG.ou_dire_s, { title: LANG.ou_dire_l }, "direction"),
|
direction: UC.newSelect(LANG.ou_dire_s, { title: LANG.ou_dire_l }, "direction"),
|
||||||
sep: UC.newBlank({ class: "pop-sep" }),
|
sep: UC.newBlank({ class: "pop-sep" }),
|
||||||
spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: toInt, show: hasSpindle }),
|
|
||||||
rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: toInt, units }),
|
|
||||||
plunge: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: toInt, units }),
|
|
||||||
down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: toFloat, units }),
|
down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: toFloat, units }),
|
||||||
step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: toFloat, bound: UC.bound(0.01, 1.0), show: () => env.popOp.outline.rec.wide }),
|
step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: toFloat, bound: UC.bound(0.01, 1.0), show: () => env.popOp.outline.rec.wide }),
|
||||||
steps: UC.newInput(LANG.cc_sovc_s, { title: LANG.cc_sovc_l, convert: toInt, bound: UC.bound(1, 500), show: () => env.popOp.outline.rec.wide }),
|
steps: UC.newInput(LANG.cc_sovc_s, { title: LANG.cc_sovc_l, convert: toInt, bound: UC.bound(1, 500), show: () => env.popOp.outline.rec.wide }),
|
||||||
|
|
@ -294,9 +385,14 @@ export function createPopOps() {
|
||||||
dogbones: UC.newBoolean(LANG.co_dogb_s, undefined, { title: LANG.co_dogb_l, show: (op) => { return !op.inputs.wide.checked } }),
|
dogbones: UC.newBoolean(LANG.co_dogb_s, undefined, { title: LANG.co_dogb_l, show: (op) => { return !op.inputs.wide.checked } }),
|
||||||
revbones: UC.newBoolean(LANG.co_dogr_s, undefined, { title: LANG.co_dogr_l, show: () => env.poppedRec.dogbones }),
|
revbones: UC.newBoolean(LANG.co_dogr_s, undefined, { title: LANG.co_dogr_l, show: () => env.poppedRec.dogbones }),
|
||||||
sep: UC.newBlank({ class: "pop-sep" }),
|
sep: UC.newBlank({ class: "pop-sep" }),
|
||||||
exp: UC.newExpand("overrides"),
|
exp: UC.newExpand("feeds & speeds", { }),
|
||||||
ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: toFloat, units }),
|
spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: toInt, show: hasSpindle }),
|
||||||
ov_botz: UC.newInput(LANG.ou_zbot_s, { title: LANG.ou_zbot_l, convert: toFloat, units }),
|
rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: toInt, units }),
|
||||||
|
plunge: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: toInt, units }),
|
||||||
|
exp_end: UC.endExpand(),
|
||||||
|
exp: UC.newExpand("top & bottom", ov_hover),
|
||||||
|
ov_topz: UC.newInput(LANG.ou_ztop_s, ov_topz),
|
||||||
|
ov_botz: UC.newInput(LANG.ou_zbot_s, ov_botz),
|
||||||
exp_end: UC.endExpand(),
|
exp_end: UC.endExpand(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -316,6 +412,7 @@ export function createPopOps() {
|
||||||
bottom: 'camContourBottom',
|
bottom: 'camContourBottom',
|
||||||
curves: 'camContourCurves',
|
curves: 'camContourCurves',
|
||||||
inside: 'camContourIn',
|
inside: 'camContourIn',
|
||||||
|
clipto: 'camStockClipTo',
|
||||||
filter: 'camContourFilter',
|
filter: 'camContourFilter',
|
||||||
axis: 'X'
|
axis: 'X'
|
||||||
}).inputs = {
|
}).inputs = {
|
||||||
|
|
@ -337,14 +434,17 @@ export function createPopOps() {
|
||||||
curves: UC.newBoolean(LANG.cf_curv_s, undefined, { title: LANG.cf_curv_l }),
|
curves: UC.newBoolean(LANG.cf_curv_s, undefined, { title: LANG.cf_curv_l }),
|
||||||
inside: UC.newBoolean(LANG.cf_olin_s, undefined, { title: LANG.cf_olin_l }),
|
inside: UC.newBoolean(LANG.cf_olin_s, undefined, { title: LANG.cf_olin_l }),
|
||||||
bottom: UC.newBoolean(LANG.cf_botm_s, undefined, { title: LANG.cf_botm_l, show: (op, conf) => conf ? conf.process.camZBottom : 0 }),
|
bottom: UC.newBoolean(LANG.cf_botm_s, undefined, { title: LANG.cf_botm_l, show: (op, conf) => conf ? conf.process.camZBottom : 0 }),
|
||||||
|
clipto: UC.newBoolean(LANG.cf_clip_s, undefined, { title:LANG.cf_clip_l, show: () => !isWebGPU() }),
|
||||||
filter: UC.newRow([UC.newButton(LANG.filter, contourFilter)], { class: "ext-buttons f-row" })
|
filter: UC.newRow([UC.newButton(LANG.filter, contourFilter)], { class: "ext-buttons f-row" })
|
||||||
};
|
};
|
||||||
|
|
||||||
createPopOp('lathe', {
|
createPopOp('lathe', {
|
||||||
tool: 'camLatheTool',
|
tool: 'camLatheTool',
|
||||||
spindle: 'camLatheSpindle',
|
spindle: 'camLatheSpindle',
|
||||||
step: 'camLatheOver',
|
|
||||||
angle: 'camLatheAngle',
|
angle: 'camLatheAngle',
|
||||||
|
step: 'camLatheOver',
|
||||||
|
down: 'camLatheDown',
|
||||||
|
expand: 'camLatheExpand',
|
||||||
rate: 'camLatheSpeed',
|
rate: 'camLatheSpeed',
|
||||||
tolerance: 'camTolerance',
|
tolerance: 'camTolerance',
|
||||||
filter: 'camContourFilter',
|
filter: 'camContourFilter',
|
||||||
|
|
@ -358,8 +458,11 @@ export function createPopOps() {
|
||||||
spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: toInt, show: hasSpindle }),
|
spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: toInt, show: hasSpindle }),
|
||||||
rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: toInt, units }),
|
rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: toInt, units }),
|
||||||
sep: UC.newBlank({ class: "pop-sep" }),
|
sep: UC.newBlank({ class: "pop-sep" }),
|
||||||
step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: toFloat, bound: UC.bound(0.01, 100.0) }),
|
|
||||||
angle: UC.newInput(LANG.cc_sang_s, { title: LANG.cc_sang_l, convert: toFloat, bound: UC.bound(0.01, 180.0) }),
|
angle: UC.newInput(LANG.cc_sang_s, { title: LANG.cc_sang_l, convert: toFloat, bound: UC.bound(0.01, 180.0) }),
|
||||||
|
step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: toFloat, bound: UC.bound(0.01, 100.0) }),
|
||||||
|
sepDn: UC.newBlank({ class: "pop-sep", show: () => env.poppedRec.down > 0 }),
|
||||||
|
down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: toFloat, units, show: isWebGPU }),
|
||||||
|
expand: UC.newInput(LANG.cl_xpnd_s, { title: LANG.cl_xpnd_l, convert: toFloat, units, show: () => env.poppedRec.down > 0 }),
|
||||||
sep: UC.newBlank({ class: "pop-sep" }),
|
sep: UC.newBlank({ class: "pop-sep" }),
|
||||||
offStart: UC.newInput(LANG.ci_laso_s, { title: LANG.ci_laso_l, convert: toFloat}),
|
offStart: UC.newInput(LANG.ci_laso_s, { title: LANG.ci_laso_l, convert: toFloat}),
|
||||||
offEnd: UC.newInput(LANG.ci_laeo_s, { title: LANG.ci_laeo_l, convert: toFloat}),
|
offEnd: UC.newInput(LANG.ci_laeo_s, { title: LANG.ci_laeo_l, convert: toFloat}),
|
||||||
|
|
@ -370,19 +473,6 @@ export function createPopOps() {
|
||||||
linear: UC.newBoolean(LANG.ci_line_s, undefined, { title: LANG.ci_line_l }),
|
linear: UC.newBoolean(LANG.ci_line_s, undefined, { title: LANG.ci_line_l }),
|
||||||
};
|
};
|
||||||
|
|
||||||
function canDogBones() {
|
|
||||||
if (!env.poppedRec) return false;
|
|
||||||
return env.poppedRec.mode === 'follow';
|
|
||||||
}
|
|
||||||
|
|
||||||
function canDogBonesRev() {
|
|
||||||
return canDogBones() && env.poppedRec.dogbone;
|
|
||||||
}
|
|
||||||
|
|
||||||
function zDogSep() {
|
|
||||||
return canDogBones() || zBottom();
|
|
||||||
}
|
|
||||||
|
|
||||||
createPopOp('trace', {
|
createPopOp('trace', {
|
||||||
mode: 'camTraceType',
|
mode: 'camTraceType',
|
||||||
offset: 'camTraceOffset',
|
offset: 'camTraceOffset',
|
||||||
|
|
@ -407,10 +497,6 @@ export function createPopOps() {
|
||||||
offset: UC.newSelect(LANG.cc_offs_s, { title: LANG.cc_offs_l, show: () => (env.poppedRec.mode === 'follow') }, "traceoff"),
|
offset: UC.newSelect(LANG.cc_offs_s, { title: LANG.cc_offs_l, show: () => (env.poppedRec.mode === 'follow') }, "traceoff"),
|
||||||
direction: UC.newSelect(LANG.ou_dire_s, { title: LANG.ou_dire_l }, "direction"),
|
direction: UC.newSelect(LANG.ou_dire_s, { title: LANG.ou_dire_l }, "direction"),
|
||||||
sep: UC.newBlank({ class: "pop-sep" }),
|
sep: UC.newBlank({ class: "pop-sep" }),
|
||||||
spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: toInt, show: hasSpindle }),
|
|
||||||
rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: toInt, units }),
|
|
||||||
plunge: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: toInt, units }),
|
|
||||||
sep: UC.newBlank({ class: "pop-sep" }),
|
|
||||||
step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: toFloat, bound: UC.bound(0.01, 1.0), show: (op) => env.popOp.trace.rec.mode === "clear" }),
|
step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: toFloat, bound: UC.bound(0.01, 1.0), show: (op) => env.popOp.trace.rec.mode === "clear" }),
|
||||||
down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: toFloat, units }),
|
down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: toFloat, units }),
|
||||||
offover: UC.newInput(LANG.cc_offd_s, { title: LANG.cc_offd_l, convert: toFloat, units, show: () => env.poppedRec.offset !== "none" || env.poppedRec.mode === "clear" }),
|
offover: UC.newInput(LANG.cc_offd_s, { title: LANG.cc_offd_l, convert: toFloat, units, show: () => env.poppedRec.offset !== "none" || env.poppedRec.mode === "clear" }),
|
||||||
|
|
@ -419,13 +505,22 @@ export function createPopOps() {
|
||||||
merge: UC.newBoolean(LANG.co_merg_s, undefined, { title: LANG.co_merg_l, show: () => !env.popOp.trace.rec.down }),
|
merge: UC.newBoolean(LANG.co_merg_s, undefined, { title: LANG.co_merg_l, show: () => !env.popOp.trace.rec.down }),
|
||||||
dogbone: UC.newBoolean(LANG.co_dogb_s, undefined, { title: LANG.co_dogb_l, show: canDogBones }),
|
dogbone: UC.newBoolean(LANG.co_dogb_s, undefined, { title: LANG.co_dogb_l, show: canDogBones }),
|
||||||
revbone: UC.newBoolean(LANG.co_dogr_s, undefined, { title: LANG.co_dogr_l, show: canDogBonesRev }),
|
revbone: UC.newBoolean(LANG.co_dogr_s, undefined, { title: LANG.co_dogr_l, show: canDogBonesRev }),
|
||||||
exp: UC.newExpand("overrides"),
|
exp: UC.newExpand("feeds & speeds", { }),
|
||||||
|
spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: toInt, show: hasSpindle }),
|
||||||
|
rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: toInt, units }),
|
||||||
|
plunge: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: toInt, units }),
|
||||||
|
exp_end: UC.endExpand(),
|
||||||
|
exp: UC.newExpand("top & bottom", ov_hover),
|
||||||
sep: UC.newBlank({ class: "pop-sep" }),
|
sep: UC.newBlank({ class: "pop-sep" }),
|
||||||
ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: toFloat, units }),
|
ov_topz: UC.newInput(LANG.ou_ztop_s, ov_topz),
|
||||||
ov_botz: UC.newInput(LANG.ou_zbot_s, { title: LANG.ou_zbot_l, convert: toFloat, units }),
|
ov_botz: UC.newInput(LANG.ou_zbot_s, ov_botz),
|
||||||
exp_end: UC.endExpand(),
|
exp_end: UC.endExpand(),
|
||||||
sep: UC.newBlank({ class: "pop-sep" }),
|
sep: UC.newBlank({ class: "pop-sep" }),
|
||||||
menu: UC.newRow([UC.newButton("select", traceAdd)], { class: "ext-buttons f-row" }),
|
menu: UC.newRow([
|
||||||
|
UC.newButton("select", traceAdd),
|
||||||
|
// UC.newButton("load", traceLoad, { show: () => env.poppedRec.mode === "follow" }),
|
||||||
|
// UC.newButton("", traceClear, { icon: '<i class="fa fa-trash"></i>' }),
|
||||||
|
], { class: "ext-buttons f-row", row: true }),
|
||||||
};
|
};
|
||||||
|
|
||||||
createPopOp('pocket', {
|
createPopOp('pocket', {
|
||||||
|
|
@ -450,10 +545,6 @@ export function createPopOps() {
|
||||||
tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
|
tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
|
||||||
direction: UC.newSelect(LANG.ou_dire_s, { title: LANG.ou_dire_l }, "direction"),
|
direction: UC.newSelect(LANG.ou_dire_s, { title: LANG.ou_dire_l }, "direction"),
|
||||||
sep: UC.newBlank({ class: "pop-sep" }),
|
sep: UC.newBlank({ class: "pop-sep" }),
|
||||||
spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: toInt, show: hasSpindle }),
|
|
||||||
rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: toInt, units }),
|
|
||||||
plunge: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: toInt, units }),
|
|
||||||
sep: UC.newBlank({ class: "pop-sep" }),
|
|
||||||
step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: toFloat, bound: UC.bound(0.01, 1.0) }),
|
step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: toFloat, bound: UC.bound(0.01, 1.0) }),
|
||||||
down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: toFloat, units, show: () => !env.poppedRec.contour }),
|
down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: toFloat, units, show: () => !env.poppedRec.contour }),
|
||||||
sep: UC.newBlank({ class: "pop-sep" }),
|
sep: UC.newBlank({ class: "pop-sep" }),
|
||||||
|
|
@ -465,10 +556,15 @@ export function createPopOps() {
|
||||||
sep: UC.newBlank({ class: "pop-sep" }),
|
sep: UC.newBlank({ class: "pop-sep" }),
|
||||||
contour: UC.newBoolean(LANG.cp_cont_s, undefined, { title: LANG.cp_cont_s }),
|
contour: UC.newBoolean(LANG.cp_cont_s, undefined, { title: LANG.cp_cont_s }),
|
||||||
outline: UC.newBoolean(LANG.cp_outl_s, undefined, { title: LANG.cp_outl_l }),
|
outline: UC.newBoolean(LANG.cp_outl_s, undefined, { title: LANG.cp_outl_l }),
|
||||||
exp: UC.newExpand("overrides"),
|
exp: UC.newExpand("feeds & speeds", { }),
|
||||||
|
spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: toInt, show: hasSpindle }),
|
||||||
|
rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: toInt, units }),
|
||||||
|
plunge: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: toInt, units }),
|
||||||
|
exp_end: UC.endExpand(),
|
||||||
|
exp: UC.newExpand("top & bottom", ov_hover),
|
||||||
sep: UC.newBlank({ class: "pop-sep" }),
|
sep: UC.newBlank({ class: "pop-sep" }),
|
||||||
ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: toFloat, units }),
|
ov_topz: UC.newInput(LANG.ou_ztop_s, ov_topz),
|
||||||
ov_botz: UC.newInput(LANG.ou_zbot_s, { title: LANG.ou_zbot_l, convert: toFloat, units }),
|
ov_botz: UC.newInput(LANG.ou_zbot_s, ov_botz),
|
||||||
exp_end: UC.endExpand(),
|
exp_end: UC.endExpand(),
|
||||||
sep: UC.newBlank({ class: "pop-sep" }),
|
sep: UC.newBlank({ class: "pop-sep" }),
|
||||||
menu: UC.newRow([UC.newButton("select", surfaceAdd)], { class: "ext-buttons f-row" }),
|
menu: UC.newRow([UC.newButton("select", surfaceAdd)], { class: "ext-buttons f-row" }),
|
||||||
|
|
@ -592,17 +688,13 @@ export function createPopOps() {
|
||||||
action: UC.newRow([UC.newButton(LANG.edit, gcodeEditor())], { class: "ext-buttons f-row" })
|
action: UC.newRow([UC.newButton(LANG.edit, gcodeEditor())], { class: "ext-buttons f-row" })
|
||||||
};
|
};
|
||||||
|
|
||||||
function angleTowardZUp() {
|
|
||||||
api.event.emit('tool.mesh.face-up');
|
|
||||||
}
|
|
||||||
|
|
||||||
createPopOp('index', {
|
createPopOp('index', {
|
||||||
degrees: 'camIndexAxis',
|
degrees: 'camIndexAxis',
|
||||||
absolute: 'camIndexAbs'
|
absolute: 'camIndexAbs'
|
||||||
}).inputs = {
|
}).inputs = {
|
||||||
degrees: UC.newInput(LANG.ci_degr_s, { title: LANG.ci_degr_l, convert: toFloat, bound: UC.bound(-360, 360.0) }),
|
degrees: UC.newInput(LANG.ci_degr_s, { title: LANG.ci_degr_l, convert: toFloat, bound: UC.bound(-360, 360.0) }),
|
||||||
absolute: UC.newBoolean(LANG.ci_abso_s, undefined, { title: LANG.ci_abso_l }),
|
absolute: UC.newBoolean(LANG.ci_abso_s, undefined, { title: LANG.ci_abso_l }),
|
||||||
select: UC.newRow([UC.newButton(LANG.ci_face_s, angleTowardZUp)], { class: "ext-buttons f-row" })
|
select: UC.newRow([UC.newButton(LANG.ci_face_s, startFaceUp)], { class: "ext-buttons f-row" })
|
||||||
};
|
};
|
||||||
|
|
||||||
const editEnable = gcodeEditor('Laser Enable Script', 'enable');
|
const editEnable = gcodeEditor('Laser Enable Script', 'enable');
|
||||||
|
|
@ -649,23 +741,13 @@ export function createPopOps() {
|
||||||
|
|
||||||
createPopOp('|', {}).inputs = {};
|
createPopOp('|', {}).inputs = {};
|
||||||
|
|
||||||
function isClear() {
|
createPopOp('loop', {
|
||||||
return env.poppedRec.mode === 'clear';
|
repeat: 2,
|
||||||
}
|
count: 1
|
||||||
|
}).inputs = {
|
||||||
function isTrace() {
|
repeat: UC.newInput('repeat', { title: "repeat count", convert: toInt, bound: UC.bound(1, 100), post: opRender }),
|
||||||
return env.poppedRec.mode === 'trace';
|
count: UC.newInput('operations', { title: "num operations", convert: toInt, bound: UC.bound(1, 20), post: opRender })
|
||||||
}
|
};
|
||||||
|
|
||||||
function isSurface() {
|
|
||||||
return env.poppedRec.mode === 'surface';
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSurfaceLinear() {
|
|
||||||
return env.poppedRec.mode === 'surface' && env.poppedRec.sr_type === 'linear';
|
|
||||||
}
|
|
||||||
|
|
||||||
const open = true;
|
|
||||||
|
|
||||||
createPopOp('area', {
|
createPopOp('area', {
|
||||||
spindle: 'camAreaSpindle',
|
spindle: 'camAreaSpindle',
|
||||||
|
|
@ -675,15 +757,18 @@ export function createPopOps() {
|
||||||
tr_type: 'camAreaTrace',
|
tr_type: 'camAreaTrace',
|
||||||
sr_type: 'camAreaSurface',
|
sr_type: 'camAreaSurface',
|
||||||
sr_angle: 'camAreaAngle',
|
sr_angle: 'camAreaAngle',
|
||||||
|
sr_alter: 'camAreaZigZag',
|
||||||
over: 'camAreaOver',
|
over: 'camAreaOver',
|
||||||
down: 'camAreaDown',
|
down: 'camAreaDown',
|
||||||
rate: 'camAreaSpeed',
|
rate: 'camAreaSpeed',
|
||||||
plunge: 'camAreaPlunge',
|
plunge: 'camAreaPlunge',
|
||||||
expand: 'camAreaExpand',
|
expand: 'camAreaExpand',
|
||||||
|
walls: 'camAreaWalls',
|
||||||
smooth: 'camAreaSmooth',
|
smooth: 'camAreaSmooth',
|
||||||
follow: 'camAreaFollow',
|
follow: 'camAreaFollow',
|
||||||
refine: 'camAreaRefine',
|
refine: 'camAreaRefine',
|
||||||
outline: 'camAreaOutline',
|
outline: 'camAreaOutline',
|
||||||
|
shadow: 'camAreaShadow',
|
||||||
tolerance: 'camTolerance',
|
tolerance: 'camTolerance',
|
||||||
dogbones: 'camAreaDogbones',
|
dogbones: 'camAreaDogbones',
|
||||||
revbones: 'camAreaRevbones',
|
revbones: 'camAreaRevbones',
|
||||||
|
|
@ -694,35 +779,43 @@ export function createPopOps() {
|
||||||
tr_type: UC.newSelect(LANG.cc_offs_s, { title: LANG.cc_offs_l, show: isTrace }, "traceoff"),
|
tr_type: UC.newSelect(LANG.cc_offs_s, { title: LANG.cc_offs_l, show: isTrace }, "traceoff"),
|
||||||
sr_type: UC.newSelect("pattern", { title: "pattern", show: isSurface }, "surftyp"),
|
sr_type: UC.newSelect("pattern", { title: "pattern", show: isSurface }, "surftyp"),
|
||||||
sep: UC.newBlank({ class: "pop-sep" }),
|
sep: UC.newBlank({ class: "pop-sep" }),
|
||||||
|
exp: UC.newExpand("area selection", { open }),
|
||||||
menu: UC.newRow([
|
menu: UC.newRow([
|
||||||
UC.newButton("edge", traceAdd),
|
UC.newButton("edge", traceAdd),
|
||||||
UC.newButton("surface", surfaceAdd),
|
UC.newButton("surface", surfaceAdd),
|
||||||
], { class: "ext-buttons f-row" }),
|
], { class: "ext-buttons f-row", show: () => !isShadow() }),
|
||||||
|
shadow: UC.newBoolean(LANG.cp_shad_s, undefined, { title: LANG.cp_shad_l }),
|
||||||
outline: UC.newBoolean(LANG.cp_outl_s, undefined, { title: LANG.cp_outl_l }),
|
outline: UC.newBoolean(LANG.cp_outl_s, undefined, { title: LANG.cp_outl_l }),
|
||||||
|
exp_end: UC.endExpand(),
|
||||||
|
sep: UC.newBlank({ class: "pop-sep" }),
|
||||||
|
exp: UC.newExpand("area modifiers", { }),
|
||||||
|
walls: UC.newBoolean(LANG.ca_wall_s, undefined, { title: LANG.ca_wall_l }),
|
||||||
expand: UC.newInput(LANG.cp_xpnd_s, { title: LANG.cp_xpnd_l, convert: toFloat, units }),
|
expand: UC.newInput(LANG.cp_xpnd_s, { title: LANG.cp_xpnd_l, convert: toFloat, units }),
|
||||||
smooth: UC.newInput(LANG.cp_smoo_s, { title: LANG.cp_smoo_l, convert: toInt, xshow: isSurface }),
|
smooth: UC.newInput(LANG.cp_smoo_s, { title: LANG.cp_smoo_l, convert: toInt, xshow: isSurface }),
|
||||||
follow: UC.newInput(LANG.cp_foll_s, { title: LANG.cp_foll_l, convert: toFloat }),
|
follow: UC.newInput(LANG.cp_foll_s, { title: LANG.cp_foll_l, convert: toFloat }),
|
||||||
tolerance: UC.newInput(LANG.ou_toll_s, { title: LANG.ou_toll_l, convert: toFloat, bound: UC.bound(0, 10.0), units, round: 4, show: isSurface }),
|
tolerance: UC.newInput(LANG.ou_toll_s, { title: LANG.ou_toll_l, convert: toFloat, bound: UC.bound(0, 10.0), units, round: 4, show: isSurface }),
|
||||||
|
exp_end: UC.endExpand(),
|
||||||
exp: UC.newExpand("tool & stepping", { open }),
|
exp: UC.newExpand("tool & stepping", { open }),
|
||||||
tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
|
tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
|
||||||
direction: UC.newSelect(LANG.ou_dire_s, { title: LANG.ou_dire_l }, "direction"),
|
direction: UC.newSelect(LANG.ou_dire_s, { title: LANG.ou_dire_l }, "direction"),
|
||||||
sr_angle: UC.newInput("step angle", { title: "step angle", convert: toFloat, bound: UC.bound(0, 360), show: isSurfaceLinear }),
|
sr_angle: UC.newInput(LANG.ca_sang_s, { title: LANG.ca_sang_l, convert: toFloat, bound: UC.bound(0, 360), show: isSurfaceLinear }),
|
||||||
over: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: toFloat, bound: UC.bound(0.001, 100.0), show: () => isClear() || isSurface() }),
|
over: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: toFloat, bound: UC.bound(0.001, 100.0), show: () => isClear() || isSurface() }),
|
||||||
down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: toFloat, bound: UC.bound(0, 100.0), units, show: () => isClear() || isTrace() }),
|
down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: toFloat, bound: UC.bound(0, 100.0), units, show: () => isClear() || isTrace() }),
|
||||||
refine: UC.newInput(LANG.cp_refi_s, { title: LANG.cp_refi_l, convert: toInt, show: isSurface }),
|
refine: UC.newInput(LANG.cp_refi_s, { title: LANG.cp_refi_l, convert: toInt, show: isSurface }),
|
||||||
|
sr_alter: UC.newBoolean(LANG.ca_altr_s, undefined, { title: LANG.ca_altr_l, show: isSurfaceLinear }),
|
||||||
dogbones: UC.newBoolean(LANG.co_dogb_s, undefined, { title: LANG.co_dogb_l, show: isTrace }),
|
dogbones: UC.newBoolean(LANG.co_dogb_s, undefined, { title: LANG.co_dogb_l, show: isTrace }),
|
||||||
revbones: UC.newBoolean(LANG.co_dogr_s, undefined, { title: LANG.co_dogr_l, show: () => env.poppedRec.dogbones }),
|
revbones: UC.newBoolean(LANG.co_dogr_s, undefined, { title: LANG.co_dogr_l, show: () => env.poppedRec.dogbones }),
|
||||||
exp_end: UC.endExpand(),
|
exp_end: UC.endExpand(),
|
||||||
exp: UC.newExpand("feeds & speeds", { open }),
|
exp: UC.newExpand("feeds & speeds", { }),
|
||||||
sep: UC.newBlank({ class: "pop-sep" }),
|
sep: UC.newBlank({ class: "pop-sep" }),
|
||||||
spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: toInt, show: hasSpindle }),
|
spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: toInt, show: hasSpindle }),
|
||||||
rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: toInt, units }),
|
rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: toInt, units }),
|
||||||
plunge: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: toInt, units }),
|
plunge: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: toInt, units }),
|
||||||
exp_end: UC.endExpand(),
|
exp_end: UC.endExpand(),
|
||||||
exp: UC.newExpand("bounds", { open }),
|
exp: UC.newExpand("bounds", ov_hover),
|
||||||
sep: UC.newBlank({ class: "pop-sep" }),
|
sep: UC.newBlank({ class: "pop-sep" }),
|
||||||
ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: toFloat, units }),
|
ov_topz: UC.newInput(LANG.ou_ztop_s, ov_topz),
|
||||||
ov_botz: UC.newInput(LANG.ou_zbot_s, { title: LANG.ou_zbot_l, convert: toFloat, units }),
|
ov_botz: UC.newInput(LANG.ou_zbot_s, ov_botz),
|
||||||
exp_end: UC.endExpand(),
|
exp_end: UC.endExpand(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
import { api } from '../../core/api.js';
|
import { api } from '../../../app/api.js';
|
||||||
import { env, clearPops } from './client.js';
|
import { env, clearPops } from './init-ui.js';
|
||||||
import { CAM } from './driver-fe.js';
|
import { CAM } from './dispatch.js';
|
||||||
import { addbox, clearboxes } from '../../core/boxes.js';
|
import { addbox, clearboxes } from '../../../app/boxes.js';
|
||||||
|
|
||||||
export let originSelectOn = false;
|
export let originSelectOn = false;
|
||||||
|
|
||||||
let { world } = api.SPACE;
|
let { world } = api.const.SPACE;
|
||||||
let alert, lastHover, lastPoints;
|
let alert, lastHover, lastPoints;
|
||||||
|
|
||||||
export function originSelect() {
|
export function originSelect() {
|
||||||
|
|
@ -1,13 +1,77 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
import { api } from '../../core/api.js';
|
import { api } from '../../../app/api.js';
|
||||||
import { env, isDark } from './client.js';
|
import { env, isDark } from './init-ui.js';
|
||||||
import { space as SPACE } from '../../../moto/space.js';
|
import { space as SPACE } from '../../../../moto/space.js';
|
||||||
|
|
||||||
const { ui: UI } = api;
|
const { ui: UI } = api;
|
||||||
|
|
||||||
let meshZTop, meshZBottom;
|
let meshZTop, meshZBottom;
|
||||||
|
|
||||||
|
{
|
||||||
|
let geo = new THREE.PlaneGeometry(1, 1);
|
||||||
|
let mat = new THREE.MeshBasicMaterial({
|
||||||
|
color: 0x779977,
|
||||||
|
opacity: 0.55,
|
||||||
|
transparent: true,
|
||||||
|
side: THREE.DoubleSide
|
||||||
|
});
|
||||||
|
meshZTop = new THREE.Mesh(geo, mat);
|
||||||
|
meshZTop.renderOrder = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let geo = new THREE.PlaneGeometry(1, 1);
|
||||||
|
let mat = new THREE.MeshBasicMaterial({
|
||||||
|
color: 0x997777,
|
||||||
|
opacity: 0.55,
|
||||||
|
transparent: true,
|
||||||
|
side: THREE.DoubleSide
|
||||||
|
});
|
||||||
|
meshZBottom = new THREE.Mesh(geo, mat);
|
||||||
|
meshZBottom.renderOrder = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBounds() {
|
||||||
|
const settings = api.conf.get();
|
||||||
|
const { stock } = settings;
|
||||||
|
const { x, y, z, center } = stock;
|
||||||
|
const widgets = api.widgets.all();
|
||||||
|
let max = { x, y, z };
|
||||||
|
for (let w of widgets) {
|
||||||
|
max.x = Math.max(max.x, w.track.box.w);
|
||||||
|
max.y = Math.max(max.y, w.track.box.h);
|
||||||
|
max.z = Math.max(max.z, w.track.box.d);
|
||||||
|
}
|
||||||
|
return widgets.length ? { max, center } : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showZTop(zval) {
|
||||||
|
SPACE.world.remove(meshZTop);
|
||||||
|
let { max, center } = getBounds();
|
||||||
|
if (zval && max && center) {
|
||||||
|
meshZTop.position.x = center.x;
|
||||||
|
meshZTop.position.y = center.y;
|
||||||
|
meshZTop.position.z = zval;
|
||||||
|
meshZTop.scale.x = max.x;
|
||||||
|
meshZTop.scale.y = max.y;
|
||||||
|
SPACE.world.add(meshZTop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showZBottom(zval) {
|
||||||
|
SPACE.world.remove(meshZBottom);
|
||||||
|
let { max, center } = getBounds();
|
||||||
|
if (zval && max && center) {
|
||||||
|
meshZBottom.position.x = center.x;
|
||||||
|
meshZBottom.position.y = center.y;
|
||||||
|
meshZBottom.position.z = zval;
|
||||||
|
meshZBottom.scale.x = max.x;
|
||||||
|
meshZBottom.scale.y = max.y;
|
||||||
|
SPACE.world.add(meshZBottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function updateStock() {
|
export function updateStock() {
|
||||||
if (env.isAnimate) {
|
if (env.isAnimate) {
|
||||||
if (env.isIndexed) {
|
if (env.isIndexed) {
|
||||||
|
|
@ -22,16 +86,12 @@ export function updateStock() {
|
||||||
SPACE.world.remove(meshZBottom);
|
SPACE.world.remove(meshZBottom);
|
||||||
SPACE.world.remove(env.camStock);
|
SPACE.world.remove(env.camStock);
|
||||||
env.camStock = null;
|
env.camStock = null;
|
||||||
meshZTop = null;
|
|
||||||
meshZBottom = null;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
api.platform.update_bounds();
|
api.platform.update_bounds();
|
||||||
|
|
||||||
const settings = api.conf.get();
|
const settings = api.conf.get();
|
||||||
const widgets = api.widgets.all();
|
|
||||||
|
|
||||||
const { stock, process } = settings;
|
const { stock, process } = settings;
|
||||||
const { x, y, z, center } = stock;
|
const { x, y, z, center } = stock;
|
||||||
|
|
||||||
|
|
@ -86,57 +146,8 @@ export function updateStock() {
|
||||||
lines.material.color = new THREE.Color(isDark() ? 0x555555 : 0xaaaaaa);
|
lines.material.color = new THREE.Color(isDark() ? 0x555555 : 0xaaaaaa);
|
||||||
}
|
}
|
||||||
|
|
||||||
SPACE.world.remove(meshZTop);
|
showZTop(process.camZTop);
|
||||||
if (process.camZTop && widgets.length) {
|
showZBottom(process.camZBottom);
|
||||||
let max = { x, y, z };
|
|
||||||
for (let w of widgets) {
|
|
||||||
max.x = Math.max(max.x, w.track.box.w);
|
|
||||||
max.y = Math.max(max.y, w.track.box.h);
|
|
||||||
max.z = Math.max(max.z, w.track.box.d);
|
|
||||||
}
|
|
||||||
let geo = new THREE.PlaneGeometry(max.x, max.y);
|
|
||||||
let mat = new THREE.MeshBasicMaterial({
|
|
||||||
color: 0x777777,
|
|
||||||
opacity: 0.55,
|
|
||||||
transparent: true,
|
|
||||||
side: THREE.DoubleSide
|
|
||||||
});
|
|
||||||
meshZTop = new THREE.Mesh(geo, mat);
|
|
||||||
meshZTop._max = max;
|
|
||||||
meshZTop.renderOrder = 1;
|
|
||||||
meshZTop.position.x = center.x;
|
|
||||||
meshZTop.position.y = center.y;
|
|
||||||
meshZTop.position.z = process.camZTop;
|
|
||||||
SPACE.world.add(meshZTop);
|
|
||||||
} else {
|
|
||||||
meshZTop = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
SPACE.world.remove(meshZBottom);
|
|
||||||
if (process.camZBottom && widgets.length) {
|
|
||||||
let max = { x, y, z };
|
|
||||||
for (let w of widgets) {
|
|
||||||
max.x = Math.max(max.x, w.track.box.w);
|
|
||||||
max.y = Math.max(max.y, w.track.box.h);
|
|
||||||
max.z = Math.max(max.z, w.track.box.d);
|
|
||||||
}
|
|
||||||
let geo = new THREE.PlaneGeometry(max.x, max.y);
|
|
||||||
let mat = new THREE.MeshBasicMaterial({
|
|
||||||
color: 0x777777,
|
|
||||||
opacity: 0.55,
|
|
||||||
transparent: true,
|
|
||||||
side: THREE.DoubleSide
|
|
||||||
});
|
|
||||||
meshZBottom = new THREE.Mesh(geo, mat);
|
|
||||||
meshZBottom._max = max;
|
|
||||||
meshZBottom.renderOrder = 1;
|
|
||||||
meshZBottom.position.x = center.x;
|
|
||||||
meshZBottom.position.y = center.y;
|
|
||||||
meshZBottom.position.z = process.camZBottom;
|
|
||||||
SPACE.world.add(meshZBottom);
|
|
||||||
} else {
|
|
||||||
meshZBottom = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
SPACE.update();
|
SPACE.update();
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
import { api } from '../../core/api.js';
|
import { api } from '../../../app/api.js';
|
||||||
import { env, clearPops } from './client.js';
|
import { env, clearPops } from './init-ui.js';
|
||||||
import { CAM } from './driver-fe.js';
|
import { CAM } from './dispatch.js';
|
||||||
|
|
||||||
const DEG2RAD = Math.PI / 180;
|
const DEG2RAD = Math.PI / 180;
|
||||||
const RAD2DEG = 180 / Math.PI;
|
const RAD2DEG = 180 / Math.PI;
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
import { api } from '../../core/api.js';
|
import { api } from '../../../app/api.js';
|
||||||
import { env, isDark } from './client.js';
|
import { colorSchemeRegistry } from '../../../app/color/schemes.js';
|
||||||
import { addbox, delbox, clearboxes } from '../../core/boxes.js';
|
import { env, isDark } from './init-ui.js';
|
||||||
|
import { addbox, delbox, clearboxes } from '../../../app/boxes.js';
|
||||||
import { traceDone } from './cl-trace.js';
|
import { traceDone } from './cl-trace.js';
|
||||||
import { space as SPACE } from '../../../moto/space.js';
|
import { space as SPACE } from '../../../../moto/space.js';
|
||||||
|
|
||||||
let showTab, lastTab, tab, iw, ic;
|
let showTab, lastTab, tab, iw, ic;
|
||||||
|
|
||||||
|
|
@ -225,9 +226,15 @@ export function tabClear() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function boxColor() {
|
function boxColor() {
|
||||||
return isDark() ? 0x00ddff : 0x0000dd;
|
const mode = 3; // CAM mode ID
|
||||||
|
const theme = isDark() ? 'dark' : 'light';
|
||||||
|
const scheme = colorSchemeRegistry.getScheme(mode, theme);
|
||||||
|
return scheme.operations?.tabs?.color ?? (isDark() ? 0x00ddff : 0x0000dd);
|
||||||
}
|
}
|
||||||
|
|
||||||
function boxOpacity() {
|
function boxOpacity() {
|
||||||
return isDark() ? 0.75 : 0.6;
|
const mode = 3; // CAM mode ID
|
||||||
|
const theme = isDark() ? 'dark' : 'light';
|
||||||
|
const scheme = colorSchemeRegistry.getScheme(mode, theme);
|
||||||
|
return scheme.operations?.tabs?.opacity ?? (isDark() ? 0.75 : 0.6);
|
||||||
}
|
}
|
||||||
230
src/kiri/mode/cam/app/cl-trace.js
Normal file
230
src/kiri/mode/cam/app/cl-trace.js
Normal file
|
|
@ -0,0 +1,230 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
import { api } from '../../../app/api.js';
|
||||||
|
import { env, clearPops, isDark } from './init-ui.js';
|
||||||
|
import { parse } from '../../../../load/svg.js';
|
||||||
|
import { CAM } from './dispatch.js';
|
||||||
|
import { Layers } from '../../../app/layers.js';
|
||||||
|
import { Stack } from '../../../app/stack.js';
|
||||||
|
import { newPolygon } from '../../../../geo/polygon.js';
|
||||||
|
|
||||||
|
export let traceOn = false;
|
||||||
|
export let lastTrace;
|
||||||
|
|
||||||
|
export function setLastTrace(trace) {
|
||||||
|
lastTrace = trace;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function traceLoad(ev) {
|
||||||
|
let widget = api.widgets.all()[0];
|
||||||
|
if (!widget) return;
|
||||||
|
window.showOpenFilePicker({
|
||||||
|
types: [{
|
||||||
|
description: 'SVG Files',
|
||||||
|
accept: { 'text/svg': ['.svg'] }
|
||||||
|
}],
|
||||||
|
multiple: false
|
||||||
|
}).then(async fileHandle => {
|
||||||
|
const file = await fileHandle[0].getFile()
|
||||||
|
const text = await file.text(); // string
|
||||||
|
// const buf = await file.arrayBuffer(); // binary
|
||||||
|
// const blob = await file.slice();
|
||||||
|
const polys = parse(text, { flat: true, soup: true });
|
||||||
|
const rec = env.poppedRec;
|
||||||
|
rec.areas[widget.id] = polys.map(p => p.toArray());
|
||||||
|
rec.svg = true;
|
||||||
|
traceAdd();
|
||||||
|
}).catch(error => {
|
||||||
|
console.log('no file load', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function traceClear(ev) {
|
||||||
|
return;
|
||||||
|
api.widgets.for(widget => {
|
||||||
|
unselectTraces(widget, true);
|
||||||
|
widget.traces = [];
|
||||||
|
widget.adds = [];
|
||||||
|
});
|
||||||
|
env.poppedRec.areas = [];
|
||||||
|
env.poppedRec.svg = false;
|
||||||
|
traceDone();
|
||||||
|
}
|
||||||
|
|
||||||
|
function startTraceAdd(ids) {
|
||||||
|
api.hide.alert(alert);
|
||||||
|
alert = api.show.alert("[esc] cancels trace editing");
|
||||||
|
api.widgets.for(widget => {
|
||||||
|
if (ids.indexOf(widget.id) >= 0) {
|
||||||
|
unselectTraces(widget, true);
|
||||||
|
widget.trace_stack = null;
|
||||||
|
}
|
||||||
|
if (widget.trace_stack) {
|
||||||
|
widget.adds.appendAll(widget.trace_stack.meshes);
|
||||||
|
widget.trace_stack.show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let areas = (env.poppedRec.areas[widget.id] || []);
|
||||||
|
let stack = new Stack(widget.mesh);
|
||||||
|
widget.trace_stack = stack;
|
||||||
|
widget.traces?.forEach(poly => {
|
||||||
|
let match = areas.filter(arr => poly.matches(arr));
|
||||||
|
let layers = new Layers();
|
||||||
|
layers.setLayer("trace", { line: 0xaaaa55, fat: 4, order: -10 }, false).addPoly(poly);
|
||||||
|
stack.addLayers(layers);
|
||||||
|
stack.new_meshes.forEach(mesh => {
|
||||||
|
mesh.trace = { widget, poly };
|
||||||
|
// ensure trace poly singleton from matches
|
||||||
|
if (match.length > 0) {
|
||||||
|
poly._trace = match[0];
|
||||||
|
} else {
|
||||||
|
poly._trace = poly.toArray();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
widget.adds.appendAll(stack.new_meshes);
|
||||||
|
// console.log(widget)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// ensure appropriate traces are toggled matching current record
|
||||||
|
api.widgets.for(widget => {
|
||||||
|
widget.setVisualState({ opacity: 0.25 });
|
||||||
|
let areas = (env.poppedRec.areas[widget.id] || []);
|
||||||
|
let stack = widget.trace_stack;
|
||||||
|
stack.meshes.forEach(mesh => {
|
||||||
|
let { poly } = mesh.trace;
|
||||||
|
let match = areas.filter(arr => poly.matches(arr));
|
||||||
|
if (match.length > 0) {
|
||||||
|
if (!mesh.selected) {
|
||||||
|
traceToggle(mesh, true);
|
||||||
|
}
|
||||||
|
} else if (mesh.selected) {
|
||||||
|
traceToggle(mesh, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function traceAdd(ev) {
|
||||||
|
if (traceOn) {
|
||||||
|
return traceDone();
|
||||||
|
}
|
||||||
|
clearPops();
|
||||||
|
alert = api.show.alert("analyzing parts...", 1000);
|
||||||
|
traceOn = env.hoveredOp;
|
||||||
|
traceOn.classList.add("editing");
|
||||||
|
api.feature.hover = true;
|
||||||
|
api.feature.hoverAdds = true;
|
||||||
|
env.hover = traceHover;
|
||||||
|
env.hoverUp = traceHoverUp;
|
||||||
|
if (env.poppedRec.svg === true) {
|
||||||
|
let widgets = api.widgets.all();
|
||||||
|
for (let [ id, areas ] of Object.entries(env.poppedRec.areas)) {
|
||||||
|
let widget = widgets.filter(w => w.id === id)[0];
|
||||||
|
widget.traces = areas.map(arr => newPolygon().fromArray(arr));
|
||||||
|
}
|
||||||
|
startTraceAdd(Object.keys(env.poppedRec.areas));
|
||||||
|
} else {
|
||||||
|
CAM.traces(startTraceAdd, env.poppedRec.select === 'lines');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function traceDone() {
|
||||||
|
if (!traceOn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
env.func.unpop();
|
||||||
|
traceOn.classList.remove("editing");
|
||||||
|
traceOn = false;
|
||||||
|
api.widgets.setOpacity(1);
|
||||||
|
api.hide.alert(alert);
|
||||||
|
api.feature.hover = false;
|
||||||
|
api.feature.hoverAdds = false;
|
||||||
|
api.widgets.for(widget => {
|
||||||
|
widget.restoreVisualState();
|
||||||
|
if (widget.trace_stack) {
|
||||||
|
widget.trace_stack.hide();
|
||||||
|
widget.adds.removeAll(widget.trace_stack.meshes);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function traceHover(data) {
|
||||||
|
if (lastTrace) {
|
||||||
|
let { color, colorSave } = lastTrace.material[0] || lastTrace.material;
|
||||||
|
color.r = colorSave.r;
|
||||||
|
color.g = colorSave.g;
|
||||||
|
color.b = colorSave.b;
|
||||||
|
}
|
||||||
|
lastTrace = null;
|
||||||
|
if (data.type === 'platform') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!data.int.object.trace) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastTrace = data.int.object;
|
||||||
|
if (lastTrace.selected) {
|
||||||
|
let event = data.event;
|
||||||
|
let target = event.target;
|
||||||
|
let { clientX, clientY } = event;
|
||||||
|
let { offsetWidth, offsetHeight } = target;
|
||||||
|
}
|
||||||
|
let material = lastTrace.material[0] || lastTrace.material;
|
||||||
|
let color = material.color;
|
||||||
|
material.colorSave = color.clone();
|
||||||
|
color.setHex(isDark() ? 0x0066ff : 0x0000ff);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function traceHoverUp(int, ev) {
|
||||||
|
if (!int) return;
|
||||||
|
let { object } = int;
|
||||||
|
traceToggle(object);
|
||||||
|
if (ev.metaKey || ev.ctrlKey) {
|
||||||
|
let { selected } = object;
|
||||||
|
let { widget, poly } = object.trace;
|
||||||
|
let avgZ = poly.avgZ();
|
||||||
|
for (let add of widget.adds) {
|
||||||
|
if (add.trace && add.selected !== selected && add.trace.poly.onZ(avgZ)) {
|
||||||
|
traceToggle(add);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function traceToggle(obj, skip) {
|
||||||
|
let material = obj.material[0] || obj.material;
|
||||||
|
if (!(material && obj.trace)) return;
|
||||||
|
let { color, colorSave } = material;
|
||||||
|
let { widget, poly } = obj.trace;
|
||||||
|
let areas = env.poppedRec.areas;
|
||||||
|
if (!areas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let wlist = areas[widget.id] = areas[widget.id] || [];
|
||||||
|
obj.selected = !obj.selected;
|
||||||
|
if (!colorSave) {
|
||||||
|
colorSave = material.colorSave = color.clone();
|
||||||
|
}
|
||||||
|
if (obj.selected) {
|
||||||
|
color.setHex(isDark() ? 0xdd0011 : 0xff0033);
|
||||||
|
colorSave.r = color.r;
|
||||||
|
colorSave.g = color.g;
|
||||||
|
colorSave.b = color.b;
|
||||||
|
if (!skip) wlist.push(poly._trace);
|
||||||
|
} else {
|
||||||
|
color.setHex(0xaaaa55);
|
||||||
|
colorSave.setHex(0xaaaa55);
|
||||||
|
if (!skip) wlist.remove(poly._trace);
|
||||||
|
}
|
||||||
|
api.conf.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unselectTraces(widget, skip) {
|
||||||
|
if (widget.trace_stack) {
|
||||||
|
widget.trace_stack.meshes.forEach(mesh => {
|
||||||
|
if (mesh.selected) {
|
||||||
|
traceToggle(mesh, skip);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/kiri/mode/cam/app/cl-zplane.js
Normal file
41
src/kiri/mode/cam/app/cl-zplane.js
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
import { api } from '../../../app/api.js';
|
||||||
|
import { clearPops, hoverStart } from './init-ui.js';
|
||||||
|
import { showZTop, showZBottom, updateStock } from './cl-stock.js';
|
||||||
|
|
||||||
|
let alert;
|
||||||
|
let plane;
|
||||||
|
let ondone;
|
||||||
|
let lastY;
|
||||||
|
|
||||||
|
export function zPlaneStart(which, onselect) {
|
||||||
|
plane = which;
|
||||||
|
ondone = onselect;
|
||||||
|
hoverStart(onHover, onHoverUp);
|
||||||
|
alert = api.show.alert("[esc] cancels z plane selection", 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function zPlaneDone() {
|
||||||
|
api.hide.alert(alert);
|
||||||
|
updateStock();
|
||||||
|
alert = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function zPlaneSelecting() {
|
||||||
|
return alert ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onHover(data) {
|
||||||
|
const { int, point } = data;
|
||||||
|
if (!int) return;
|
||||||
|
if (plane === 'top') showZTop(lastY = point.y); else
|
||||||
|
if (plane === 'bottom') showZBottom(lastY = point.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onHoverUp(int) {
|
||||||
|
if (int) {
|
||||||
|
ondone(lastY);
|
||||||
|
clearPops();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
import { api } from '../../core/api.js';
|
import { api } from '../../../app/api.js';
|
||||||
import { codec } from '../../core/codec.js';
|
import { codec } from '../../../core/codec.js';
|
||||||
|
|
||||||
function surface_prep(index, ondone) {
|
function surface_prep(index, ondone) {
|
||||||
api.client.sync();
|
api.client.sync();
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
import { api } from '../../core/api.js';
|
import { api } from '../../../app/api.js';
|
||||||
import { originReset, originSelect } from './cl-origin.js';
|
import { originReset, originSelect } from './cl-origin.js';
|
||||||
import { updateTool } from './tools.js';
|
import { updateTool } from './tools.js';
|
||||||
|
|
||||||
|
|
@ -30,6 +30,30 @@ function zAnchorSave() {
|
||||||
api.platform.update_top_z();
|
api.platform.update_top_z();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bottom = true;
|
||||||
|
const top = true;
|
||||||
|
|
||||||
|
function selector(type, { input, event, opt }) {
|
||||||
|
let which = opt?.top ? 'top' : opt?.bottom ? 'bottom' : 'unknown';
|
||||||
|
let value = parseFloat(input.value);
|
||||||
|
switch (type) {
|
||||||
|
case 'click':
|
||||||
|
api.event.emit('cam.zplane.select', { which, onselect(value) {
|
||||||
|
input.focus();
|
||||||
|
input.value = value;
|
||||||
|
input.blur();
|
||||||
|
} } );
|
||||||
|
break;
|
||||||
|
case 'enter':
|
||||||
|
api.event.emit('cam.zplane.show', { which, value });
|
||||||
|
break;
|
||||||
|
case 'leave':
|
||||||
|
api.event.emit('cam.zplane.hide', { which });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export function menu() {
|
export function menu() {
|
||||||
let anim = ui.anim = {};
|
let anim = ui.anim = {};
|
||||||
|
|
||||||
|
|
@ -112,15 +136,17 @@ export function menu() {
|
||||||
camStockZ: newInput(LANG.cs_hght_s, {title:LANG.cs_hght_l, convert:toFloat, bound:bound(0,9999), units}),
|
camStockZ: newInput(LANG.cs_hght_s, {title:LANG.cs_hght_l, convert:toFloat, bound:bound(0,9999), units}),
|
||||||
separator: newBlank({ class:"set-sep", driven }),
|
separator: newBlank({ class:"set-sep", driven }),
|
||||||
camStockOffset: newBoolean(LANG.cs_offs_s, onBooleanClick, {title:LANG.cs_offs_l}),
|
camStockOffset: newBoolean(LANG.cs_offs_s, onBooleanClick, {title:LANG.cs_offs_l}),
|
||||||
camStockClipTo: newBoolean(LANG.cs_clip_s, onBooleanClick, {title:LANG.cs_clip_l}),
|
|
||||||
camStockIndexed: newBoolean(LANG.cs_indx_s, onBooleanClick, {title:LANG.cs_indx_l}),
|
camStockIndexed: newBoolean(LANG.cs_indx_s, onBooleanClick, {title:LANG.cs_indx_l}),
|
||||||
camStockIndexGrid: newBoolean(LANG.cs_ishg_s, onBooleanClick, {title:LANG.cs_ishg_l, show:() => ui.camStockIndexed.checked}),
|
camStockIndexGrid: newBoolean(LANG.cs_ishg_s, onBooleanClick, {title:LANG.cs_ishg_l, show:() => ui.camStockIndexed.checked}),
|
||||||
|
// separator: newBlank({ class:"set-sep", driven }),
|
||||||
|
// camStockManual: newRow([
|
||||||
|
// (ui.stockPlace = newButton('position', onButtonClick, { })),
|
||||||
|
// ], {class:"ext-buttons f-row"}),
|
||||||
_____: newGroup(LANG.cc_menu, $('cam-limits'), { modes:CAM, driven, separator }),
|
_____: newGroup(LANG.cc_menu, $('cam-limits'), { modes:CAM, driven, separator }),
|
||||||
camZAnchor: newSelect(LANG.ou_zanc_s, {title: LANG.ou_zanc_l, action:zAnchorSave, show:() => !ui.camStockIndexed.checked}, "zanchor"),
|
camZAnchor: newSelect(LANG.ou_zanc_s, {title: LANG.ou_zanc_l, action:zAnchorSave, show:() => !ui.camStockIndexed.checked}, "zanchor"),
|
||||||
camZOffset: newInput(LANG.ou_ztof_s, {title:LANG.ou_ztof_l, convert:toFloat, units}),
|
camZOffset: newInput(LANG.ou_ztof_s, {title:LANG.ou_ztof_l, convert:toFloat, units}),
|
||||||
camZTop: newInput(LANG.ou_ztop_s, {title:LANG.ou_ztop_l, convert:toFloat, units, trigger}),
|
camZTop: newInput(LANG.ou_ztop_s, {title:LANG.ou_ztop_l, convert:toFloat, units, trigger, selector, top }),
|
||||||
camZBottom: newInput(LANG.ou_zbot_s, {title:LANG.ou_zbot_l, convert:toFloat, units, trigger}),
|
camZBottom: newInput(LANG.ou_zbot_s, {title:LANG.ou_zbot_l, convert:toFloat, units, trigger, selector, bottom }),
|
||||||
// camZThru: newInput(LANG.ou_ztru_s, {title:LANG.ou_ztru_l, convert:toFloat, bound:bound(0.0,100), units }),
|
|
||||||
camZClearance: newInput(LANG.ou_zclr_s, {title:LANG.ou_zclr_l, convert:toFloat, bound:bound(0.01,100), units }),
|
camZClearance: newInput(LANG.ou_zclr_s, {title:LANG.ou_zclr_l, convert:toFloat, bound:bound(0.01,100), units }),
|
||||||
separator: newBlank({ class:"set-sep", driven }),
|
separator: newBlank({ class:"set-sep", driven }),
|
||||||
camFastFeed: newInput(LANG.cc_rapd_s, {title:LANG.cc_rapd_l, convert:toFloat, units}),
|
camFastFeed: newInput(LANG.cc_rapd_s, {title:LANG.cc_rapd_l, convert:toFloat, units}),
|
||||||
|
|
@ -1,24 +1,25 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
import { $ } from '../../../moto/webui.js';
|
import { $ } from '../../../../moto/webui.js';
|
||||||
import { api } from '../../core/api.js';
|
import { api } from '../../../app/api.js';
|
||||||
import { load } from '../../../load/file.js';
|
import { animate as anim_2d, animate_clear as anim_2d_clear } from './anim-2d.js';
|
||||||
import { animate as anim_2d, animate_clear as anim_2d_clear } from './anim-2d-fe.js';
|
import { animate2 as anim_3d, animate_clear2 as anim_3d_clear } from './anim-3d.js';
|
||||||
import { animate2 as anim_3d, animate_clear2 as anim_3d_clear } from './anim-3d-fe.js';
|
|
||||||
import { space as SPACE } from '../../../moto/space.js';
|
|
||||||
import { Layers } from '../../core/layers.js';
|
|
||||||
import { Stack } from '../../core/stack.js';
|
|
||||||
import { updateStock } from './cl-stock.js';
|
|
||||||
import { createPopOps } from './cl-ops.js';
|
import { createPopOps } from './cl-ops.js';
|
||||||
|
import { helicalOn, helicalDone } from './cl-helical.js';
|
||||||
|
import { holeSelOn, selectHolesDone, clearHolesRec } from './cl-hole.js';
|
||||||
|
import { load } from '../../../../load/file.js';
|
||||||
|
import { Layers } from '../../../app/layers.js';
|
||||||
|
import { originSelectDone } from './cl-origin.js';
|
||||||
|
import { recreateTabs } from './cl-tab.js';
|
||||||
|
import { showZTop, showZBottom, updateStock } from './cl-stock.js';
|
||||||
|
import { space as SPACE } from '../../../../moto/space.js';
|
||||||
|
import { space } from '../../../../moto/space.js';
|
||||||
|
import { Stack } from '../../../app/stack.js';
|
||||||
|
import { surfaceOn, surfaceDone } from './cl-surface.js';
|
||||||
import { tabAdd, tabDone, tabClear, restoreTabs, rotateTabs, updateTabs, clearTabs, mirrorTabs } from './cl-tab.js';
|
import { tabAdd, tabDone, tabClear, restoreTabs, rotateTabs, updateTabs, clearTabs, mirrorTabs } from './cl-tab.js';
|
||||||
import { traceOn, traceDone, unselectTraces } from './cl-trace.js';
|
import { traceOn, traceDone, unselectTraces } from './cl-trace.js';
|
||||||
import { holeSelOn, selectHolesDone, clearHolesRec } from './cl-hole.js';
|
import { Widget, newWidget } from '../../../app/widget.js';
|
||||||
import { surfaceOn, surfaceDone } from './cl-surface.js';
|
import { zPlaneStart, zPlaneDone, zPlaneSelecting } from './cl-zplane.js';
|
||||||
import { helicalOn, helicalDone } from './cl-helical.js';
|
|
||||||
import { originSelectDone } from './cl-origin.js';
|
|
||||||
import { Widget, newWidget } from '../../core/widget.js';
|
|
||||||
import { space } from '../../../moto/space.js';
|
|
||||||
import { recreateTabs } from './cl-tab.js';
|
|
||||||
|
|
||||||
const { BufferGeometryUtils } = THREE;
|
const { BufferGeometryUtils } = THREE;
|
||||||
|
|
||||||
|
|
@ -62,13 +63,28 @@ export function isDark() {
|
||||||
|
|
||||||
export function clearPops() {
|
export function clearPops() {
|
||||||
if (env.func.unpop) env.func.unpop();
|
if (env.func.unpop) env.func.unpop();
|
||||||
|
helicalDone();
|
||||||
originSelectDone();
|
originSelectDone();
|
||||||
selectHolesDone();
|
selectHolesDone();
|
||||||
surfaceDone();
|
surfaceDone();
|
||||||
tabDone();
|
tabDone();
|
||||||
traceDone();
|
traceDone();
|
||||||
selectHolesDone();
|
zPlaneDone();
|
||||||
helicalDone();
|
// unbind
|
||||||
|
hoverDone();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hoverStart(onhover, onhoverup) {
|
||||||
|
clearPops();
|
||||||
|
env.hover = onhover;
|
||||||
|
env.hoverUp = onhoverup;
|
||||||
|
api.feature.hover = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hoverDone() {
|
||||||
|
env.hover = noop;
|
||||||
|
env.hoverUp = noop;
|
||||||
|
api.feature.hover = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function animFn() {
|
function animFn() {
|
||||||
|
|
@ -95,7 +111,8 @@ function updateIndex() {
|
||||||
if (op.absolute) {
|
if (op.absolute) {
|
||||||
index = op.degrees
|
index = op.degrees
|
||||||
} else {
|
} else {
|
||||||
index += op.degrees;
|
let numloop = op._numloop ?? 1;
|
||||||
|
index += (op.degrees * numloop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -124,6 +141,7 @@ function updateAxisMode(refresh) {
|
||||||
const showNonIndexed = env.isIndexed ? 'none' : '';
|
const showNonIndexed = env.isIndexed ? 'none' : '';
|
||||||
$('cam-index').style.display = showIndexed;
|
$('cam-index').style.display = showIndexed;
|
||||||
$('cam-lathe').style.display = showIndexed;
|
$('cam-lathe').style.display = showIndexed;
|
||||||
|
$('cam-loop').style.display = showIndexed;
|
||||||
$('cam-flip').style.display = showNonIndexed;
|
$('cam-flip').style.display = showNonIndexed;
|
||||||
$('cam-reg').style.display = showNonIndexed;
|
$('cam-reg').style.display = showNonIndexed;
|
||||||
if (!changed) {
|
if (!changed) {
|
||||||
|
|
@ -165,6 +183,10 @@ const opAddLevel = () => {
|
||||||
opAdd(env.popOp.level.new());
|
opAdd(env.popOp.level.new());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const opAddLoop = () => {
|
||||||
|
opAdd(env.popOp.loop.new());
|
||||||
|
};
|
||||||
|
|
||||||
const opAddRough = () => {
|
const opAddRough = () => {
|
||||||
opAdd(env.popOp.rough.new());
|
opAdd(env.popOp.rough.new());
|
||||||
};
|
};
|
||||||
|
|
@ -282,13 +304,29 @@ export function opRender() {
|
||||||
let mark = Date.now();
|
let mark = Date.now();
|
||||||
let html = [];
|
let html = [];
|
||||||
let bind = {};
|
let bind = {};
|
||||||
let scale = api.view.unit_scale();
|
|
||||||
let notime = false;
|
let notime = false;
|
||||||
|
let inloop = 0;
|
||||||
|
let numloop;
|
||||||
oplist.forEach((rec, i) => {
|
oplist.forEach((rec, i) => {
|
||||||
let title = '';
|
let title = '';
|
||||||
let clock = rec.type === '|';
|
let clock = rec.type === '|';
|
||||||
|
let isLoop = rec.type === 'loop';
|
||||||
let label = clock ? `` : (rec.mode ? rec.type + ' ' + rec.mode : rec.type);
|
let label = clock ? `` : (rec.mode ? rec.type + ' ' + rec.mode : rec.type);
|
||||||
let clazz = notime ? ["draggable", "notime"] : ["draggable"];
|
let clazz = notime ? ["draggable", "notime"] : ["draggable"];
|
||||||
|
|
||||||
|
// Loop operation label and styling
|
||||||
|
if (isLoop) {
|
||||||
|
label = `loop ${rec.repeat || 2}x`;
|
||||||
|
clazz.push('loop');
|
||||||
|
title = ` title="loop: repeat next ${rec.count || 1} operation(s) ${rec.repeat || 2} time(s)"`;
|
||||||
|
inloop = rec.count;
|
||||||
|
numloop = rec.repeat;
|
||||||
|
} else if (inloop && !clock) {
|
||||||
|
clazz.push('in-loop');
|
||||||
|
rec._numloop = numloop;
|
||||||
|
inloop--;
|
||||||
|
}
|
||||||
|
|
||||||
let notable = rec.note ? rec.note.split(' ').filter(v => v.charAt(0) === '#') : undefined;
|
let notable = rec.note ? rec.note.split(' ').filter(v => v.charAt(0) === '#') : undefined;
|
||||||
if (clock) { clazz.push('clock'); title = ` title="end of ops chain\ndrag/drop like an op\nops after this are disabled"` }
|
if (clock) { clazz.push('clock'); title = ` title="end of ops chain\ndrag/drop like an op\nops after this are disabled"` }
|
||||||
if (notable?.length) {
|
if (notable?.length) {
|
||||||
|
|
@ -333,6 +371,9 @@ export function opRender() {
|
||||||
if (!env.isIndexed && type === 'lathe') {
|
if (!env.isIndexed && type === 'lathe') {
|
||||||
rec.disabled = true;
|
rec.disabled = true;
|
||||||
}
|
}
|
||||||
|
if (!env.isIndexed && (type === 'loop' || rec._numloop)) {
|
||||||
|
rec.disabled = true;
|
||||||
|
}
|
||||||
if (!hasSharedArrays && (type === 'contour' || type === 'lathe')) {
|
if (!hasSharedArrays && (type === 'contour' || type === 'lathe')) {
|
||||||
rec.disabled = true;
|
rec.disabled = true;
|
||||||
}
|
}
|
||||||
|
|
@ -346,7 +387,8 @@ export function opRender() {
|
||||||
let popped = false;
|
let popped = false;
|
||||||
let poprec = env.popOp[rec.type];
|
let poprec = env.popOp[rec.type];
|
||||||
if (type === 'index' && indexing && !rec.disabled) {
|
if (type === 'index' && indexing && !rec.disabled) {
|
||||||
index = rec.absolute ? rec.degrees : index + rec.degrees;
|
let numloop = rec._numloop ?? 1;
|
||||||
|
index = rec.absolute ? rec.degrees : index + (rec.degrees * numloop);
|
||||||
}
|
}
|
||||||
el.rec = rec;
|
el.rec = rec;
|
||||||
el.unpop = () => {
|
el.unpop = () => {
|
||||||
|
|
@ -529,10 +571,29 @@ export function opRender() {
|
||||||
updateStock();
|
updateStock();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function zPlaneShow({ which, value }) {
|
||||||
|
if (!value) return;
|
||||||
|
if (which === 'top') showZTop(value); else
|
||||||
|
if (which === 'bottom') showZBottom(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function zPlaneHide({ which }) {
|
||||||
|
updateStock();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function zPlaneSelect({ which, onselect }) {
|
||||||
|
if (zPlaneSelecting()) {
|
||||||
|
return zPlaneDone();
|
||||||
|
}
|
||||||
|
clearPops();
|
||||||
|
zPlaneStart(which, value => {
|
||||||
|
onselect(parseFloat(value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function init() {
|
export function init() {
|
||||||
|
|
||||||
api.event.on('tool.mesh.face-normal', normal => {
|
api.event.on('tool.mesh.face-normal', normal => {
|
||||||
// console.log({ env.poppedRec });
|
|
||||||
env.poppedRec.degrees = (Math.atan2(normal.y, normal.z) * RAD2DEG).round(2);
|
env.poppedRec.degrees = (Math.atan2(normal.y, normal.z) * RAD2DEG).round(2);
|
||||||
env.poppedRec.absolute = true;
|
env.poppedRec.absolute = true;
|
||||||
opRender();
|
opRender();
|
||||||
|
|
@ -747,14 +808,13 @@ export function init() {
|
||||||
};
|
};
|
||||||
|
|
||||||
$('op-add-list').onclick = (ev) => {
|
$('op-add-list').onclick = (ev) => {
|
||||||
let settings = api.conf.get();
|
|
||||||
let { process, device } = settings;
|
|
||||||
switch (ev.target.innerText.toLowerCase()) {
|
switch (ev.target.innerText.toLowerCase()) {
|
||||||
case "index": return opAddIndex();
|
case "index": return opAddIndex();
|
||||||
case "laser on": return opAddLaserOn();
|
case "laser on": return opAddLaserOn();
|
||||||
case "laser off": return opAddLaserOff();
|
case "laser off": return opAddLaserOff();
|
||||||
case "gcode": return opAddGCode();
|
case "gcode": return opAddGCode();
|
||||||
case "level": return opAddLevel();
|
case "level": return opAddLevel();
|
||||||
|
case "loop": return opAddLoop();
|
||||||
case "rough": return opAddRough();
|
case "rough": return opAddRough();
|
||||||
case "outline": return opAddOutline();
|
case "outline": return opAddOutline();
|
||||||
case "contour":
|
case "contour":
|
||||||
|
|
@ -792,8 +852,24 @@ export function init() {
|
||||||
|
|
||||||
// TAB/TRACE BUTTON HANDLERS
|
// TAB/TRACE BUTTON HANDLERS
|
||||||
api.event.on("button.click", target => {
|
api.event.on("button.click", target => {
|
||||||
let process = api.conf.get().process;
|
let { process, stock } = api.conf.get();
|
||||||
switch (target) {
|
switch (target) {
|
||||||
|
case api.ui.stockPlace:
|
||||||
|
api.uc.prompt("stock x,y,z position", "").then(csv => {
|
||||||
|
if (csv) {
|
||||||
|
let [x,y,z] = csv.split(',').map(v => parseInt(v) || 0);
|
||||||
|
let center = {
|
||||||
|
x: x ?? 0,
|
||||||
|
y: y ?? 0,
|
||||||
|
z: z ?? 0
|
||||||
|
}
|
||||||
|
// move widgets based on postion
|
||||||
|
// more complex than just moving stock
|
||||||
|
// because stock is based on contents, not the other way
|
||||||
|
console.log({ x,y,z });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
case api.ui.tabAdd:
|
case api.ui.tabAdd:
|
||||||
return tabAdd();
|
return tabAdd();
|
||||||
case api.ui.tabDun:
|
case api.ui.tabDun:
|
||||||
|
|
@ -806,17 +882,20 @@ export function init() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
api.event.on("cam.op.add", opAdd);
|
api.event
|
||||||
|
.on("cam.op.add", opAdd)
|
||||||
|
.on("cam.op.del", opDel)
|
||||||
|
.on("cam.op.render", opRender);
|
||||||
|
|
||||||
api.event.on("cam.op.del", opDel);
|
api.event
|
||||||
|
.on("cam.tabs.add", tabAdd)
|
||||||
|
.on("cam.tabs.done", tabDone)
|
||||||
|
.on("cam.tabs.clear", tabClear);
|
||||||
|
|
||||||
api.event.on("cam.op.render", opRender);
|
api.event
|
||||||
|
.on("cam.zplane.show", zPlaneShow)
|
||||||
api.event.on("cam.tabs.add", tabAdd);
|
.on("cam.zplane.hide", zPlaneHide)
|
||||||
|
.on("cam.zplane.select", zPlaneSelect);
|
||||||
api.event.on("cam.tabs.done", tabDone);
|
|
||||||
|
|
||||||
api.event.on("cam.tabs.clear", tabClear);
|
|
||||||
|
|
||||||
// COMMON TAB/TRACE EVENT HANDLERS
|
// COMMON TAB/TRACE EVENT HANDLERS
|
||||||
api.event.on("slice.begin", () => {
|
api.event.on("slice.begin", () => {
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue