Merge branch 'rel-next'
This commit is contained in:
commit
c2eca07ed2
170 changed files with 36860 additions and 2409 deletions
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
|
@ -11,7 +11,7 @@ jobs:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
os: [ubuntu-latest, macos-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
|
|
||||||
25
.github/workflows/prettier-check.yml
vendored
25
.github/workflows/prettier-check.yml
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
name: 'Docs Fomatting Check'
|
name: 'Docs Formatting Check'
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
|
|
@ -12,12 +12,19 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Setup bun
|
- name: Setup Node
|
||||||
uses: oven-sh/setup-bun@v2
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
bun-version: latest
|
node-version: '20'
|
||||||
- name: Install dependencies
|
- name: Check docs formatting (retry once on transient network failure)
|
||||||
run: bun install
|
shell: bash
|
||||||
- name: check format for docs
|
run: |
|
||||||
run:
|
for attempt in 1 2; do
|
||||||
bun docs-check
|
npx --yes prettier@3.5.3 --config ./conf/prettier.config.js ./docs --check && exit 0
|
||||||
|
if [ "$attempt" -lt 2 ]; then
|
||||||
|
echo "Prettier check failed (attempt $attempt). Retrying..."
|
||||||
|
sleep 5
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "Prettier check failed after retries."
|
||||||
|
exit 1
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -17,6 +17,7 @@ package-lock.json
|
||||||
src/ext/jszip-esm.js
|
src/ext/jszip-esm.js
|
||||||
src/ext/quickjs.js
|
src/ext/quickjs.js
|
||||||
src/ext/three.js
|
src/ext/three.js
|
||||||
|
src/void/solver
|
||||||
tmp
|
tmp
|
||||||
tmp_*/
|
tmp_*/
|
||||||
web/boot/bundle*
|
web/boot/bundle*
|
||||||
|
|
|
||||||
22
app-el.js
22
app-el.js
|
|
@ -14,6 +14,23 @@ const devel = process.argv.slice(2).map(v => v.replaceAll('-', '')).indexOf('dev
|
||||||
|
|
||||||
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = true;
|
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = true;
|
||||||
|
|
||||||
|
// Enable GPU/graphics acceleration for Linux (addresses WebGL issues)
|
||||||
|
// Equivalent to Chrome flags that fixed the rendering in Chrome browser
|
||||||
|
if (process.platform === 'linux') {
|
||||||
|
// CRITICAL: Override GPU blocklist - allows hardware acceleration on blocked GPUs
|
||||||
|
app.commandLine.appendSwitch('ignore-gpu-blocklist');
|
||||||
|
|
||||||
|
// Enable WebGL draft extensions (improves WebGL compatibility)
|
||||||
|
app.commandLine.appendSwitch('enable-webgl-draft-extensions');
|
||||||
|
|
||||||
|
// Force GPU acceleration for 2D/3D rendering
|
||||||
|
app.commandLine.appendSwitch('enable-gpu-rasterization');
|
||||||
|
|
||||||
|
// Optional: Try Vulkan if available (Chromium auto-falls back to OpenGL if not)
|
||||||
|
// Uncomment if you need Vulkan specifically, but most systems work without it:
|
||||||
|
app.commandLine.appendSwitch('enable-features', 'Vulkan');
|
||||||
|
}
|
||||||
|
|
||||||
server({
|
server({
|
||||||
port: 5309,
|
port: 5309,
|
||||||
apps: basDir,
|
apps: basDir,
|
||||||
|
|
@ -61,12 +78,15 @@ function createWindow() {
|
||||||
if (url.endsWith('/mesh') || url.endsWith('/mesh/')) {
|
if (url.endsWith('/mesh') || url.endsWith('/mesh/')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (url.endsWith('/void') || url.endsWith('/void/')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
shell.openExternal(url);
|
shell.openExternal(url);
|
||||||
});
|
});
|
||||||
|
|
||||||
webContents.on('did-finish-load', () => {
|
webContents.on('did-finish-load', () => {
|
||||||
mainWindow.webContents.executeJavaScript(`{ let x = document.getElementById('app-quit'); if (x) { x.onclick = () => window.close() } }; null;`);
|
// console.log('did finish load');
|
||||||
});
|
});
|
||||||
|
|
||||||
if (devel) {
|
if (devel) {
|
||||||
|
|
|
||||||
59
app.js
59
app.js
|
|
@ -26,6 +26,7 @@ const mods = {};
|
||||||
const load = [];
|
const load = [];
|
||||||
const api = {};
|
const api = {};
|
||||||
|
|
||||||
|
let lastTouchTime = {};
|
||||||
let forceUseCache = false;
|
let forceUseCache = false;
|
||||||
let serviceWorker = true;
|
let serviceWorker = true;
|
||||||
let crossOrigin = false;
|
let crossOrigin = false;
|
||||||
|
|
@ -134,10 +135,12 @@ function init(mod) {
|
||||||
"/boot" : redir((pre??"") + "/boot/", 301),
|
"/boot" : redir((pre??"") + "/boot/", 301),
|
||||||
"/kiri" : redir((pre??"") + "/kiri/", 301),
|
"/kiri" : redir((pre??"") + "/kiri/", 301),
|
||||||
"/mesh" : redir((pre??"") + "/mesh/", 301),
|
"/mesh" : redir((pre??"") + "/mesh/", 301),
|
||||||
"/meta" : redir((pre??"") + "/meta/", 301),
|
"/void" : redir((pre??"") + "/void/", 301),
|
||||||
|
"/form" : redir((pre??"") + "/form/", 301),
|
||||||
"/kiri/index.html" : redir((pre??"") + "/kiri/", 301),
|
"/kiri/index.html" : redir((pre??"") + "/kiri/", 301),
|
||||||
"/mesh/index.html" : redir((pre??"") + "/mesh/", 301),
|
"/mesh/index.html" : redir((pre??"") + "/mesh/", 301),
|
||||||
"/meta/index.html" : redir((pre??"") + "/meta/", 301)
|
"/void/index.html" : redir((pre??"") + "/void/", 301),
|
||||||
|
"/form/index.html" : redir((pre??"") + "/form/", 301)
|
||||||
}));
|
}));
|
||||||
mod.add(handleVersion);
|
mod.add(handleVersion);
|
||||||
mod.add(fixedmap("/api/", api));
|
mod.add(fixedmap("/api/", api));
|
||||||
|
|
@ -153,12 +156,15 @@ function init(mod) {
|
||||||
mod.static("/lib/", "alt");
|
mod.static("/lib/", "alt");
|
||||||
mod.static("/lib/", "src");
|
mod.static("/lib/", "src");
|
||||||
mod.static("/obj/", "web/obj");
|
mod.static("/obj/", "web/obj");
|
||||||
mod.static("/font/", "web/font");
|
mod.static("/boot/", "web/boot");
|
||||||
mod.static("/fon2/", "web/fon2");
|
mod.static("/fon2/", "web/fon2");
|
||||||
|
mod.static("/font/", "web/font");
|
||||||
|
mod.static("/form/", "web/void");
|
||||||
|
mod.static("/icon/", "web/icon");
|
||||||
|
mod.static("/kiri/", "web/kiri");
|
||||||
mod.static("/mesh/", "web/mesh");
|
mod.static("/mesh/", "web/mesh");
|
||||||
mod.static("/moto/", "web/moto");
|
mod.static("/moto/", "web/moto");
|
||||||
mod.static("/kiri/", "web/kiri");
|
mod.static("/void/", "web/void");
|
||||||
mod.static("/boot/", "web/boot");
|
|
||||||
|
|
||||||
// module loader
|
// module loader
|
||||||
function load_modules(root, force) {
|
function load_modules(root, force) {
|
||||||
|
|
@ -186,10 +192,10 @@ function init(mod) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// load development and 3rd party modules
|
// load development and app modules (onshape, thingiverse)
|
||||||
load_modules('mod');
|
load_modules('mod');
|
||||||
|
|
||||||
// load optional local modules
|
// load optional local modules (bambu)
|
||||||
load_modules('mods');
|
load_modules('mods');
|
||||||
|
|
||||||
// run load functions injected by modules
|
// run load functions injected by modules
|
||||||
|
|
@ -208,9 +214,19 @@ function init(mod) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// create alt artifacts with module extensions
|
// synthesize new main when applicable
|
||||||
|
createArtifacts();
|
||||||
|
}
|
||||||
|
|
||||||
|
// create alt artifacts with module extensions
|
||||||
|
function createArtifacts() {
|
||||||
if (dryrun || !isElectron) {
|
if (dryrun || !isElectron) {
|
||||||
logger.log('creating artifacts', Object.keys(append));
|
if (debug) {
|
||||||
|
setTimeout(createArtifacts, 1000);
|
||||||
|
}
|
||||||
|
if (Object.keys(lastTouchTime).length === 0) {
|
||||||
|
logger.log('creating artifacts', Object.keys(append));
|
||||||
|
}
|
||||||
for (let [ key, val ] of Object.entries(append)) {
|
for (let [ key, val ] of Object.entries(append)) {
|
||||||
// append mains
|
// append mains
|
||||||
let src = `${dir}/src/main/${key}.js`;
|
let src = `${dir}/src/main/${key}.js`;
|
||||||
|
|
@ -218,6 +234,14 @@ function init(mod) {
|
||||||
logger.log('missing', src);
|
logger.log('missing', src);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
let ltt = fs.statSync(src).mtimeMs;
|
||||||
|
if (lastTouchTime[src] === ltt) {
|
||||||
|
continue;
|
||||||
|
} else if (debug) {
|
||||||
|
logger.log('changed', src);
|
||||||
|
}
|
||||||
|
lastTouchTime[src] = ltt;
|
||||||
|
// console.log({ src, ltt });
|
||||||
fs.mkdirSync(`${dir}/alt/main`, { recursive: true });
|
fs.mkdirSync(`${dir}/alt/main`, { recursive: true });
|
||||||
let body = fs.readFileSync(src);
|
let body = fs.readFileSync(src);
|
||||||
fs.writeFileSync(`${dir}/alt/main/${key}.js`, body + val);
|
fs.writeFileSync(`${dir}/alt/main/${key}.js`, body + val);
|
||||||
|
|
@ -234,7 +258,7 @@ function init(mod) {
|
||||||
} else {
|
} else {
|
||||||
logger.log('skipping artifacts');
|
logger.log('skipping artifacts');
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// either add module assets to path or require(init.js)
|
// either add module assets to path or require(init.js)
|
||||||
function loadModule(mod, dir) {
|
function loadModule(mod, dir) {
|
||||||
|
|
@ -257,7 +281,7 @@ function initModule(mod, file, dir) {
|
||||||
logger.log({ module: file, dir });
|
logger.log({ module: file, dir });
|
||||||
require_fresh(file)({
|
require_fresh(file)({
|
||||||
// express functions added here show up at "/api/" url root
|
// express functions added here show up at "/api/" url root
|
||||||
api: api,
|
api,
|
||||||
adm: {
|
adm: {
|
||||||
setver(ver) { oversion = ver },
|
setver(ver) { oversion = ver },
|
||||||
crossOrigin(bool) { crossOrigin = bool }
|
crossOrigin(bool) { crossOrigin = bool }
|
||||||
|
|
@ -329,8 +353,10 @@ function initModule(mod, file, dir) {
|
||||||
const path = mod.dir + '/' + dir + '/' + file;
|
const path = mod.dir + '/' + dir + '/' + file;
|
||||||
try {
|
try {
|
||||||
const body = fs.readFileSync(path);
|
const body = fs.readFileSync(path);
|
||||||
if (debug && !single) logger.log({ inject: code, file, opt });
|
if (debug && !single) {
|
||||||
if (opt.first) {
|
logger.log({ inject: code, file, opt });
|
||||||
|
}
|
||||||
|
if (opt.first && append[code]) {
|
||||||
append[code] = body.toString() + '\n' + append[code];
|
append[code] = body.toString() + '\n' + append[code];
|
||||||
} else {
|
} else {
|
||||||
append[code] += body.toString() + '\n';
|
append[code] += body.toString() + '\n';
|
||||||
|
|
@ -369,8 +395,11 @@ function handleSetup(req, res, next) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const productionMap = {
|
const productionMap = {
|
||||||
'/lib/mesh/work.js' : '/lib/pack/mesh-work.js',
|
'/lib/main/void.js' : '/lib/pack/void-main.js',
|
||||||
|
'/lib/main/planegcs.wasm' : '/lib/void/solver/planegcs_dist/planegcs.wasm',
|
||||||
|
'/lib/worker/solids_worker.js' : '/lib/pack/void-work-solid.js',
|
||||||
'/lib/main/mesh.js' : '/lib/pack/mesh-main.js',
|
'/lib/main/mesh.js' : '/lib/pack/mesh-main.js',
|
||||||
|
'/lib/mesh/work.js' : '/lib/pack/mesh-work.js',
|
||||||
'/lib/main/kiri.js' : '/lib/pack/kiri-main.js',
|
'/lib/main/kiri.js' : '/lib/pack/kiri-main.js',
|
||||||
'/lib/kiri/run/engine.js' : '/lib/pack/kiri-eng.js',
|
'/lib/kiri/run/engine.js' : '/lib/pack/kiri-eng.js',
|
||||||
'/lib/kiri/run/minion.js' : '/lib/pack/kiri-pool.js',
|
'/lib/kiri/run/minion.js' : '/lib/pack/kiri-pool.js',
|
||||||
|
|
@ -487,7 +516,7 @@ function ifModifiedDate(req) {
|
||||||
|
|
||||||
function addCorsHeaders(req, res) {
|
function addCorsHeaders(req, res) {
|
||||||
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
||||||
res.setHeader('Access-Control-Allow-Headers', 'X-Moto-Ajax, Content-Type');
|
res.setHeader('Access-Control-Allow-Headers', 'X-Api-Key, X-Host, X-Moto-Ajax, Content-Type');
|
||||||
res.setHeader('Access-Control-Allow-Origin', req.headers['origin'] || '*');
|
res.setHeader('Access-Control-Allow-Origin', req.headers['origin'] || '*');
|
||||||
if (req.headers['access-control-request-private-network'] === 'true') {
|
if (req.headers['access-control-request-private-network'] === 'true') {
|
||||||
res.setHeader('Access-Control-Allow-Private-Network', 'true');
|
res.setHeader('Access-Control-Allow-Private-Network', 'true');
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,8 @@
|
||||||
{ "src": "src/pack/kiri-pool.js", "dst": "lib/kiri/run/minion.js" },
|
{ "src": "src/pack/kiri-pool.js", "dst": "lib/kiri/run/minion.js" },
|
||||||
{ "src": "src/gpu/raster-worker.js", "dst": "lib/gpu/raster-worker.js" },
|
{ "src": "src/gpu/raster-worker.js", "dst": "lib/gpu/raster-worker.js" },
|
||||||
{ "src": "src/ext/tween.js", "dst": "lib/ext/tween.js" },
|
{ "src": "src/ext/tween.js", "dst": "lib/ext/tween.js" },
|
||||||
{ "src": "src/moto/license.js", "dst": "lib/moto/license.js" }
|
{ "src": "src/moto/license.js", "dst": "lib/moto/license.js" },
|
||||||
|
{ "src": "src/void/solver/planegcs_dist/planegcs.wasm", "dst": "lib/main/planegcs.wasm" }
|
||||||
],
|
],
|
||||||
"excludes": [
|
"excludes": [
|
||||||
"src/pack",
|
"src/pack",
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@ const isProd = mode === 'prod';
|
||||||
|
|
||||||
console.log(`Building in ${mode} mode...`);
|
console.log(`Building in ${mode} mode...`);
|
||||||
|
|
||||||
|
const VOID_OUTFILE = 'src/pack/void-main.js';
|
||||||
|
const VOID_EXTRAS = [ ];
|
||||||
|
|
||||||
const MESH_OUTFILE = 'src/pack/mesh-main.js';
|
const MESH_OUTFILE = 'src/pack/mesh-main.js';
|
||||||
const MESH_EXTRAS = [ ];
|
const MESH_EXTRAS = [ ];
|
||||||
|
|
||||||
|
|
@ -29,7 +32,7 @@ async function appendExtraModules(extras, outfile, minify = false) {
|
||||||
const result = await transform(code, {
|
const result = await transform(code, {
|
||||||
minify: true,
|
minify: true,
|
||||||
loader: 'js',
|
loader: 'js',
|
||||||
target: 'es2020',
|
target: 'es2022',
|
||||||
});
|
});
|
||||||
return result.code;
|
return result.code;
|
||||||
})
|
})
|
||||||
|
|
@ -92,7 +95,7 @@ const rec = {
|
||||||
minify: isProd, // false for dev, true for prod
|
minify: isProd, // false for dev, true for prod
|
||||||
platform: 'browser',
|
platform: 'browser',
|
||||||
sourcemap: false,
|
sourcemap: false,
|
||||||
target: 'es2020',
|
target: 'es2022',
|
||||||
};
|
};
|
||||||
|
|
||||||
async function buildApp() {
|
async function buildApp() {
|
||||||
|
|
@ -100,6 +103,20 @@ async function buildApp() {
|
||||||
// Concatenate kiri devices
|
// Concatenate kiri devices
|
||||||
generateDevices();
|
generateDevices();
|
||||||
|
|
||||||
|
// Bundle void main app
|
||||||
|
await build(Object.assign({}, rec, {
|
||||||
|
entryPoints: [ 'src/main/void.js' ],
|
||||||
|
outfile: VOID_OUTFILE,
|
||||||
|
}));
|
||||||
|
|
||||||
|
appendExtraModules(VOID_EXTRAS, VOID_OUTFILE, isProd);
|
||||||
|
|
||||||
|
// Bundle void worker
|
||||||
|
await build(Object.assign({}, rec, {
|
||||||
|
entryPoints: [ 'src/void/worker/solids_worker.js' ],
|
||||||
|
outfile: 'src/pack/void-work-solid.js',
|
||||||
|
}));
|
||||||
|
|
||||||
// Bundle mesh main app
|
// Bundle mesh main app
|
||||||
await build(Object.assign({}, rec, {
|
await build(Object.assign({}, rec, {
|
||||||
entryPoints: [ 'src/main/mesh.js' ],
|
entryPoints: [ 'src/main/mesh.js' ],
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { LineMaterial } from '../node_modules/three/examples/jsm/lines/LineMater
|
||||||
import { LineGeometry } from '../node_modules/three/examples/jsm/lines/LineGeometry.js';
|
import { LineGeometry } from '../node_modules/three/examples/jsm/lines/LineGeometry.js';
|
||||||
import { LineSegments2 } from '../node_modules/three/examples/jsm/lines/LineSegments2.js';
|
import { LineSegments2 } from '../node_modules/three/examples/jsm/lines/LineSegments2.js';
|
||||||
import { LineSegmentsGeometry } from '../node_modules/three/examples/jsm/lines/LineSegmentsGeometry.js';
|
import { LineSegmentsGeometry } from '../node_modules/three/examples/jsm/lines/LineSegmentsGeometry.js';
|
||||||
|
import { TrackballControls } from '../node_modules/three/examples/jsm/controls/TrackballControls.js';
|
||||||
|
|
||||||
import * as MeshBVHLib from '../node_modules/three-mesh-bvh/build/index.module.js';
|
import * as MeshBVHLib from '../node_modules/three-mesh-bvh/build/index.module.js';
|
||||||
|
|
||||||
|
|
@ -19,5 +20,6 @@ export {
|
||||||
LineGeometry,
|
LineGeometry,
|
||||||
LineSegments2,
|
LineSegments2,
|
||||||
LineSegmentsGeometry,
|
LineSegmentsGeometry,
|
||||||
|
TrackballControls,
|
||||||
MeshBVHLib
|
MeshBVHLib
|
||||||
};
|
};
|
||||||
|
|
@ -1,17 +1,19 @@
|
||||||
src/ext/gerber.js,../../node_modules/@tracespace/parser/umd/parser.js
|
src//gpu/raster.js,../../node_modules/@gridspace/raster-path/build/raster-path.js
|
||||||
src/ext/manifold.js,../../node_modules/manifold-3d/manifold.js
|
src//gpu/raster-worker.js,../../node_modules/@gridspace/raster-path/build/raster-worker.js
|
||||||
src/ext/base64.js,../../node_modules/base64-js/base64js.min.js
|
src//ext/gerber.js,../../node_modules/@tracespace/parser/umd/parser.js
|
||||||
src/ext/earcut.js,../../node_modules/earcut/src/earcut.js
|
src//ext/manifold.js,../../node_modules/manifold-3d/manifold.js
|
||||||
src/ext/tween.js,../../node_modules/@tweenjs/tween.js/src/Tween.js
|
src//ext/base64.js,../../node_modules/base64-js/base64js.min.js
|
||||||
src/ext/jszip.js,../../node_modules/jszip/dist/jszip.js
|
src//ext/earcut.js,../../node_modules/earcut/src/earcut.js
|
||||||
src/wasm/manifold.wasm,../../node_modules/manifold-3d/manifold.wasm
|
src//ext/tween.js,../../node_modules/@tweenjs/tween.js/src/Tween.js
|
||||||
src/kiri/lang-en.js,../../web/kiri/lang/en.js
|
src//ext/jszip.js,../../node_modules/jszip/dist/jszip.js
|
||||||
web/fon2,../node_modules/bootstrap-icons/font/
|
src//wasm/manifold.wasm,../../node_modules/manifold-3d/manifold.wasm
|
||||||
web/kiri/lang/pl.js,pl-pl.js
|
src//kiri/app/lang-en.js,../../../web/kiri/lang/en.js
|
||||||
web/kiri/lang/pt-pt.js,pt.js
|
web//fon2,../node_modules/bootstrap-icons/font/
|
||||||
web/kiri/lang/da-dk.js,da.js
|
web//kiri/lang/pl.js,pl-pl.js
|
||||||
web/kiri/lang/en-us.js,en.js
|
web//kiri/lang/pt-pt.js,pt.js
|
||||||
web/kiri/lang/fr-fr.js,fr.js
|
web//kiri/lang/da-dk.js,da.js
|
||||||
web/kiri/lang/de.js,de-de.js
|
web//kiri/lang/en-us.js,en.js
|
||||||
web/kiri/lang/es.js,es-es.js
|
web//kiri/lang/fr-fr.js,fr.js
|
||||||
web/font,../node_modules/@fortawesome/fontawesome-free/
|
web//kiri/lang/de.js,de-de.js
|
||||||
|
web//kiri/lang/es.js,es-es.js
|
||||||
|
web//font,../node_modules/@fortawesome/fontawesome-free/
|
||||||
|
|
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
|
|
||||||
# Contributing
|
|
||||||
|
|
||||||
This is a community driven project, and we welcome any contributions you'd like to make. You can connect with us on [discord](https://discord.gg/suyCCgr).
|
|
||||||
|
|
||||||
|
|
||||||
## Running Locally
|
|
||||||
|
|
||||||
- Ensure you have [node](https://nodejs.org/en/download/), or an equivalent installed
|
|
||||||
- Clone the [repository](https://github.com/GridSpace/grid-apps) from https://github.com/GridSpace/grid-apps
|
|
||||||
- `npm run setup`
|
|
||||||
- `npm run dev`
|
|
||||||
- Open browser [http://localhost:8080/kiri](http://localhost:8080/kiri)
|
|
||||||
|
|
||||||
|
|
||||||
## How to add a new machine
|
|
||||||
|
|
||||||
- Make sure you have your tested machine selected
|
|
||||||
- Open the developer console
|
|
||||||
- Run the following code: `kiri.api.conf.get().device`
|
|
||||||
- Right click on the object and select `Copy object`
|
|
||||||
- Make a new file in the `src/kiri/dev/<mode>` directory, with the name of your machine, no spaces or special characters, and a `.json` extension.
|
|
||||||
- Paste the copied object into the new file, and save it.
|
|
||||||
- Publish your changes to a git repo
|
|
||||||
- Submit a [pull request](https://github.com/GridSpace/grid-apps/compare)
|
|
||||||
960
docs/agents.md
Normal file
960
docs/agents.md
Normal file
|
|
@ -0,0 +1,960 @@
|
||||||
|
# AI Agent TL;DR - gs-apps
|
||||||
|
|
||||||
|
Quick reference for AI agents working on this project.
|
||||||
|
|
||||||
|
## What Is This?
|
||||||
|
|
||||||
|
**gs-apps** is a monorepo containing three Grid.Space web applications:
|
||||||
|
|
||||||
|
| App | Purpose | Status | Entry Point |
|
||||||
|
| ------------- | ----------------------------- | ---------- | ------------------ |
|
||||||
|
| **kiri:moto** | Multi-axis CNC/FDM/SLA slicer | Production | `src/main/kiri.js` |
|
||||||
|
| **mesh:tool** | 3D mesh editor & repair | Active dev | `src/main/mesh.js` |
|
||||||
|
| **void:form** | Parametric CAD modeler | Phase 1 | `src/main/void.js` |
|
||||||
|
|
||||||
|
All three share common infrastructure in `src/moto/`, `src/geo/`, `src/load/`, and `src/ext/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. KIRI:MOTO - CNC/FDM/SLA Slicer
|
||||||
|
|
||||||
|
### Purpose
|
||||||
|
|
||||||
|
Multi-mode manufacturing tool for slicing 3D models for CNC milling, 3D printing, laser cutting, SLA, wire EDM, and waterjet.
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── main/kiri.js # Bootstrap entry point (2.3KB)
|
||||||
|
├── kiri/
|
||||||
|
│ ├── app/ # Application layer (45 modules)
|
||||||
|
│ │ ├── api.js # Main API surface (~10KB)
|
||||||
|
│ │ ├── platform.js # Platform/printer setup (~40KB)
|
||||||
|
│ │ ├── inputs.js # UI input handling (~30KB)
|
||||||
|
│ │ ├── paint.js # Viewport rendering (~23KB)
|
||||||
|
│ │ ├── widget.js # Core slicing widget (~12KB)
|
||||||
|
│ │ ├── devices.js # Machine definitions
|
||||||
|
│ │ └── conf/ # Device/process configs
|
||||||
|
│ ├── core/ # Engine core (7 modules)
|
||||||
|
│ │ ├── codec.js # Data encoding/decoding
|
||||||
|
│ │ ├── print.js # Print/slice object (~30KB)
|
||||||
|
│ │ ├── slice.js # Slicing logic
|
||||||
|
│ │ └── widget.js # Widget manipulation (~30KB)
|
||||||
|
│ ├── mode/ # Machine implementations (7 types)
|
||||||
|
│ │ ├── cam/ # CNC/CAM milling
|
||||||
|
│ │ ├── fdm/ # 3D printing (FDM/FFF)
|
||||||
|
│ │ ├── laser/ # Laser cutting/engraving
|
||||||
|
│ │ ├── sla/ # Resin printing (SLA)
|
||||||
|
│ │ ├── drag/ # Drag operations
|
||||||
|
│ │ ├── wedm/ # Wire EDM cutting
|
||||||
|
│ │ └── wjet/ # Water jet cutting
|
||||||
|
│ └── run/ # Worker/threading (5 modules)
|
||||||
|
│ ├── worker.js # Worker orchestration (~25KB)
|
||||||
|
│ ├── engine.js # Engine execution (~6KB)
|
||||||
|
│ └── minion.js # Worker pool (~10KB)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
|
||||||
|
- **Multi-threaded slicing**: Web Worker pool (up to 4 minions)
|
||||||
|
- **Multiple modes**: CAM, FDM, LASER, SLA, WEDM, WJET
|
||||||
|
- **Device profiles**: JSON-based machine configs (`src/cli/`)
|
||||||
|
- **Widget-based**: Objects as "widgets" for slicing operations
|
||||||
|
- **Tabs interface**: Multi-document workspace
|
||||||
|
|
||||||
|
### Routes
|
||||||
|
|
||||||
|
- `/kiri/` - Main slicer interface
|
||||||
|
- `/lib/pack/kiri-main.js` - Main bundle (~28KB minified)
|
||||||
|
- `/lib/pack/kiri-work.js` - Worker bundle
|
||||||
|
- `/lib/pack/kiri-eng.js` - Engine bundle
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Full docs: `/Users/stewart/Code/gs-apps/docs/kiri-moto/`
|
||||||
|
- API reference: `/Users/stewart/Code/gs-apps/docs/kiri-moto/apis.md`
|
||||||
|
|
||||||
|
### Database (IndexedDB)
|
||||||
|
|
||||||
|
- Device profiles, process settings, print history
|
||||||
|
- Workspace restoration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. MESH:TOOL - 3D Mesh Editor
|
||||||
|
|
||||||
|
### Purpose
|
||||||
|
|
||||||
|
Direct 3D mesh editing, boolean operations, mesh repair, face/edge selection, and 2D sketch system.
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── main/mesh.js # Bootstrap entry point (29KB)
|
||||||
|
└── mesh/
|
||||||
|
├── api.js # Main API surface (~1,730 lines)
|
||||||
|
├── build.js # UI builder (~42KB)
|
||||||
|
├── model.js # Mesh model class (~26KB)
|
||||||
|
├── group.js # Group/assembly (~4KB)
|
||||||
|
├── tool.js # Tool operations (~35KB)
|
||||||
|
├── work.js # Worker communication (~19KB)
|
||||||
|
├── sketch.js # 2D sketch mode (~22KB)
|
||||||
|
├── handles.js # Manipulation handles (~9KB)
|
||||||
|
├── edges.js # Edge visualization (~6KB)
|
||||||
|
├── history.js # Undo/redo system
|
||||||
|
└── util.js # Utilities (~9KB)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
|
||||||
|
- **Mode-based UI**: Object, Tool, Face, Surface, Edge, Sketch modes
|
||||||
|
- **Boolean operations**: Union, intersect, difference (Manifold WASM)
|
||||||
|
- **Mesh repair**: Heal, clean, triangulate
|
||||||
|
- **Face/edge selection**: Direct geometry manipulation
|
||||||
|
- **2D sketching**: Sketch on 3D planes
|
||||||
|
- **Group management**: Assemblies and hierarchy
|
||||||
|
- **Undo/redo**: Full history system
|
||||||
|
|
||||||
|
### UI Components
|
||||||
|
|
||||||
|
- Feature tree (left panel)
|
||||||
|
- Mode buttons (object/tool/face/surface/edge/sketch)
|
||||||
|
- Object properties panel
|
||||||
|
- Wireframe/normals visualization
|
||||||
|
|
||||||
|
### Routes
|
||||||
|
|
||||||
|
- `/mesh/` - Main mesh editor
|
||||||
|
- `/lib/pack/mesh-main.js` - Main bundle
|
||||||
|
- `/lib/pack/mesh-work.js` - Worker bundle
|
||||||
|
|
||||||
|
### Database (IndexedDB)
|
||||||
|
|
||||||
|
- `admin` store - Metadata, preferences, cache
|
||||||
|
- `space` store - Models, groups, sketches
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- `/Users/stewart/Code/gs-apps/docs/mesh-tool.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. VOID:FORM - Parametric CAD
|
||||||
|
|
||||||
|
### Purpose
|
||||||
|
|
||||||
|
Onshape-inspired parametric CAD with constraint-based sketching, feature history, and BREP operations.
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── main/void.js # Bootstrap entry point (210 lines)
|
||||||
|
└── void/
|
||||||
|
├── api.js # API composition root
|
||||||
|
├── api/
|
||||||
|
│ ├── document.js # Document persistence + revisions/undo/redo
|
||||||
|
│ ├── features.js # Feature list mutations
|
||||||
|
│ ├── origin.js # Origin point visibility/state
|
||||||
|
│ ├── sketch.js # Sketch feature creation scaffold
|
||||||
|
│ ├── sketch_runtime.js # Sketch runtime orchestrator/state
|
||||||
|
│ ├── sketch_runtime_arc.js # Arc/line endpoint + arc sampling helpers
|
||||||
|
│ ├── sketch_runtime_markers.js # Sketch point/arc-center marker builders
|
||||||
|
│ ├── sketch_runtime_profiles.js # Closed-profile detection + fill loops
|
||||||
|
│ └── sketch_runtime_ui.js # Sketch runtime style/preview/glyph UI helpers
|
||||||
|
├── toolbar.js # Top toolbar UI
|
||||||
|
├── tree.js # Tree composition root
|
||||||
|
├── tree/
|
||||||
|
│ ├── model.js # Tree data/section logic
|
||||||
|
│ └── render.js # Tree DOM builders
|
||||||
|
├── overlay.js # 2D/3D tracking overlay
|
||||||
|
├── datum.js # Datum planes (XY, XZ, YZ)
|
||||||
|
├── plane.js # Plane primitive class
|
||||||
|
├── interact.js # Interaction composition root + event wiring
|
||||||
|
├── interact/
|
||||||
|
│ ├── sketch.js # Sketch interaction orchestrator (event flow + mutations)
|
||||||
|
│ ├── sketch_constraints_actions.js # Constraint apply/toggle/delete actions
|
||||||
|
│ ├── sketch_marquee.js # Marquee selection + geometry hit rules
|
||||||
|
│ ├── sketch_pointer.js # Pointer/hover/drag gesture handlers
|
||||||
|
│ ├── sketch_tools.js # Sketch tool mode + keybinding behavior
|
||||||
|
│ ├── sketch_geometry.js # Sketch hit-test/projection/drag geometry helpers
|
||||||
|
│ ├── sketch_constants.js # Shared sketch interaction constants
|
||||||
|
│ ├── planes.js # Plane hover/select/resize + view-normal
|
||||||
|
│ ├── points.js # Point hover/select hit-testing
|
||||||
|
│ ├── selection.js # Shared selection state transitions
|
||||||
|
│ └── targets.js # Sketch target/frame resolution
|
||||||
|
├── sketch_constraints.js # Constraint orchestration (planegcs + post-solve hooks)
|
||||||
|
├── sketch_constraints_fallback.js # Legacy/incremental fallback solver
|
||||||
|
├── sketch_constraints_tangent.js # Tangent constraint solver helpers
|
||||||
|
└── viewcube.js # ViewCube navigation widget (NEW)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
|
||||||
|
- **Feature tree scaffold**: Sidebar structure is present; full history dependency/update graph is not wired yet
|
||||||
|
- **Datum planes**: XY, XZ, YZ reference planes
|
||||||
|
- **Constraint sketching**: integrated (`@salusoft89/planegcs` + fallback solver path)
|
||||||
|
- **Manifold BREP**: planned feature path (extrude/cut/revolve), early stubs today
|
||||||
|
- **Onshape camera**: Left=select, Middle=pan/zoom, Right=rotate
|
||||||
|
- **ViewCube**: 3D navigation widget (top-right corner)
|
||||||
|
- **2D overlay**: SVG overlay for 3D point tracking
|
||||||
|
|
||||||
|
### Status
|
||||||
|
|
||||||
|
**Very early development (Phase 1 foundation, early feature workflow in place)**
|
||||||
|
|
||||||
|
- 3D viewport with Onshape camera controls
|
||||||
|
- Datum planes with interaction
|
||||||
|
- Feature tree with default geometry visibility controls
|
||||||
|
- ViewCube navigation widget
|
||||||
|
- 2D/3D overlay system
|
||||||
|
- Document persistence + revision history with undo/redo
|
||||||
|
- Sketch feature creation scaffold (target plane/face -> sketch feature entry)
|
||||||
|
|
||||||
|
**Current implementation notes (important for agents)**
|
||||||
|
|
||||||
|
- Direct-call architecture in `void:form` (no broker event bus in current runtime path)
|
||||||
|
- `toolbar` wires real actions for docs, camera modes, undo/redo, and sketch creation
|
||||||
|
- `toolbar` now includes a `Preferences` dialog (`⚙`) with persisted runtime tuning:
|
||||||
|
- solid edge loop-promotion threshold (segment count)
|
||||||
|
- solid edge hover/select `Line2` widths
|
||||||
|
- fit padding (perspective + orthographic)
|
||||||
|
- `tree.render()` is still caller-driven for feature mutations; refresh explicitly after non-tree-originated changes
|
||||||
|
- `src/main/void.js` currently enables overlay test primitives with a hardcoded `if (true)` block (debug scaffolding)
|
||||||
|
- `Origin` in void is an overlay point (not `space.platform` origin)
|
||||||
|
- IndexedDB revision store name is `versions` (older notes may still mention `features`)
|
||||||
|
- Feature tree now includes early history controls:
|
||||||
|
- per-feature `suppress/unsuppress`
|
||||||
|
- feature reorder (up/down)
|
||||||
|
- timeline slider (`0..N`) controlling active rebuild prefix
|
||||||
|
- all above are revisioned + undo/redoable
|
||||||
|
- feature creation now inserts at the active timeline marker (`index + 1`) instead of always appending to the end
|
||||||
|
- Sketch runtime currently renders from the active rebuild set (`features.listBuilt()`), not raw full feature list
|
||||||
|
- Open TODO: stabilise dual-tangent sketch behavior (`line` tangent to two circles/arcs with endpoint-on-arc constraints)
|
||||||
|
- Min/max distance constraints are now wired (`circle/arc` vs `point/line/circle-arc`) but still need stability tuning under drag:
|
||||||
|
- current known issue: circle-in-box (`min` to two orthogonal lines) can feel jerky while drag-resizing radius
|
||||||
|
- current implementation favors deterministic branching for line targets; revisit with solver-side branch lock per drag gesture if needed
|
||||||
|
- Known regression history: commit `5093eec4` introduced an overly permissive derived-edge proximity gate (`segLen * 0.35`) in `resolveDerivedEdgeCandidate`; this causes incorrect face/edge picks in sketch derive hover. Keep tight gate (`2.5`) unless replaced with a screen-space metric.
|
||||||
|
- Geometry graph refactor plan is tracked in `docs/void/plan-geomgraph.md` (surfaces + boundaries as canonical entities; solids as derived artifacts).
|
||||||
|
- Derived sketch entity rearchitecture plan is tracked in `docs/void/plan-derived.md` (immutable, rebuild-driven references to upstream geometry).
|
||||||
|
- Terminology (use consistently in code/docs/issues):
|
||||||
|
- `segment`: one boundary edge between two 3D points
|
||||||
|
- `chain`: ordered open polyline of connected segments
|
||||||
|
- `loop`: ordered closed polyline of connected segments
|
||||||
|
- `surface`: bounded face patch on a solid (planar or curved)
|
||||||
|
- `region`: selectable enclosed 2D sketch profile area
|
||||||
|
- TODO (open): de-dup overlapping boundary projections/derives in sketch `Use (u)` flow.
|
||||||
|
- Symptom: side faces on cubes/cylinders/arc-cutouts can project/derive overlapping duplicate lines.
|
||||||
|
- Requirement: de-dup identical segments/chains by geometric equivalence (endpoint tolerance + chain shape/length), not by source face id.
|
||||||
|
- Phase 0 scaffolding status:
|
||||||
|
- new `GeometryStore` API is wired as the single active path (no rollout flags)
|
||||||
|
- Phase 1 in-progress status:
|
||||||
|
- surface/profile/edge hover ranking logic has been extracted into `src/void/interact/selection_resolver.js`
|
||||||
|
- `src/void/interact/planes.js#getPrimarySurfaceHitFromIntersections()` is now a thin delegate to the resolver
|
||||||
|
- current behavior is parity-focused (same thresholds and tie-break order), giving a stable seam for future boundary/surface entity routing
|
||||||
|
- resolver now accepts explicit `mode` + `intents` context (`SELECTION_MODES`, `SELECTION_INTENTS`) while preserving current behavior
|
||||||
|
- resolver candidates now carry passive canonical entity descriptors:
|
||||||
|
- `profile -> region`
|
||||||
|
- `solid-face -> surface`
|
||||||
|
- `solid-edge -> boundary-segment`
|
||||||
|
- resolver now sources canonical face/edge entity ids from solids runtime mappings when available:
|
||||||
|
- face key -> `surface:*` (stable)
|
||||||
|
- edge key -> `segment:*` (stable)
|
||||||
|
- loop edge key -> `boundary:*` (stable)
|
||||||
|
- canonical ids are now primary for selection entity payloads
|
||||||
|
- solids runtime now publishes a passive geometry snapshot into `document.geometry_store` on sync:
|
||||||
|
- surfaces, boundaries, segments, points, regions, topology maps
|
||||||
|
- still read-only; selection and ops remain on legacy paths for parity
|
||||||
|
- sketch profile/area selection now toggles multi by default (no cmd/meta required); clear remains on `space`/`esc`
|
||||||
|
- derive (`u`) path now carries canonical entity metadata for segment-backed sources:
|
||||||
|
- `source.entity.kind = boundary-segment`
|
||||||
|
- `source.entity.id = segment:faceedge:<faceKey>:<segIndex>`
|
||||||
|
- plus `source.face_key` and `source.boundary_segment_id` for migration bridging
|
||||||
|
- extrude profile targets now use `region_id` (canonical key) only in selection + rebuild paths
|
||||||
|
- chamfer edge refs now carry canonical boundary metadata:
|
||||||
|
- `boundary_segment_id` and `entity: { kind: 'boundary-segment', id: 'segment:...' }`
|
||||||
|
- chamfer selection sync/remove resolves through canonical boundary refs
|
||||||
|
- chamfer apply consumes canonical refs (legacy parse path removed)
|
||||||
|
- boolean/solid-op editing paths removed `input.solids` compatibility branches (use `targets/tools` only)
|
||||||
|
- TODO (tracked): during sketch editing, allow `Use (u)` derive from other visible sketch entities
|
||||||
|
|
||||||
|
**Phase 2: Sketch System (Current Workstream)**
|
||||||
|
|
||||||
|
- planegcs constraint solver integration is active
|
||||||
|
- sketch runtime supports point/line/arc/circle/rectangle workflows
|
||||||
|
- sketch mirror mode is now Onshape-style:
|
||||||
|
- select exactly one line as mirror axis, then press `M` (or use Constraints -> Mirror)
|
||||||
|
- while mirror mode is active, clicking sketch entities mirrors them immediately and keeps the axis highlighted
|
||||||
|
- sketch circular pattern mode (new, WIP):
|
||||||
|
- enter from `Pattern -> Circular` with exactly one selected center point (point/origin/arc-center)
|
||||||
|
- while active, clicking sketch entities creates linked circular copies around that center
|
||||||
|
- pattern constraint glyph stays visible, supports drag offset, and double-click edits copy count
|
||||||
|
- deleting the pattern glyph removes the driving pattern constraint and leaves copied geometry unbound
|
||||||
|
- known regression (open): after certain circular-pattern drag operations, some entities become effectively locked/non-movable
|
||||||
|
- observed after moving patterned elements with additional constraints in sketch
|
||||||
|
- likely in drag ownership propagation / fallback solver interaction for `circular_pattern`
|
||||||
|
- status: unresolved, needs focused repro + solver trace
|
||||||
|
- sketch grid pattern mode (new, WIP):
|
||||||
|
- entered via `Pattern -> Grid`, requires exactly one selected sketch point as anchor
|
||||||
|
- creates two construction guide lines (U/V) from anchor with default `horizontal`/`vertical` constraints
|
||||||
|
- renders two always-visible count glyphs (`Hn`, `Vn`) near guide endpoints (double-click to edit counts)
|
||||||
|
- copies are regenerated from source using guide-line vectors (guide constraints can be removed for skewed grids)
|
||||||
|
- known issues (open):
|
||||||
|
- dragging the grid anchor can invert U/V construction line direction unexpectedly
|
||||||
|
- patterned circle dimension behavior is inconsistent between source and clone circles (dimension propagation/ownership)
|
||||||
|
- mirror axis is highlighted purple while mode is active
|
||||||
|
- each subsequently selected sketch entity is mirrored immediately across that axis
|
||||||
|
- `Esc` or `Space` exits mirror mode
|
||||||
|
- constraints currently wired: coincident, point-on-line, fixed, horizontal, vertical, perpendicular, equal, collinear, tangent, arc-center coincident, midpoint, min-distance, max-distance
|
||||||
|
- deferred: Onshape-like under/fully constrained coloring for sketch entities needs a custom per-entity DoF analysis layer on top of planegcs (not directly exposed as per-entity status by solver)
|
||||||
|
- horizontal/vertical can target line entities or a selected point pair
|
||||||
|
- rectangle tools are implemented as constrained line sets:
|
||||||
|
- corner rectangle
|
||||||
|
- center rectangle
|
||||||
|
|
||||||
|
**Phase 3: Feature History Scaffold (in progress)**
|
||||||
|
|
||||||
|
- `extrude` can now be created as a history feature from a selected sketch (tree + document/history plumbing)
|
||||||
|
- 3D solid generation/rebuild is active via Manifold replay (extrude + boolean paths)
|
||||||
|
- timeline/reorder/suppress semantics are active at the feature-history layer before full BREP ops
|
||||||
|
- chamfer scaffolding (phase-1 UI only) is active:
|
||||||
|
- solid-mode edge hover/select now parallels face hover/select
|
||||||
|
- `Chamfer` feature can be created from selected edges (toolbar button + properties dialog with edge list)
|
||||||
|
- geometry mutation/rebuild for chamfer edges is not yet applied (selection/dialog/history plumbing only)
|
||||||
|
|
||||||
|
**Solid Pipeline (new scaffold)**
|
||||||
|
|
||||||
|
- `void` now has a dedicated solid path (separate from `kiri/mesh` CSG wrappers):
|
||||||
|
- `src/void/api/solids.js` (rebuild scheduling + orchestration)
|
||||||
|
- `src/void/solid/kernel.js` (direct Manifold JS initialization/extrude entrypoint)
|
||||||
|
- `src/void/solid/rebuild.js` (feature replay -> generated solids artifacts)
|
||||||
|
- `src/void/solid/provenance.js` (seed provenance model for feature/profile->body mapping)
|
||||||
|
- `src/void/worker/solids_worker.js` (phase-1 compute worker for rebuild replay)
|
||||||
|
- Solids tree should read generated artifacts (`doc.generated.solids`) rather than mirroring feature rows.
|
||||||
|
- Phase-1 worker behavior (current):
|
||||||
|
- main thread builds a compact rebuild snapshot (`builtFeatures`, sketch planes, profile loops)
|
||||||
|
- worker runs feature replay + manifold ops off-main-thread
|
||||||
|
- mesh payload returns as transferable typed arrays (zero-copy `ArrayBuffer` transfer)
|
||||||
|
- if worker fails, runtime falls back to existing main-thread rebuild path
|
||||||
|
- Path forward:
|
||||||
|
- phase-2: incremental suffix replay + cancellation preemption
|
||||||
|
- phase-3: cached per-feature artifacts keyed by input hash
|
||||||
|
- phase-4: worker pool for independent heavy ops (exports/tessellation), keeping deterministic rebuild order
|
||||||
|
|
||||||
|
**Sketch MVP Contract (checkpointed, 2026-02-06)**
|
||||||
|
|
||||||
|
- Primitive rollout:
|
||||||
|
- v1: `point` + `line`
|
||||||
|
- `arc` implemented
|
||||||
|
- `circle` implemented (internal circle-mode arc representation)
|
||||||
|
- Rectangle is not a primitive; model as constrained lines (corner/center patterns)
|
||||||
|
- Input behavior:
|
||||||
|
- Click+drag creation for points and lines
|
||||||
|
- No snapping/inference in v1; rely on explicit constraints
|
||||||
|
- View behavior:
|
||||||
|
- No auto camera orientation on sketch edit entry (user uses `n` manually)
|
||||||
|
- Non-edit sketch display remains gray when visible, hidden when invisible
|
||||||
|
- While editing a sketch, disable hover-highlight behavior for that sketch
|
||||||
|
- Coordinate model:
|
||||||
|
- Store sketch geometry in sketch-local 2D coordinates
|
||||||
|
- Plane/frame transform maps sketch-local geometry into 3D scene
|
||||||
|
- This is required for future derived geometry from non-datum faces/parts
|
||||||
|
- When rendering world-space derived previews inside sketch runtime, convert world coords to parent-local before drawing (avoid double-transform rotation/offset artifacts)
|
||||||
|
- For solid feature ops that derive cutters from selected edges (ex: chamfer), preserve mesh-topology edges (`indices` adjacency) instead of position-welding vertices for adjacency lookup. Position welding can pair non-adjacent triangles and rotate/offset generated cutters.
|
||||||
|
- Chamfer cutter prism winding matters: keep end-cap triangle winding outward/consistent with side faces. Reversed cap winding can make Manifold subtraction fail (`NotManifold`) even when cutter placement is correct.
|
||||||
|
- Constraint rollout (checkpoint):
|
||||||
|
- Solver-backed enforcement is active (planegcs + fallback)
|
||||||
|
- Implemented:
|
||||||
|
- Lines: `horizontal`, `vertical`, `perpendicular`
|
||||||
|
- Points: `coincident`, `fixed`
|
||||||
|
- Arc/Circle: `arc_center_coincident`
|
||||||
|
- Next target set:
|
||||||
|
- `equal` (line length), `collinear`, `tangent`
|
||||||
|
- Dimensions:
|
||||||
|
- Support both driven and derived dimensions (for later variable system)
|
||||||
|
- Selection roadmap:
|
||||||
|
- v1: click selection
|
||||||
|
- later: rectangle selection parity with Onshape semantics:
|
||||||
|
- right-drag = must fully enclose
|
||||||
|
- left-drag = crossing/touch selects
|
||||||
|
- TODO later: bring rectangle/marquee selection parity to non-sketch (global 3D) mode
|
||||||
|
- Construction geometry:
|
||||||
|
- Required early
|
||||||
|
- Toggle selected entity construction state with `q`
|
||||||
|
- Construction lines render dashed
|
||||||
|
- Undo/redo granularity:
|
||||||
|
- One undo unit per mutation (entity create/complete move/change, dimension change)
|
||||||
|
- Not per low-level pointer gesture frame
|
||||||
|
|
||||||
|
**Sketch Point Rendering (current)**
|
||||||
|
|
||||||
|
- Sketch point and arc-center markers are now shader-based (`THREE.Points` + fragment rings) in WebGL:
|
||||||
|
- camera-facing, circular, pixel-sized (zoom invariant)
|
||||||
|
- avoids DOM overlay jitter at high entity counts
|
||||||
|
- Legacy `_markerParts` compatibility shims are retained so existing hover/select styling code paths continue to work.
|
||||||
|
- Centralized JS color tuning now starts in `src/void/palette.js` (current coverage: sketch + viewcube, expanding incrementally).
|
||||||
|
- Sketch non-construction lines/arcs now use `Line2/LineMaterial` for visible hover/select thickness control (`lineWidths` in palette).
|
||||||
|
- Arc/circle sketch dimensions now use **diameter semantics** (stored/edited/measured as diameter; solver applies radius = diameter / 2). Dimension decoration renders:
|
||||||
|
- inside circle/arc: full diameter line with arrow end caps
|
||||||
|
- outside circle/arc: leader line with arrow pointing to the circle
|
||||||
|
- Known runtime refresh issue (open, under validation):
|
||||||
|
- if pointer remains over tree/panels long enough, viewport updates can appear stalled (sketch hover/render). Keep `space` activity/refresh alive for UI-target mousemove paths.
|
||||||
|
|
||||||
|
### Routes
|
||||||
|
|
||||||
|
- `/void/` - Primary URL
|
||||||
|
- `/form/` - Alias (same app)
|
||||||
|
|
||||||
|
### Database (IndexedDB)
|
||||||
|
|
||||||
|
- `admin` store - Metadata, camera position
|
||||||
|
- `documents` store - Document data
|
||||||
|
- `versions` store - Revision history (snapshots/deltas)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- `/Users/stewart/Code/gs-apps/VOID-FORM.md` - Full implementation notes
|
||||||
|
|
||||||
|
### Dependencies (Unique to void:form)
|
||||||
|
|
||||||
|
- **@salusoft89/planegcs** ^1.1.7 - 2D constraint solver (active)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Shared Infrastructure
|
||||||
|
|
||||||
|
All three apps build on common modules, but usage patterns differ by app:
|
||||||
|
|
||||||
|
### Core Systems (moto/)
|
||||||
|
|
||||||
|
#### 1. Event System (`broker.js` - 156 lines)
|
||||||
|
|
||||||
|
Used heavily by `kiri:moto` and `mesh:tool`. `void:form` currently does not use broker in its runtime path.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { broker } from '../moto/broker.js';
|
||||||
|
|
||||||
|
// Publish
|
||||||
|
broker.publish('feature.selected', { id: 'plane-1' });
|
||||||
|
|
||||||
|
// Subscribe
|
||||||
|
broker.subscribe('feature.selected', (data) => { ... });
|
||||||
|
|
||||||
|
// Typed send interface
|
||||||
|
broker.send.feature_selected({ id: 'plane-1' });
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 3D Viewport (`space.js` - 55KB)
|
||||||
|
|
||||||
|
Three.js wrapper with camera, scene, and interaction:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { space } from '../moto/space.js';
|
||||||
|
|
||||||
|
// Initialize viewport
|
||||||
|
space.init(container, onMove, useKeys);
|
||||||
|
|
||||||
|
// Scene hierarchy
|
||||||
|
SCENE (Three.js Scene)
|
||||||
|
└── WORLD (THREE.Group, rotated -π/2 on X-axis)
|
||||||
|
└── Your objects here
|
||||||
|
|
||||||
|
// Use space.world.add(), NOT space.scene.add()
|
||||||
|
space.world.add(group);
|
||||||
|
|
||||||
|
// Camera controls (Onshape-style for void, configurable for others)
|
||||||
|
space.view.top() // Top view
|
||||||
|
space.view.front() // Front view
|
||||||
|
space.view.right() // Right view
|
||||||
|
space.view.left() // Left view
|
||||||
|
space.view.back() // Back view
|
||||||
|
space.view.bottom() // Bottom view
|
||||||
|
space.view.fit() // Fit all to view
|
||||||
|
|
||||||
|
// Camera state
|
||||||
|
space.view.save() // Returns { left, up, panX, panY, panZ, scale }
|
||||||
|
space.view.load(state) // Restore saved state
|
||||||
|
space.view.getFocus() // Get orbit target
|
||||||
|
space.view.setFocus(vec3) // Set orbit target
|
||||||
|
|
||||||
|
// Mouse bindings (configurable)
|
||||||
|
RIGHT = Orbit // Rotate around target
|
||||||
|
MIDDLE = Pan // Pan view
|
||||||
|
WHEEL = Zoom // Zoom in/out
|
||||||
|
|
||||||
|
// Internals access
|
||||||
|
const { camera, renderer, raycaster, platform, container } = space.internals();
|
||||||
|
|
||||||
|
// Listen for camera changes
|
||||||
|
space.view.ctrl.addEventListener('change', callback);
|
||||||
|
|
||||||
|
// After-render callbacks (for ViewCube, etc.)
|
||||||
|
space.afterRender((renderer) => {
|
||||||
|
// Custom render pass
|
||||||
|
viewcube.render(renderer);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tracking plane for drag operations (void:form)
|
||||||
|
space.tracking.setMode('camera-aligned'); // 'platform', 'camera-aligned', 'world-xy'
|
||||||
|
space.tracking.setDistance(1000); // Distance from camera
|
||||||
|
space.tracking.getMode(); // Get current mode
|
||||||
|
space.tracking.getPlane(); // Get THREE.Mesh for advanced use
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Camera Controls (`orbit.js` - 25KB)
|
||||||
|
|
||||||
|
Orbit control class for camera manipulation:
|
||||||
|
|
||||||
|
- Spherical coordinates (theta/phi)
|
||||||
|
- Pan, zoom, rotate operations
|
||||||
|
- Tweening for smooth animations
|
||||||
|
- Touch support
|
||||||
|
|
||||||
|
#### 4. Web UI Helpers (`webui.js` - 4KB)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { $, $C, h } from '../moto/webui.js';
|
||||||
|
|
||||||
|
$('element-id') // Get element by ID
|
||||||
|
$C('ClassName') // Get elements by class
|
||||||
|
h.div([...]) // Create DOM elements
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. Worker System (`client.js`, `worker.js`)
|
||||||
|
|
||||||
|
Web Worker abstraction with promise-based API:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { client } from '../moto/client.js';
|
||||||
|
|
||||||
|
const worker = client.new('worker-url.js');
|
||||||
|
worker.send('method', data).then(result => { ... });
|
||||||
|
```
|
||||||
|
|
||||||
|
### Geometry & Math (geo/)
|
||||||
|
|
||||||
|
Shared by all apps for 2D/3D operations:
|
||||||
|
|
||||||
|
- `base.js` - Core math utilities (22KB)
|
||||||
|
- `polygon.js` - 2D polygon operations (48KB)
|
||||||
|
- `polygons.js` - Multi-polygon operations (39KB)
|
||||||
|
- `point.js` - Point data structure (30KB)
|
||||||
|
- `paths.js` - Path operations (27KB)
|
||||||
|
- `slicer.js` - Slicing algorithms (31KB)
|
||||||
|
- `line.js`, `bounds.js`, `csg.js`, etc.
|
||||||
|
|
||||||
|
### File Loading (load/)
|
||||||
|
|
||||||
|
Format detection and parsing:
|
||||||
|
|
||||||
|
- `file.js` - Auto-detect file type
|
||||||
|
- `stl.js` - STL (binary & ASCII)
|
||||||
|
- `obj.js` - Wavefront OBJ
|
||||||
|
- `3mf.js` - 3MF (Microsoft 3D)
|
||||||
|
- `step.js` - STEP (CAD format)
|
||||||
|
- `svg.js` - SVG (2D vector)
|
||||||
|
- `gbr.js` - Gerber (PCB format)
|
||||||
|
- `png.js` - PNG (height map)
|
||||||
|
|
||||||
|
### External Libraries (ext/)
|
||||||
|
|
||||||
|
Pre-integrated WASM and libraries:
|
||||||
|
|
||||||
|
- `three.js` - Three.js v0.182.0 (2.4MB)
|
||||||
|
- `manifold.js` - 3D boolean operations (WASM)
|
||||||
|
- `quickjs.js` - JavaScript VM (2.4MB WASM)
|
||||||
|
- `jszip.js` - ZIP file handling
|
||||||
|
- `jspoly.js` - Polygon library (240KB)
|
||||||
|
- `clip2.js` - Polygon clipping (203KB)
|
||||||
|
- `pngjs.js` - PNG reading
|
||||||
|
- `earcut.js` - Polygon triangulation
|
||||||
|
- `tween.js` - Animation tweening
|
||||||
|
- `md5.js` - MD5 hashing
|
||||||
|
|
||||||
|
### Data Storage (data/)
|
||||||
|
|
||||||
|
IndexedDB wrapper:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { open as dataOpen } from '../data/index.js';
|
||||||
|
|
||||||
|
const stores = dataOpen('dbname', {
|
||||||
|
stores: ['admin', 'documents'],
|
||||||
|
version: 1
|
||||||
|
}).init();
|
||||||
|
|
||||||
|
const db = {
|
||||||
|
admin: stores.promise('admin'),
|
||||||
|
documents: stores.promise('documents')
|
||||||
|
};
|
||||||
|
|
||||||
|
db.admin.put('key', value);
|
||||||
|
db.admin.get('key').then(value => { ... });
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Architectural Patterns
|
||||||
|
|
||||||
|
### 1. Three.js Native Objects
|
||||||
|
|
||||||
|
All 3D primitives are native Three.js objects:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { THREE } from "../ext/three.js";
|
||||||
|
|
||||||
|
const { Group, Mesh, LineSegments, BoxGeometry, MeshBasicMaterial } = THREE;
|
||||||
|
|
||||||
|
// Create as Group with children
|
||||||
|
const group = new Group();
|
||||||
|
group.add(mesh);
|
||||||
|
group.add(outline);
|
||||||
|
|
||||||
|
// Add userData for back-references
|
||||||
|
group.userData.featureType = "plane";
|
||||||
|
group.userData.plane = this;
|
||||||
|
|
||||||
|
// Set renderOrder to control draw order (avoid z-fighting)
|
||||||
|
mesh.renderOrder = 1;
|
||||||
|
outline.renderOrder = 2;
|
||||||
|
|
||||||
|
// Transparent objects MUST have depthWrite: false
|
||||||
|
const material = new MeshBasicMaterial({
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.5,
|
||||||
|
depthWrite: false, // CRITICAL for transparency
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Event-Driven Communication
|
||||||
|
|
||||||
|
`kiri:moto` and `mesh:tool` use broker for loose coupling. `void:form` currently uses direct module calls/shared API state.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Subscribe to events
|
||||||
|
broker.subscribe("model.updated", (data) => {
|
||||||
|
updateUI(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Publish events
|
||||||
|
broker.publish("model.updated", { model });
|
||||||
|
|
||||||
|
// Or use typed interface
|
||||||
|
broker.send.model_updated({ model });
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// void:form pattern (current)
|
||||||
|
api.document.create();
|
||||||
|
api.features.add(feature);
|
||||||
|
tree.render();
|
||||||
|
datum.updateLabels(overlay);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.1. Void Interaction Contract (Current)
|
||||||
|
|
||||||
|
`void:form` interaction is currently plane-centric and depends on `userData` back-references:
|
||||||
|
|
||||||
|
- Raycast targets are returned from `interact.getInteractiveObjects()`
|
||||||
|
- Selection/hover resolve via `intersection.object.userData.plane`
|
||||||
|
- Drag-resize logic is implemented for plane corner handles (`handleType = 'plane-resize'`)
|
||||||
|
- `space.mouse.*Select()` callbacks are two-phase: first call with no event returns raycast targets, second call handles resolved intersections
|
||||||
|
- For resize start, `interact.downSelect` should prioritize handle hits from full intersections (`ints`) so selected handles remain draggable when occluded by plane meshes
|
||||||
|
- Non-plane feature types should extend `src/void/interact/planes.js` + `src/void/interact/targets.js`; `registerPlane()` alone is not sufficient for custom interactions
|
||||||
|
- Plane labels should be bound to plane changes (size/position/rotation/label), not only camera movement
|
||||||
|
|
||||||
|
### 3. Mouse Interaction Pattern
|
||||||
|
|
||||||
|
Standard pattern across all apps:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
space.mouse.downSelect((intersection, event, allIntersections) => {
|
||||||
|
if (!event) {
|
||||||
|
// Return objects for raycasting
|
||||||
|
return [mesh1, mesh2, mesh3];
|
||||||
|
}
|
||||||
|
// Handle click
|
||||||
|
if (intersection) {
|
||||||
|
const obj = intersection.object.userData.myObject;
|
||||||
|
// ... do something
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
space.mouse.onHover(
|
||||||
|
(intersection, event, allIntersections) => {
|
||||||
|
if (!event) return getInteractiveObjects();
|
||||||
|
// Handle hover
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
// Handle hover exit
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
space.mouse.onDrag((delta) => {
|
||||||
|
// Handle drag (delta = {x, y} in pixels)
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Worker/Threading Pattern
|
||||||
|
|
||||||
|
- **Kiri**: Multi-threaded minion pool for slicing (up to 4 workers)
|
||||||
|
- **Mesh**: Single worker for heavy 3D operations
|
||||||
|
- **Void**: Single worker for solid rebuild replay (active), constraint solve still on main thread
|
||||||
|
|
||||||
|
### 5. API Surface Pattern
|
||||||
|
|
||||||
|
Each app exports main `api` object:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Kiri API - ~45 subsystems
|
||||||
|
api.widgets, api.function, api.mode, api.work, api.device, ...
|
||||||
|
|
||||||
|
// Mesh API - ~18 subsystems
|
||||||
|
api.selection, api.group, api.model, api.sketch, api.tool, ...
|
||||||
|
|
||||||
|
// Void API - ~6 subsystems (expanding)
|
||||||
|
api.document, api.features, api.sketch, api.origin, api.selection, api.datum, ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Database Pattern
|
||||||
|
|
||||||
|
IndexedDB with named stores, per-app schema:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
dataOpen("appname", { stores: ["admin", "data"], version: 1 });
|
||||||
|
api.db.admin.put(key, value);
|
||||||
|
api.db.data.get(id);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Differences Between Apps
|
||||||
|
|
||||||
|
| Aspect | Kiri:Moto | Mesh:Tool | Void:Form |
|
||||||
|
| --------------- | ---------------------------- | ------------------------------------ | ----------------------------------------------- |
|
||||||
|
| **Purpose** | Slicing for manufacturing | Mesh editing & repair | Parametric CAD design |
|
||||||
|
| **Data Model** | Widget-based slicing | Triangle mesh + sketches | Early document/features scaffold + datum planes |
|
||||||
|
| **UI Pattern** | Tabs + device/process panels | Tree + mode buttons | Toolbar + feature tree scaffold |
|
||||||
|
| **3D System** | space.js + platform | space.js + platform | space.js + datum planes |
|
||||||
|
| **Calculation** | Web Workers (minion pool) | Web Worker | Single rebuild worker (active) |
|
||||||
|
| **Modes** | CAM/FDM/LASER/SLA/WEDM/WJET | Object/Tool/Face/Surface/Edge/Sketch | Sketch mode (phase 2) |
|
||||||
|
| **Mouse** | Configurable bindings | Standard bindings | Onshape-style bindings |
|
||||||
|
| **Database** | Profiles, settings, history | Models, groups, sketches | Documents + versions revision history |
|
||||||
|
| **Status** | Production mature | Actively developed | Very early prototype / Phase 1 foundation |
|
||||||
|
| **API Size** | ~10KB, 45 subsystems | ~1,730 lines, 18 subsystems | Split modules (document/features/origin/sketch) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Tasks
|
||||||
|
|
||||||
|
### Adding a New Feature Type (void:form)
|
||||||
|
|
||||||
|
1. Create class in `src/void/yourfeature.js` similar to `Plane`
|
||||||
|
2. Return `THREE.Group` with children (mesh, outline, handles)
|
||||||
|
3. Set `userData.featureType = 'yourtype'` and `userData.yourfeature = this`
|
||||||
|
4. For plane-like behavior, register with `interact.registerPlane()`; for non-plane behavior, extend `src/void/interact/planes.js` hit-testing and/or `src/void/interact/targets.js`
|
||||||
|
5. If the feature has labels/anchors, expose change notifications so overlays update on geometry/transform edits
|
||||||
|
6. Update `api.document/features` and refresh dependent UI directly (no broker path today)
|
||||||
|
|
||||||
|
### Adding a Tool Operation (mesh:tool)
|
||||||
|
|
||||||
|
1. Add function to `src/mesh/tool.js`
|
||||||
|
2. Register in `api.tool.yourOperation()`
|
||||||
|
3. Send to worker if heavy operation (`api.work.send()`)
|
||||||
|
4. Update UI via broker events
|
||||||
|
5. Add history entry for undo/redo
|
||||||
|
|
||||||
|
### Adding a Slicing Mode (kiri:moto)
|
||||||
|
|
||||||
|
1. Create mode directory in `src/kiri/mode/yourmode/`
|
||||||
|
2. Implement slice, setup, export functions
|
||||||
|
3. Register mode in `api.mode`
|
||||||
|
4. Add device profiles in `src/cli/`
|
||||||
|
5. Update worker bundles
|
||||||
|
|
||||||
|
### Working with Transparent Objects
|
||||||
|
|
||||||
|
To avoid z-fighting with transparent planes/faces:
|
||||||
|
|
||||||
|
- Set `renderOrder` (higher = rendered later)
|
||||||
|
- Use `depthWrite: false` on transparent materials
|
||||||
|
- Consider separate render passes for complex transparency
|
||||||
|
- void:form ViewCube uses separate render pass to avoid z-fighting
|
||||||
|
|
||||||
|
### Viewport Rendering (Multiple Passes)
|
||||||
|
|
||||||
|
For widgets needing separate rendering (ViewCube pattern):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
space.afterRender((renderer) => {
|
||||||
|
// Save current viewport
|
||||||
|
const currentViewport = new THREE.Vector4();
|
||||||
|
renderer.getViewport(currentViewport);
|
||||||
|
|
||||||
|
// Set custom viewport (e.g., top-right corner)
|
||||||
|
renderer.setViewport(x, y, width, height);
|
||||||
|
renderer.setScissor(x, y, width, height);
|
||||||
|
renderer.setScissorTest(true);
|
||||||
|
renderer.autoClear = false;
|
||||||
|
|
||||||
|
// Render your scene
|
||||||
|
renderer.render(myScene, myCamera);
|
||||||
|
|
||||||
|
// Restore
|
||||||
|
renderer.setViewport(currentViewport);
|
||||||
|
renderer.setScissorTest(false);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
ViewCube caveat:
|
||||||
|
|
||||||
|
- `ViewCube` renders in a separate pass via `space.afterRender()`
|
||||||
|
- Preserve and restore renderer viewport/scissor/autoclear state when adding more overlays/widgets
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Critical Rules
|
||||||
|
|
||||||
|
1. **ALWAYS** read files before editing them
|
||||||
|
2. **NEVER** use `SCENE.add()` - use `space.world.add()` instead
|
||||||
|
3. **NEVER** forget `depthWrite: false` on transparent materials
|
||||||
|
4. **ALWAYS** dispose of Three.js geometry/materials when removing objects
|
||||||
|
5. **ALWAYS** use `userData` for back-references on Three.js objects
|
||||||
|
6. **PREFER** repo-consistent tooling and keep edits minimal/reviewable
|
||||||
|
7. **ALWAYS** test z-fighting issues with transparent overlapping geometry
|
||||||
|
8. **NEVER** modify shared moto/ infrastructure without considering all three apps
|
||||||
|
9. **USE BROKER WHEN THE APP ALREADY FOLLOWS THAT PATTERN** (`kiri:moto`, `mesh:tool`); `void:form` currently uses direct module calls
|
||||||
|
10. **NEVER** block the main thread - use workers for heavy computation
|
||||||
|
11. **RESPECT SPACE MOUSE CALLBACK SHAPE**: target-discovery and event handling are separate phases; use full intersection lists when interaction priority matters
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Important File Paths
|
||||||
|
|
||||||
|
### Entry Points
|
||||||
|
|
||||||
|
- `/Users/stewart/Code/gs-apps/src/main/kiri.js` - Kiri:Moto bootstrap
|
||||||
|
- `/Users/stewart/Code/gs-apps/src/main/mesh.js` - Mesh:Tool bootstrap
|
||||||
|
- `/Users/stewart/Code/gs-apps/src/main/void.js` - Void:Form bootstrap
|
||||||
|
|
||||||
|
### Core APIs
|
||||||
|
|
||||||
|
- `/Users/stewart/Code/gs-apps/src/kiri/app/api.js` - Kiri API (~10KB)
|
||||||
|
- `/Users/stewart/Code/gs-apps/src/mesh/api.js` - Mesh API (~1,730 lines)
|
||||||
|
- `/Users/stewart/Code/gs-apps/src/void/api.js` - Void API composition root
|
||||||
|
- `/Users/stewart/Code/gs-apps/src/void/api/document.js` - Void document + revisions/undo/redo
|
||||||
|
- `/Users/stewart/Code/gs-apps/src/void/interact.js` - Void interaction composition root
|
||||||
|
|
||||||
|
### Shared Infrastructure
|
||||||
|
|
||||||
|
- `/Users/stewart/Code/gs-apps/src/moto/space.js` - 3D viewport (55KB)
|
||||||
|
- `/Users/stewart/Code/gs-apps/src/moto/broker.js` - Event system (156 lines)
|
||||||
|
- `/Users/stewart/Code/gs-apps/src/moto/orbit.js` - Camera controls (25KB)
|
||||||
|
- `/Users/stewart/Code/gs-apps/src/moto/webui.js` - DOM helpers (4KB)
|
||||||
|
|
||||||
|
### Geometry & Loading
|
||||||
|
|
||||||
|
- `/Users/stewart/Code/gs-apps/src/geo/` - Math & geometry (12 modules)
|
||||||
|
- `/Users/stewart/Code/gs-apps/src/load/` - File format loaders (9 formats)
|
||||||
|
- `/Users/stewart/Code/gs-apps/src/ext/` - External libraries (Three.js, Manifold, etc.)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- `/Users/stewart/Code/gs-apps/docs/kiri-moto/` - Kiri:Moto docs (extensive)
|
||||||
|
- `/Users/stewart/Code/gs-apps/docs/mesh-tool.md` - Mesh:Tool docs
|
||||||
|
- `/Users/stewart/Code/gs-apps/VOID-FORM.md` - Void:Form implementation notes
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
- `/Users/stewart/Code/gs-apps/app.js` - Express server (routes at lines 135-166)
|
||||||
|
- `/Users/stewart/Code/gs-apps/package.json` - Dependencies
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Routes
|
||||||
|
|
||||||
|
### Development URLs (http://localhost:8080)
|
||||||
|
|
||||||
|
- `/kiri/` - Kiri:Moto slicer
|
||||||
|
- `/mesh/` - Mesh:Tool editor
|
||||||
|
- `/void/` - Void:Form CAD (primary)
|
||||||
|
- `/form/` - Void:Form CAD (alias)
|
||||||
|
|
||||||
|
### Static Assets
|
||||||
|
|
||||||
|
- `/lib/pack/kiri-main.js` - Kiri main bundle (~28KB)
|
||||||
|
- `/lib/pack/kiri-work.js` - Kiri worker bundle
|
||||||
|
- `/lib/pack/kiri-eng.js` - Kiri engine bundle
|
||||||
|
- `/lib/pack/mesh-main.js` - Mesh main bundle
|
||||||
|
- `/lib/pack/mesh-work.js` - Mesh worker bundle
|
||||||
|
- `/lib/pack/void-main.js` - Void main bundle
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install # Install dependencies
|
||||||
|
npm run dev # Start dev server (port 8080)
|
||||||
|
npm run build # Build for production
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Shared (all apps)
|
||||||
|
|
||||||
|
- **three** ^0.182.0 - 3D rendering
|
||||||
|
- **manifold-3d** ^3.3.2 - BREP operations
|
||||||
|
- **jszip** - ZIP file handling
|
||||||
|
|
||||||
|
### Void-specific
|
||||||
|
|
||||||
|
- **@salusoft89/planegcs** ^1.1.7 - 2D constraint solver
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Git Status
|
||||||
|
|
||||||
|
- Current branch: `rel-4.6-void`
|
||||||
|
- Main branch: `master` (use for PRs)
|
||||||
|
- Recent work: ViewCube widget, datum planes, plane primitives
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
### Kiri:Moto
|
||||||
|
|
||||||
|
- Mature product, maintenance mode
|
||||||
|
- Device profile updates
|
||||||
|
- Mode-specific improvements
|
||||||
|
|
||||||
|
### Mesh:Tool
|
||||||
|
|
||||||
|
- Active development
|
||||||
|
- Face/edge selection enhancements
|
||||||
|
- Boolean operation improvements
|
||||||
|
- Sketch system refinements
|
||||||
|
|
||||||
|
### Void:Form
|
||||||
|
|
||||||
|
**Phase 2: Sketch System** (Next)
|
||||||
|
|
||||||
|
1. planegcs constraint solver integration
|
||||||
|
2. 2D sketch canvas overlay
|
||||||
|
3. Geometric primitives (line, circle, arc)
|
||||||
|
4. Constraints (distance, angle, parallel, perpendicular)
|
||||||
|
|
||||||
|
**Phase 3: Features**
|
||||||
|
|
||||||
|
1. Extrude feature using Manifold
|
||||||
|
2. Feature history tree with parametric updates
|
||||||
|
3. Cut, revolve, sweep operations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated:** 2026-02-03 (ViewCube integration, comprehensive coverage)
|
||||||
49
docs/future-palette.md
Normal file
49
docs/future-palette.md
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Shared Palette Audit (Kiri / Mesh / Void)
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Define a shared semantic color system in `web/moto/palette.css` and migrate each app incrementally with low risk.
|
||||||
|
|
||||||
|
## Phase 1 Delivered
|
||||||
|
|
||||||
|
- Added shared file: `web/moto/palette.css`
|
||||||
|
- Added semantic tokens for `light` and `dark` themes:
|
||||||
|
- `--color-bg`, `--color-surface`, `--color-text`, `--color-border`, `--color-accent`, etc
|
||||||
|
- Added compatibility aliases (bridge variables) for Mesh/Void existing CSS vars.
|
||||||
|
- Wired palette stylesheet into:
|
||||||
|
- `web/kiri/index.html`
|
||||||
|
- `web/mesh/index.html`
|
||||||
|
- `web/void/index.html`
|
||||||
|
- Added root attributes:
|
||||||
|
- Kiri: `data-app="kiri"` and early `data-theme="light|dark"` set in head script
|
||||||
|
- Mesh: `data-app="mesh" data-theme="dark"`
|
||||||
|
- Void: `data-app="void" data-theme="dark"`
|
||||||
|
|
||||||
|
## Naming Recommendation (semantic first)
|
||||||
|
|
||||||
|
- Surfaces:
|
||||||
|
- `--color-bg`, `--color-bg-elev`, `--color-bg-subtle`
|
||||||
|
- `--color-surface`, `--color-surface-2`
|
||||||
|
- Content:
|
||||||
|
- `--color-text`, `--color-text-muted`
|
||||||
|
- `--color-border`, `--color-border-strong`
|
||||||
|
- Interaction:
|
||||||
|
- `--color-accent`, `--color-accent-hover`, `--color-focus`
|
||||||
|
- `--color-selection`, `--color-selection-hover`
|
||||||
|
- Status:
|
||||||
|
- `--color-success`, `--color-warning`, `--color-danger`
|
||||||
|
|
||||||
|
## Step-wise Migration Plan
|
||||||
|
|
||||||
|
1. Convert top-level containers/menus/panels in each app to semantic tokens only.
|
||||||
|
2. Convert controls/interactions (hover/focus/selected) to semantic tokens.
|
||||||
|
3. Convert specialty overlays (grids, badges, debug panes) last.
|
||||||
|
4. Remove legacy per-app aliases once selectors are migrated.
|
||||||
|
|
||||||
|
## Validation Checklist Per Step
|
||||||
|
|
||||||
|
- Kiri light unchanged
|
||||||
|
- Kiri dark unchanged
|
||||||
|
- Mesh dark unchanged
|
||||||
|
- Void dark unchanged
|
||||||
|
- Contrast and hover/focus states still readable
|
||||||
108
docs/future.md
Normal file
108
docs/future.md
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
# Grid:Apps Future Notes
|
||||||
|
|
||||||
|
## `C` cosmetic, `F` functional, `P` performance, `B` bug fix
|
||||||
|
|
||||||
|
- `F` option to hide platform when viewing from below
|
||||||
|
|
||||||
|
# Kiri:Moto
|
||||||
|
|
||||||
|
- `B` origin (and bed size) bug (Onshape?) when switching device modes
|
||||||
|
- `B` can't drag slider bar on ipad / ios -- touch pad scrolling dodgy
|
||||||
|
- `B` prevent or ask for really large models when scaling (crash ui)
|
||||||
|
|
||||||
|
- `P` refactor vertex replacement and widget update matrix tracking
|
||||||
|
- `P` duplicate objects should share same slice data unless rotated or scaled
|
||||||
|
- `P` move all persisted / workspace settings/data to IndexedDB (LS limitations)
|
||||||
|
- `P` faster ray intersect https://github.com/gkjohnson/three-mesh-bvh/
|
||||||
|
- `P` try material clipping planes for slice range selection
|
||||||
|
- `P` switch png lib to https://github.com/photopea/UPNG.js
|
||||||
|
|
||||||
|
- `F` edit in Mesh:Tool
|
||||||
|
- `F` custom device vars for profiles / ranges / gcode
|
||||||
|
- `F` show slider range values in workspace units (on hover?)
|
||||||
|
- `F` allow select of a range by typing in values in slices or workspace units
|
||||||
|
- `F` complete and expose grouping feature
|
||||||
|
- `F` add svgnest-like arrange algorithm
|
||||||
|
- `F` date column and sorting in recent files list
|
||||||
|
- `F` highlight part outside workspace bounds (like can neg Z shader)
|
||||||
|
|
||||||
|
# FDM
|
||||||
|
|
||||||
|
- `B` fix support projection/end with grouped parts
|
||||||
|
- `B` multi-extruder rendering of raft fails to offset the rest of the print
|
||||||
|
- `B` multi-extruder purge blocks fail to generate properly for rafts
|
||||||
|
|
||||||
|
- `F` new parameters for bridging speed / bridge fan control
|
||||||
|
- `F` convert ranges to z offsets while continuing to show layer #
|
||||||
|
- `F` support pillar top/bottom should conform to part
|
||||||
|
- `F` more explicit line width control with ranges and min/max adaptive
|
||||||
|
- `F` test outlining solid projected areas (internally)
|
||||||
|
- `F` gradient infill https://www.youtube.com/watch?v=hq53gsYREHU&feature=emb_logo
|
||||||
|
- `F` segment large flat areas on first layer to mitigate peeling
|
||||||
|
- `F` option to support interior bridges when 0% infill
|
||||||
|
- `F` calculate filament use per extruder per print
|
||||||
|
- `F` expand internal supporting flats / solids before projection (threshold)
|
||||||
|
|
||||||
|
- `P` auto purge pillars when quick layers are detected for extra cooling
|
||||||
|
- `P` solid fill the tops of supports for down facing flats
|
||||||
|
|
||||||
|
# FDM - BELT
|
||||||
|
|
||||||
|
- `B` auto bed resizing leaves the origin in the wrong place at first
|
||||||
|
- `B` re-add progress calls for all work units
|
||||||
|
|
||||||
|
- `F` test and enable arcs in belt more
|
||||||
|
- `F` slightly angle supports to lean into the Z of the part
|
||||||
|
- `F` anchors should be generated anywhere needed in the print, not just head
|
||||||
|
|
||||||
|
# CAM
|
||||||
|
|
||||||
|
- `B` feed rate for next tool set before tool change (push/pop feed rates?)
|
||||||
|
- `B` tabs do not properly track widget mirror events
|
||||||
|
- `B` contour does not honor clip to stock
|
||||||
|
|
||||||
|
- `F` allow import, rotation, scaling of stock
|
||||||
|
- `F` get gcode coordinates off a part with point/click or hover?
|
||||||
|
- `F` include tools in default devices (Carvera)
|
||||||
|
- `F` add `{progress}` substitution and maybe `{time-remaining}` if can be calc'd
|
||||||
|
- `F` import and follow 2D paths (conformed like pocket contours)
|
||||||
|
- `F` add `plunge max` to contouring that can override z feed limit
|
||||||
|
- `F` add lead-in milling (requires adding clamp / no go areas)
|
||||||
|
- `F` add linear clearing strategy
|
||||||
|
- `F` add adaptive clearing strategy
|
||||||
|
- `F` change color of line selection in trace op when not a closed poly
|
||||||
|
- `F` limit cut depth to flute length of selected tool (or warn)
|
||||||
|
- `F` validate muti-part layout and spacing exceeds largest outside tool diameter
|
||||||
|
- `F` trapezoidal tabs in Z
|
||||||
|
- `F` ease-in and ease-out especially on tab cut-out start/stop
|
||||||
|
- `F` maintain several part orientations + op chains in a single profile
|
||||||
|
|
||||||
|
- `P` outer outside corners as arc moves
|
||||||
|
- `P` improve parser - do not require spaced tokens and support implied G0 / G1
|
||||||
|
- `P` log Z interpolation for contour XYZ moves
|
||||||
|
|
||||||
|
# Laser
|
||||||
|
|
||||||
|
- `F` add PLT / HP-GL output format (https://en.wikipedia.org/wiki/HP-GL)
|
||||||
|
|
||||||
|
# Mesh:Tool
|
||||||
|
|
||||||
|
- add undo/redo
|
||||||
|
- add TextGeometry
|
||||||
|
- https://threejs.org/docs/#examples/en/geometries/TextGeometry
|
||||||
|
- https://dustinpfister.github.io/2023/07/05/threejs-text-geometry/
|
||||||
|
- font to path with https://github.com/paulzi/svg-text-to-path
|
||||||
|
- click to repair normals based on a known good (selected) face
|
||||||
|
- send to Kiri:Moto workspace (or update model vertices in place)
|
||||||
|
- better z snap using just vertexes from face intersected
|
||||||
|
- add section view. local clip. raycast skip points above plane
|
||||||
|
- add decimate op = face reduction
|
||||||
|
- add flatten/crush op: for z bottoms (or surfaces?)
|
||||||
|
- allow setting model/group origin for scale/rotate
|
||||||
|
- fix mirror to work with groups (just models currently)
|
||||||
|
- add analyze results dialog
|
||||||
|
- remove/hide auto-repair function
|
||||||
|
|
||||||
|
# Other
|
||||||
|
|
||||||
|
- preferred icons https://icons.getbootstrap.com/
|
||||||
|
|
@ -6,117 +6,117 @@ description: Keyboard Shortcuts and Mouse Controls
|
||||||
|
|
||||||
## Layer Navigation
|
## Layer Navigation
|
||||||
|
|
||||||
| Key | Action | Notes |
|
| Key | Action | Notes |
|
||||||
| --- | ------ | ----- |
|
| --------------- | ------------------------ | ------------------------------------ |
|
||||||
| ` (backtick) | Show layer 0 | First layer |
|
| ` (backtick) | Show layer 0 | First layer |
|
||||||
| 1-9 | Show layers at 10-90% | In 10% increments |
|
| 1-9 | Show layers at 10-90% | In 10% increments |
|
||||||
| 0 | Show all layers (100%) | Show maximum layer |
|
| 0 | Show all layers (100%) | Show maximum layer |
|
||||||
| Meta+Up | Next layer | Navigate up one layer |
|
| Meta+Up | Next layer | Navigate up one layer |
|
||||||
| Meta+Down | Previous layer | Navigate down one layer |
|
| Meta+Down | Previous layer | Navigate down one layer |
|
||||||
| [shift + wheel] | Cycle through layers | Preview mode only |
|
| [shift + wheel] | Cycle through layers | Preview mode only |
|
||||||
| v | Toggle single slice view | Preview mode: toggle single vs range |
|
| v | Toggle single slice view | Preview mode: toggle single vs range |
|
||||||
|
|
||||||
## View Navigation
|
## View Navigation
|
||||||
|
|
||||||
| Key | Action | Notes |
|
| Key | Action | Notes |
|
||||||
| --- | ------ | ----- |
|
| --- | -------------------- | ------------------------------------- |
|
||||||
| h | Home View | Default 45° view |
|
| h | Home View | Default 45° view |
|
||||||
| t | Top View | Top-down view |
|
| t | Top View | Top-down view |
|
||||||
| f | Front View | Direct front side-view |
|
| f | Front View | Direct front side-view |
|
||||||
| b | Back View | Direct back side-view |
|
| b | Back View | Direct back side-view |
|
||||||
| z | Reset View | Default zoom and home |
|
| z | Reset View | Default zoom and home |
|
||||||
| F | Fit View to Contents | Zoom to fit all objects |
|
| F | Fit View to Contents | Zoom to fit all objects |
|
||||||
| `<` | Previous Side View | Cycle: left → back → right → front |
|
| `<` | Previous Side View | Cycle: left → back → right → front |
|
||||||
| `>` | Next Side View | Cycle: front → right → back → left |
|
| `>` | Next Side View | Cycle: front → right → back → left |
|
||||||
| v | Focus on Selection | Arrange mode: center camera on object |
|
| v | Focus on Selection | Arrange mode: center camera on object |
|
||||||
|
|
||||||
## File Operations
|
## File Operations
|
||||||
|
|
||||||
| Key | Action | Notes |
|
| Key | Action | Notes |
|
||||||
| --- | ------ | ----- |
|
| --- | ------------------- | ------------------------ |
|
||||||
| i | File Import Dialog | Import 3D models |
|
| i | File Import Dialog | Import 3D models |
|
||||||
| r | Recent Files Dialog | Open recently used files |
|
| r | Recent Files Dialog | Open recently used files |
|
||||||
|
|
||||||
## Workflow Actions
|
## Workflow Actions
|
||||||
|
|
||||||
| Key | Action | Notes |
|
| Key | Action | Notes |
|
||||||
| --- | ------ | ----- |
|
| ----- | ------------- | ------------------------------------------------------------ |
|
||||||
| s / S | Slice Object | Process workspace objects |
|
| s / S | Slice Object | Process workspace objects |
|
||||||
| p / P | Preview Paths | Route planning (hidden in SLA mode) |
|
| p / P | Preview Paths | Route planning (hidden in SLA mode) |
|
||||||
| g | CNC Animation | Enter milling animation mode (CNC only) |
|
| g | CNC Animation | Enter milling animation mode (CNC only) |
|
||||||
| x / X | Export | Export as GCode, SVG, or DXF |
|
| x / X | Export | Export as GCode, SVG, or DXF |
|
||||||
| a | Arrange | Switch to ARRANGE view, or auto-layout if already in ARRANGE |
|
| a | Arrange | Switch to ARRANGE view, or auto-layout if already in ARRANGE |
|
||||||
|
|
||||||
## Object Manipulation
|
## Object Manipulation
|
||||||
|
|
||||||
| Key | Action | Notes |
|
| Key | Action | Notes |
|
||||||
| --- | ------ | ----- |
|
| ---------------------- | --------------------- | ------------------------------------ |
|
||||||
| d | Duplicate Selected | Create copy of selected objects |
|
| d | Duplicate Selected | Create copy of selected objects |
|
||||||
| m | Mirror Selected | Mirror vertices of selected objects |
|
| m | Mirror Selected | Mirror vertices of selected objects |
|
||||||
| O | Manual Rotation Input | Enter rotation values manually |
|
| O | Manual Rotation Input | Enter rotation values manually |
|
||||||
| [delete] | Delete Selected | Remove selected objects |
|
| [delete] | Delete Selected | Remove selected objects |
|
||||||
| [ctrl + a] / [cmd + a] | Select All | Select all objects in workspace |
|
| [ctrl + a] / [cmd + a] | Select All | Select all objects in workspace |
|
||||||
| [shift + click] | Toggle Selection | Select or deselect individual object |
|
| [shift + click] | Toggle Selection | Select or deselect individual object |
|
||||||
|
|
||||||
## Rotation & Movement
|
## Rotation & Movement
|
||||||
|
|
||||||
| Key | Action | Notes |
|
| Key | Action | Notes |
|
||||||
| --- | ------ | ----- |
|
| --------------- | ---------- | ------------------------------------ |
|
||||||
| [arrow] | Rotate 90° | Arrow keys rotate on respective axes |
|
| [arrow] | Rotate 90° | Arrow keys rotate on respective axes |
|
||||||
| [shift + arrow] | Rotate 5° | Fine rotation control |
|
| [shift + arrow] | Rotate 5° | Fine rotation control |
|
||||||
| [alt + arrow] | Move 5mm | Move object in X/Y plane |
|
| [alt + arrow] | Move 5mm | Move object in X/Y plane |
|
||||||
|
|
||||||
## Rendering Modes
|
## Rendering Modes
|
||||||
|
|
||||||
| Key | Action | Notes |
|
| Key | Action | Notes |
|
||||||
| --- | ------ | ----- |
|
| -------------- | ---------------------- | ---------------------------------------- |
|
||||||
| w | Toggle Ghost Rendering | Cycle between solid and semi-transparent |
|
| w | Toggle Ghost Rendering | Cycle between solid and semi-transparent |
|
||||||
| W | Toggle Wireframe | Cycle between solid and wireframe |
|
| W | Toggle Wireframe | Cycle between solid and wireframe |
|
||||||
| Ctrl+W / Cmd+W | Toggle Edge Rendering | Show/hide model edges |
|
| Ctrl+W / Cmd+W | Toggle Edge Rendering | Show/hide model edges |
|
||||||
|
|
||||||
## Settings & Dialogs
|
## Settings & Dialogs
|
||||||
|
|
||||||
| Key | Action | Notes |
|
| Key | Action | Notes |
|
||||||
| --- | ------ | ----- |
|
| --- | ------------------- | -------------------------------- |
|
||||||
| e | Device Dialog | Select and customize devices |
|
| e | Device Dialog | Select and customize devices |
|
||||||
| o | Tool Dialog | CNC mode only |
|
| o | Tool Dialog | CNC mode only |
|
||||||
| q | Preferences Dialog | Change application behaviors |
|
| q | Preferences Dialog | Change application behaviors |
|
||||||
| l | Work Profile Dialog | Load, save, rename work profiles |
|
| l | Work Profile Dialog | Load, save, rename work profiles |
|
||||||
| ? | Help Dialog | Show help and documentation |
|
| ? | Help Dialog | Show help and documentation |
|
||||||
| C | Refresh Catalog | Reload device catalog |
|
| C | Refresh Catalog | Reload device catalog |
|
||||||
|
|
||||||
## Workspace Management
|
## Workspace Management
|
||||||
|
|
||||||
| Key | Action | Notes |
|
| Key | Action | Notes |
|
||||||
| --- | ------ | ----- |
|
| ------ | --------------------- | ------------------------------------------ |
|
||||||
| Ctrl+S | Save Settings | Save current settings |
|
| Ctrl+S | Save Settings | Save current settings |
|
||||||
| Cmd+S | Save Workspace | Save workspace state (macOS) |
|
| Cmd+S | Save Workspace | Save workspace state (macOS) |
|
||||||
| Cmd+L | Restore Workspace | Restore saved workspace (macOS) |
|
| Cmd+L | Restore Workspace | Restore saved workspace (macOS) |
|
||||||
| Z | Reset All Preferences | Clear all settings (requires confirmation) |
|
| Z | Reset All Preferences | Clear all settings (requires confirmation) |
|
||||||
|
|
||||||
## Mouse Controls
|
## Mouse Controls
|
||||||
|
|
||||||
### General Navigation
|
### General Navigation
|
||||||
|
|
||||||
| Input | Action | Notes |
|
| Input | Action | Notes |
|
||||||
| ----- | ------ | ----- |
|
| --------------- | ----------- | ------------------------------- |
|
||||||
| [left + drag] | Rotate View | Orbit camera around focus point |
|
| [left + drag] | Rotate View | Orbit camera around focus point |
|
||||||
| [right + drag] | Pan View | Move camera focus |
|
| [right + drag] | Pan View | Move camera focus |
|
||||||
| [meta + drag] | Pan View | Alternative pan (macOS) |
|
| [meta + drag] | Pan View | Alternative pan (macOS) |
|
||||||
| [middle + drag] | Zoom | Mouse wheel click + drag |
|
| [middle + drag] | Zoom | Mouse wheel click + drag |
|
||||||
| [mouse wheel] | Zoom | Scroll to zoom in/out |
|
| [mouse wheel] | Zoom | Scroll to zoom in/out |
|
||||||
|
|
||||||
### Object Interaction
|
### Object Interaction
|
||||||
|
|
||||||
| Input | Action | Notes |
|
| Input | Action | Notes |
|
||||||
| ----- | ------ | ----- |
|
| -------------- | -------- | ----------------------------------- |
|
||||||
| [ctrl + click] | Lay Flat | Rotate clicked face toward platform |
|
| [ctrl + click] | Lay Flat | Rotate clicked face toward platform |
|
||||||
| [meta + click] | Lay Flat | Alternative (macOS) |
|
| [meta + click] | Lay Flat | Alternative (macOS) |
|
||||||
|
|
||||||
### FDM Support Mode
|
### FDM Support Mode
|
||||||
|
|
||||||
| Input | Action | Notes |
|
| Input | Action | Notes |
|
||||||
| ----- | ------ | ----- |
|
| ---------------------------- | --------------------- | ---------------------------- |
|
||||||
| [left click] | Toggle Support Column | Add or remove support column |
|
| [left click] | Toggle Support Column | Add or remove support column |
|
||||||
| [ctrl + drag] / [cmd + drag] | Erase Columns | Remove support columns |
|
| [ctrl + drag] / [cmd + drag] | Erase Columns | Remove support columns |
|
||||||
| [alt + drag] / [opt + drag] | Draw Columns | Add support columns |
|
| [alt + drag] / [opt + drag] | Draw Columns | Add support columns |
|
||||||
|
|
|
||||||
390
docs/release.md
Normal file
390
docs/release.md
Normal file
|
|
@ -0,0 +1,390 @@
|
||||||
|
# Release Notes
|
||||||
|
|
||||||
|
Full docs @ https://docs.grid.space/projects/kiri-moto
|
||||||
|
|
||||||
|
# Release 4.4.0
|
||||||
|
|
||||||
|
## General
|
||||||
|
|
||||||
|
- add version numbering utility script
|
||||||
|
- split out help/info menu and language menus
|
||||||
|
- move install/uninstall/quit menu to center app menu
|
||||||
|
- add help menu visual callout
|
||||||
|
- improve language translation coverage
|
||||||
|
- improve build and serve workflow for alternate builds
|
||||||
|
- update service worker configuration
|
||||||
|
- add install/uninstall options in setup menu
|
||||||
|
|
||||||
|
## CAM
|
||||||
|
|
||||||
|
- implement tab hopping - toolpath goes up and over tab instead of interrupting cut (fixes #207)
|
||||||
|
- add GPU accelerated contouring for faster operations
|
||||||
|
- add radial GPU raster support (experimental)
|
||||||
|
- add new register mode that marks bottom cutout tabs
|
||||||
|
- add curves-only support to GPU contour
|
||||||
|
- add zsafe and move to safe z at start of new operations
|
||||||
|
- fix 'safe' moves during operation changes to be moves, not cuts
|
||||||
|
- fix tolerance/resolution for GPU raster mesh calculations
|
||||||
|
- fix NullPointerException with tabs in contour
|
||||||
|
- improve merge/co-planar point handling
|
||||||
|
- add slice layer naming using operation notes (fixes #447)
|
||||||
|
- fix level step down units (fixes #446)
|
||||||
|
|
||||||
|
## FDM
|
||||||
|
|
||||||
|
- document remain_time macro variable (refs #339)
|
||||||
|
- re-add base flow rate multiplier for belt mode (fixes #444)
|
||||||
|
- fix belt fan control ordering (fixes #438)
|
||||||
|
- exclude belt from bridge fan layer
|
||||||
|
- add centering feature for fill areas (fixes #448)
|
||||||
|
|
||||||
|
## Mesh:Tool
|
||||||
|
|
||||||
|
- add STEP export capability
|
||||||
|
- add STEP import with face generation
|
||||||
|
- improve face generation with inner holes
|
||||||
|
- add mesh scripting tool and persistence
|
||||||
|
- add plane and plane.loft operations
|
||||||
|
- add global edge map and edge reuse for better topology
|
||||||
|
- add devel device export option
|
||||||
|
|
||||||
|
## Electron
|
||||||
|
|
||||||
|
- clean up electron build process
|
||||||
|
- merge electron mod into core
|
||||||
|
- add service routes when available
|
||||||
|
- set proper content-type for appended code bodies
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- update @gridspace/raster-path to latest version with GPU acceleration
|
||||||
|
- update @gridspace/app-server for proper fallthrough handling
|
||||||
|
- switch from npmjs.org to git repos for @gridspace packages
|
||||||
|
- update WebGPU implementation
|
||||||
|
|
||||||
|
## Bug Fixes
|
||||||
|
|
||||||
|
- fix Bambu Lab local URL and module load order
|
||||||
|
- fix progress reporting for raster operations
|
||||||
|
- fix GPU lathe progress and contour API usage
|
||||||
|
- fix tool offsets when using GPU acceleration
|
||||||
|
- fix indexed rotations epsilon value for accurate zflat calculation
|
||||||
|
- fix traceload debug option
|
||||||
|
- sanitize topo3 resolution to avoid GPU floating-point errors
|
||||||
|
|
||||||
|
# Release 4.3
|
||||||
|
|
||||||
|
## General
|
||||||
|
|
||||||
|
- add lathe mode for CAM operations
|
||||||
|
- add volumetric flow calculations for FDM
|
||||||
|
- improve bundler with better dependency tracking
|
||||||
|
- migrate to ESM module format across codebase
|
||||||
|
- improve arc support in gcode generation
|
||||||
|
|
||||||
|
## FDM
|
||||||
|
|
||||||
|
- add scarf seams for better surface finish
|
||||||
|
- add hole compensation feature
|
||||||
|
- add spiral layer start option
|
||||||
|
- overhaul thin wall detection and handling
|
||||||
|
- improve volumetric flow rate controls
|
||||||
|
- add retraction tuning for better print quality
|
||||||
|
|
||||||
|
## CAM
|
||||||
|
|
||||||
|
- add lathe operation support with threading
|
||||||
|
- improve arc support for smoother toolpaths
|
||||||
|
- add drill from stock top option
|
||||||
|
- improve tab positioning and generation
|
||||||
|
- enhance surface selection and filtering
|
||||||
|
- add 4th axis lathe debug and testing mode
|
||||||
|
- improve tool path optimization
|
||||||
|
|
||||||
|
## Devices
|
||||||
|
|
||||||
|
- add new machine profiles
|
||||||
|
- improve device profile management
|
||||||
|
|
||||||
|
# Release 4.2
|
||||||
|
|
||||||
|
## CAM
|
||||||
|
|
||||||
|
- add arc support to gcode output for smoother paths
|
||||||
|
- add drill from stock top feature
|
||||||
|
- improve contour operations
|
||||||
|
- fix animation issues with shared tool numbers
|
||||||
|
- enhance trace operations with arc support
|
||||||
|
- improve pocket smoothing algorithms
|
||||||
|
|
||||||
|
## FDM
|
||||||
|
|
||||||
|
- improve support generation
|
||||||
|
- add new retraction options
|
||||||
|
- enhance layer time controls
|
||||||
|
|
||||||
|
## General
|
||||||
|
|
||||||
|
- improve gcode arc import and export
|
||||||
|
- fix file loading edge cases
|
||||||
|
- update device profiles
|
||||||
|
|
||||||
|
# Release 4.1
|
||||||
|
|
||||||
|
## CAM
|
||||||
|
|
||||||
|
- improve drill operations and positioning
|
||||||
|
- fix multi-part drilling bugs
|
||||||
|
- enhance trace line selection
|
||||||
|
- add dogbone support to trace ops
|
||||||
|
- improve pocket operation reliability
|
||||||
|
- fix trace selection with flip operations
|
||||||
|
|
||||||
|
## FDM
|
||||||
|
|
||||||
|
- add RatRig machine profiles
|
||||||
|
- improve Bambu Lab device control integration
|
||||||
|
- enhance support placement algorithms
|
||||||
|
- fix belt mode support generation
|
||||||
|
|
||||||
|
## General
|
||||||
|
|
||||||
|
- improve file import handling
|
||||||
|
- fix workspace restore for complex projects
|
||||||
|
- enhance mobile touch interactions
|
||||||
|
- update machine profiles for popular devices
|
||||||
|
|
||||||
|
# Release 4.0 (2024-01-21)
|
||||||
|
|
||||||
|
- major UI refactor
|
||||||
|
- add profile cloud sync
|
||||||
|
- add electron desktop binary builds
|
||||||
|
- add timelines for all cnc op chains
|
||||||
|
- add dragknife, wire-edm, waterjet device types
|
||||||
|
- add gerber import file support
|
||||||
|
- add SVG import dialog, more options
|
||||||
|
- add arbitrary belt slice angle
|
||||||
|
- add mesh sketching, new ui, menus, gears, patterns
|
||||||
|
- fix 3MF imports lacking mesh translation
|
||||||
|
- improve cam animation
|
||||||
|
- clean up and normalize dark mode
|
||||||
|
- optimize docker image size
|
||||||
|
- new machine profiles
|
||||||
|
- dozens of bug fixes
|
||||||
|
|
||||||
|
# Release 3.9 (2023-03-19)
|
||||||
|
|
||||||
|
- more graceful handling of security contexts blocking SharedArrayBuffer
|
||||||
|
- FDM refactor the 'detect' support feature for auto-placing manual supports
|
||||||
|
- FDM fix supports in belt mode
|
||||||
|
- CAM fix issue #230 shared tool numbers cause animation errors
|
||||||
|
- CAM fix surface / trace selection copy on hover/pop/new op
|
||||||
|
- CAM decrease cutting speed when entire tool is engaged in roughing
|
||||||
|
- CAM add dogbones support to traces ops
|
||||||
|
- CAM add scripted contour filtering (to be extended to other ops)
|
||||||
|
- CAM add calculation of taper length from angle
|
||||||
|
- CAM add open poly-line offsetting with trace op
|
||||||
|
- CAM clearly delineate ops reachable or not on timeline
|
||||||
|
- CAM add 4th axis lathe operation for debug and testing (can be optimized)
|
||||||
|
- CAM add rough all stock to aid lathe mode
|
||||||
|
- CAM stock is now always on, whether offset or absolute
|
||||||
|
- CAM add lathe worker parallelization (2x - 6x speedup)
|
||||||
|
- CAM fix pocket/trace selection with flip op
|
||||||
|
|
||||||
|
* CAM fix lathe yellow path in light mode
|
||||||
|
|
||||||
|
# Release 3.8 (2023-01-21)
|
||||||
|
|
||||||
|
- update server-side sample module code
|
||||||
|
- various fixes and improvements to CAM indexing
|
||||||
|
- various fixes and improvements to gcode variables
|
||||||
|
- improve gcode arc import decoding
|
||||||
|
- removed auto-decimation on object import
|
||||||
|
- FDM slicing memory reduction from team lychee
|
||||||
|
- FDM add new parameters, range overrides
|
||||||
|
- FDM fix raft line fill strategy
|
||||||
|
- CAM improve trace line selection with zoom adaptive thresholds
|
||||||
|
- CAM option to control first output point order
|
||||||
|
- CAM move to save Z between ops, refresh spindle speed
|
||||||
|
- CAM add origin offset optional parameters
|
||||||
|
- CAM add control to omit initial tool change
|
||||||
|
- CAM fix vertical face selection and step over defaults
|
||||||
|
- CAM fix multi-part object import / grouping
|
||||||
|
- CAM fix tracing nested polyline offset ordering
|
||||||
|
- CAM refactor of contouring yields 2x - 20x speedup
|
||||||
|
- CAM update traces to async slicing, fix use with flip
|
||||||
|
- CAM add threading support for all slicing ops
|
||||||
|
- CAM add omit pocket option to outline op
|
||||||
|
- CAM add trace support for taper tip diameter
|
||||||
|
- CAM add trace offset override parameter
|
||||||
|
- CAM add leave stock parameter to contour
|
||||||
|
- CAM add contour output density control (reduction)
|
||||||
|
- CAM improve parsing and visualization of large files
|
||||||
|
|
||||||
|
# Release 3.7 (2022-11-12)
|
||||||
|
|
||||||
|
- add CAM axes scaling gcode header directive
|
||||||
|
- add CAM 4th axis indexing support, timeline op, updated visuals
|
||||||
|
- update most CAM ops to work in 4th axis indexing mode
|
||||||
|
- align FDM top/bottom layer options with convention
|
||||||
|
- change Kiri:Moto SVG import to default to boolean repair
|
||||||
|
- add Mesh:Tool SVG import options for extrusion depth and boolean repair
|
||||||
|
- replace jscad/modeling with Manifold project for faster mesh boolean
|
||||||
|
- various CAM gcode, preview, animation fixes (3 axis)
|
||||||
|
- various mobile touch, file load fixes
|
||||||
|
- add FDM slice support growth option to help merging pillars
|
||||||
|
- add CAM tools export / import parity with devices and settings
|
||||||
|
|
||||||
|
# Release 3.6 (2022-10-22)
|
||||||
|
|
||||||
|
## Kiri:Moto
|
||||||
|
|
||||||
|
- add new Carvera machine target in CAM mode with laser support
|
||||||
|
- add laser output operators and device settings in CAM mode
|
||||||
|
- add fullscreen option. button next to user profile
|
||||||
|
- mobile pinch zoom and layer slider usability improvements
|
||||||
|
- update CLI to work in CAM mode and add working samples
|
||||||
|
- improved FDM preview rendering speed and reduced memory usage
|
||||||
|
- threaded task and message passing performance improvements
|
||||||
|
- refactor FDM supports and synthetic widgets to use more common code
|
||||||
|
- allow FDM mixing of automatic and manual / detected supports
|
||||||
|
- improve CAM animation speeds using shared array buffers
|
||||||
|
- improve CAM render quality using solids instead of lines
|
||||||
|
- add CAM leveling part offset parameters for XY and Z
|
||||||
|
- add CAM pocket smoothing and contouring which is closer to true 3 axis
|
||||||
|
- add CAM 3D engraving and marking with the pocket contouring operation
|
||||||
|
- fix CAM invalidation of tabs on scale and traces on scale or rotate
|
||||||
|
- fix belt fan override for base extrusions touching belt
|
||||||
|
- fix belt X axis label order
|
||||||
|
|
||||||
|
# Release 3.5 (2022-10-07)
|
||||||
|
|
||||||
|
## Kiri:Moto
|
||||||
|
|
||||||
|
- add optional service workers and manifest to support full PWA + install
|
||||||
|
- add support to run as Progressive Web Apps for installation and offline use
|
||||||
|
- add assembly import when KM used inside of onshape
|
||||||
|
- add configurable flatness for contour clipping
|
||||||
|
- add faster render mode for FDM slices
|
||||||
|
- add axis label remapping in FDM
|
||||||
|
- add new path rendering engine
|
||||||
|
- add bridging option in CAM contouring
|
||||||
|
- add option to force z max routing in CAM
|
||||||
|
- add option to ignore z bottom in CAM contouring
|
||||||
|
- add CAM pocket option to ignore interior features (outline only)
|
||||||
|
- add CAM Z bottom visualization, make it relative to stock instead of part
|
||||||
|
- add CAM Z bottom inversion option to flip operator
|
||||||
|
- add CAM custom gcode operator (can be used for pausing, too)
|
||||||
|
- add CAM z extend option on registration op independent of "Z Thru" global
|
||||||
|
- add CAM pocket surface selection filter by angle
|
||||||
|
- add optional CAM operation notes (helps with many similar ops)
|
||||||
|
- add option to limit CAM trace ops to Z bottom limit (when in use)
|
||||||
|
- extend url loading of workspaces to all formats
|
||||||
|
- alert when healing is enabled and non-manifold geometries are detected
|
||||||
|
- fix thin output start and end point tracking which broke retraction
|
||||||
|
- fix for importing with some obj formatting
|
||||||
|
- fix profile seeding for newer device record formats
|
||||||
|
- fix workspace import / restore for some file formats
|
||||||
|
- fix potential crash into stock during moves when parts are z bottom anchored
|
||||||
|
|
||||||
|
# Release 3.4 (2022-05-14)
|
||||||
|
|
||||||
|
## Kiri:Moto
|
||||||
|
|
||||||
|
- added batch processing to object adds/removes to speedup complex workspace restore
|
||||||
|
- substitute some prusa slicer [variables] with KM `{variables}` on import
|
||||||
|
- fix CNC output order for tool changes an spindle speed updates
|
||||||
|
- add CNC pocket operation using surface selection
|
||||||
|
- fix dog-bones on outlines cut by tabs
|
||||||
|
- skip pockets that resolve to null
|
||||||
|
- fix CNC contour path collision
|
||||||
|
- 10x speedup for true shadow generation
|
||||||
|
- add FDM gcode feature macros for transitions
|
||||||
|
- add FDM option to alternate shell winding direction
|
||||||
|
- add FDM print time estimate fudge factor for devices
|
||||||
|
- add `clear top` option to CNC outline operation
|
||||||
|
- add FDM layer retraction as a range option
|
||||||
|
|
||||||
|
## Mesh:Tool (1.2.0)
|
||||||
|
|
||||||
|
- auto-fog in wireframe view to aid close mesh inspections
|
||||||
|
- significant speed-up for large surface selections
|
||||||
|
- add boolean operations for subtract and intersect
|
||||||
|
- multi-body identification and isolation
|
||||||
|
- quick add primitives: cube, cylinder
|
||||||
|
- control wireframe transparency
|
||||||
|
- parameterize png image import
|
||||||
|
- code added to show camera focal point
|
||||||
|
- better Z split snapping using vertex closest to mouse
|
||||||
|
|
||||||
|
# Release 3.3 (2022-04-01)
|
||||||
|
|
||||||
|
## Kiri:Moto
|
||||||
|
|
||||||
|
- reorganization of code to use updated dependency loader (gapp)
|
||||||
|
- refactor main into supporting classes (part of a larger ui re-org)
|
||||||
|
- group "main" entry points under "kiri-run"
|
||||||
|
- extract and group utilities from print and other modules
|
||||||
|
- add thin wall pull-down & allow for newer strategies
|
||||||
|
- extract preview render engine from FDM
|
||||||
|
- allow loading of workspaces from url on page load
|
||||||
|
- properly import profiles attached to devices
|
||||||
|
- improved routing on "fast" layers & layers with multiple islands
|
||||||
|
- start/stop minions depending on whether threading enabled
|
||||||
|
- abstract file loading (onshape import, mesh replace, etc)
|
||||||
|
- enable/disable ray intersect path on feature state change
|
||||||
|
- new and updated device profiles: Prusa MK2S/MK3S+, Ender 3
|
||||||
|
- trigger solid layer when transitions lead to 50% projected areas
|
||||||
|
- limit non-manifold solution search depth
|
||||||
|
- refactor slicers to use single improved slice core (cnc deferred)
|
||||||
|
- add parameterized solid projection expansion (infill -> solid expand)
|
||||||
|
- add parameterized control of bridge/flat and infill print speeds
|
||||||
|
- add api control over threading workloads and use of wasm
|
||||||
|
- updates to raft generation: add border, connect infill lines
|
||||||
|
- fix phantom support generation off part or under bed
|
||||||
|
- template vars: nozzles used and layers until next use (IDEX)
|
||||||
|
- fdm export control of preamble comments position (for ultimaker)
|
||||||
|
|
||||||
|
## Mesh:Tool (1.1.0)
|
||||||
|
|
||||||
|
- add surface selection mode
|
||||||
|
- add preferences for normal length and color
|
||||||
|
- add preferences for face selection and surface matching (radians/radius)
|
||||||
|
- add svg and image import conversion (created shared load. libs)
|
||||||
|
- replace triangulation algorithm that was causing some union failures
|
||||||
|
- add pinned log busy spinner
|
||||||
|
- add version chooser
|
||||||
|
- add welcome menu
|
||||||
|
|
||||||
|
# Release 3.2 (2022-02-12)
|
||||||
|
|
||||||
|
https://forum.grid.space/t/kiri-moto-version-3-2/580
|
||||||
|
|
||||||
|
## General
|
||||||
|
|
||||||
|
- Better memory management
|
||||||
|
- Rendering speedups
|
||||||
|
- 3MF instancing support
|
||||||
|
- SVG import improvements
|
||||||
|
- Enable/Disable individual models
|
||||||
|
|
||||||
|
## FDM
|
||||||
|
|
||||||
|
- Improved non-manifold handling
|
||||||
|
- Gcode macro if/then/else code flow
|
||||||
|
- Updated CLI utility
|
||||||
|
- Draft Shields
|
||||||
|
|
||||||
|
## Belt
|
||||||
|
|
||||||
|
- Height-based spacing
|
||||||
|
- Random X layout
|
||||||
|
|
||||||
|
## CNC
|
||||||
|
|
||||||
|
- Drill marking option
|
||||||
|
- Numerous bug fixes
|
||||||
|
|
||||||
|
## Onshape
|
||||||
|
|
||||||
|
- Improved session management
|
||||||
226
docs/void/plan-chamfer-offset-v2.md
Normal file
226
docs/void/plan-chamfer-offset-v2.md
Normal file
|
|
@ -0,0 +1,226 @@
|
||||||
|
# Void Chamfer V2 Plan (Geometric Offset, Not Boolean Cutters)
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Replace the current chamfer implementation based on cutter solids + boolean difference with a deterministic geometric chamfer pipeline that operates directly on mesh topology and face offsets.
|
||||||
|
|
||||||
|
## Why Change
|
||||||
|
|
||||||
|
1. Current path is boolean-driven (`solid/chamfer.js`) and depends on synthetic cutter prisms.
|
||||||
|
2. Boolean chamfer is fragile near:
|
||||||
|
- short edges
|
||||||
|
- dense/curved topology
|
||||||
|
- multi-edge corner interactions
|
||||||
|
- near-coplanar/low-angle neighborhoods
|
||||||
|
3. Provenance and boundary tracking are harder when chamfer is represented as a subtract operation, rather than explicit edge-face reconstruction.
|
||||||
|
4. Debugging and deterministic replay are harder with cutter generation and manifold fallback behavior.
|
||||||
|
|
||||||
|
## Current-State Findings
|
||||||
|
|
||||||
|
1. Chamfer currently:
|
||||||
|
- resolves selected edges
|
||||||
|
- builds cutter meshes from adjacent triangle normals
|
||||||
|
- performs boolean difference
|
||||||
|
- writes resulting body as manifold output
|
||||||
|
|
||||||
|
2. Signals in current code indicate boolean-centric lifecycle:
|
||||||
|
- `manifold_chamfer_passthrough`
|
||||||
|
- `manifold_chamfer_ready`
|
||||||
|
- cutter debug/failure logs
|
||||||
|
|
||||||
|
3. Edge references are already fairly good:
|
||||||
|
- chamfer refs use canonical boundary/segment identities
|
||||||
|
- this is strong input for a topology-based rebuild
|
||||||
|
|
||||||
|
## Target Architecture
|
||||||
|
|
||||||
|
Chamfer becomes a topology/geometry transform, not a subtractive solid operation.
|
||||||
|
|
||||||
|
1. Input:
|
||||||
|
- selected sharp edges (from stable boundary segment refs)
|
||||||
|
- chamfer distance (and later optional asymmetric distances)
|
||||||
|
|
||||||
|
2. Core operation:
|
||||||
|
- for each selected edge, offset its two incident face planes by chamfer distance
|
||||||
|
- intersect offset planes with local wedge to compute chamfer strip geometry
|
||||||
|
- trim neighboring faces and insert chamfer face(s)
|
||||||
|
|
||||||
|
3. Corner resolution:
|
||||||
|
- solve multi-edge vertex neighborhoods explicitly
|
||||||
|
- produce watertight corner patches without global booleans
|
||||||
|
|
||||||
|
4. Output:
|
||||||
|
- rebuilt manifold mesh + updated provenance/boundary mappings
|
||||||
|
- explicit chamfer faces with stable IDs (not anonymous boolean remnants)
|
||||||
|
|
||||||
|
## Data Model / Provenance Updates
|
||||||
|
|
||||||
|
1. Extend chamfer result metadata:
|
||||||
|
- `source_edge_segment_ids[]`
|
||||||
|
- `generated_face_ids[]`
|
||||||
|
- `status: geometric_chamfer_ready`
|
||||||
|
|
||||||
|
2. GeometryStore integration:
|
||||||
|
- chamfer faces emit boundaries/segments directly
|
||||||
|
- chamfer faces carry source edge lineage
|
||||||
|
|
||||||
|
3. Preserve compatibility:
|
||||||
|
- existing docs still readable
|
||||||
|
- optional fallback to legacy boolean chamfer behind feature flag
|
||||||
|
|
||||||
|
## Algorithm Plan
|
||||||
|
|
||||||
|
## Stage A: Topology Extraction
|
||||||
|
|
||||||
|
1. Build edge->incident-face adjacency from input mesh.
|
||||||
|
2. Identify valid chamfer candidates:
|
||||||
|
- manifold edges with exactly two incident faces
|
||||||
|
- non-smooth crease threshold gating
|
||||||
|
|
||||||
|
3. Group selected edges into connected chamfer regions.
|
||||||
|
|
||||||
|
## Stage B: Per-Edge Offset Construction
|
||||||
|
|
||||||
|
1. For each selected edge:
|
||||||
|
- compute incident face normals
|
||||||
|
- construct two offset face planes
|
||||||
|
- compute chamfer line as plane-plane intersection in local neighborhood
|
||||||
|
|
||||||
|
2. Create edge strip endpoints using neighboring trim constraints.
|
||||||
|
|
||||||
|
## Stage C: Face Trimming + Insertion
|
||||||
|
|
||||||
|
1. Trim original incident faces against chamfer boundary lines.
|
||||||
|
2. Insert chamfer quad/tri strip faces.
|
||||||
|
3. Maintain winding and local normal consistency.
|
||||||
|
|
||||||
|
## Stage D: Vertex Corner Solver
|
||||||
|
|
||||||
|
1. At each selected vertex:
|
||||||
|
- collect incoming chamfer strips
|
||||||
|
- solve intersection polygon in tangent frame
|
||||||
|
- triangulate corner patch deterministically
|
||||||
|
|
||||||
|
2. Handle edge cases:
|
||||||
|
- 2-edge corner
|
||||||
|
- n-edge star corner
|
||||||
|
- near-parallel incident faces
|
||||||
|
|
||||||
|
## Stage E: Rebuild + Mapping
|
||||||
|
|
||||||
|
1. Rebuild indexed mesh with new vertices/faces.
|
||||||
|
2. Recompute boundary segments and canonical edge refs.
|
||||||
|
3. Emit provenance mappings:
|
||||||
|
- old edge ref -> new chamfer face/segments
|
||||||
|
- unchanged faces preserve IDs where possible
|
||||||
|
|
||||||
|
## Execution Phases
|
||||||
|
|
||||||
|
## Phase 1: Infrastructure + Feature Flag
|
||||||
|
|
||||||
|
1. Add `chamfer_mode` toggle:
|
||||||
|
- `legacy_boolean` (default initially)
|
||||||
|
- `geometric_offset` (new path)
|
||||||
|
2. Build shared adjacency/topology helpers.
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
1. New path can run no-op safely and fall back cleanly.
|
||||||
|
|
||||||
|
## Phase 2: Single-Edge Geometric Chamfer
|
||||||
|
|
||||||
|
1. Implement robust one-edge chamfer on simple prism/cube cases.
|
||||||
|
2. Add deterministic unit fixtures.
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
1. Single selected edge produces expected geometry with no booleans.
|
||||||
|
|
||||||
|
## Phase 3: Multi-Edge Same-Face + Parallel Chains
|
||||||
|
|
||||||
|
1. Handle multiple selected edges on same body.
|
||||||
|
2. Ensure trim interactions are stable and watertight.
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
1. Common user workflows work without mesh cracks.
|
||||||
|
|
||||||
|
## Phase 4: Corner Solver
|
||||||
|
|
||||||
|
1. Implement n-edge corner patches.
|
||||||
|
2. Add tolerance policy and degeneracy handling.
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
1. Complex corners no longer require boolean fallback.
|
||||||
|
|
||||||
|
## Phase 5: Provenance + GeometryStore Wiring
|
||||||
|
|
||||||
|
1. Emit chamfer-derived boundaries/patch IDs with lineage.
|
||||||
|
2. Update hover/select mapping for chamfer outputs.
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
1. Chamfer boundaries are first-class and traceable.
|
||||||
|
|
||||||
|
## Phase 6: Default Cutover
|
||||||
|
|
||||||
|
1. Make geometric mode default.
|
||||||
|
2. Keep legacy boolean fallback for one release window.
|
||||||
|
3. Remove legacy path after stability window.
|
||||||
|
|
||||||
|
## Testing Plan
|
||||||
|
|
||||||
|
## Unit
|
||||||
|
|
||||||
|
1. Edge adjacency correctness.
|
||||||
|
2. Plane offset/intersection math.
|
||||||
|
3. Corner patch triangulation determinism.
|
||||||
|
4. Degenerate geometry tolerance behavior.
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
|
||||||
|
1. Cube single-edge chamfer.
|
||||||
|
2. Multiple connected edges.
|
||||||
|
3. Concave/convex mixed selections.
|
||||||
|
4. Timeline edits upstream/downstream with stable refs.
|
||||||
|
5. Interaction with boolean-added bodies.
|
||||||
|
|
||||||
|
## Regression
|
||||||
|
|
||||||
|
1. No face holes/non-manifold edges after chamfer.
|
||||||
|
2. No ID churn for unaffected faces.
|
||||||
|
3. Boundary segment refs remain selectable post-chamfer.
|
||||||
|
|
||||||
|
## Risks and Mitigations
|
||||||
|
|
||||||
|
1. Risk: corner solver complexity.
|
||||||
|
- Mitigation: staged rollout with strict fixtures before cutover.
|
||||||
|
|
||||||
|
2. Risk: precision instability on small geometry.
|
||||||
|
- Mitigation: unified epsilon policy + local frame math.
|
||||||
|
|
||||||
|
3. Risk: behavior divergence from existing chamfer expectations.
|
||||||
|
- Mitigation: side-by-side mode comparison tooling and temp dual-run validator.
|
||||||
|
|
||||||
|
## Implementation Touchpoints
|
||||||
|
|
||||||
|
1. `src/void/solid/chamfer.js`
|
||||||
|
- split into legacy boolean and new geometric engine.
|
||||||
|
|
||||||
|
2. `src/void/solid/rebuild.js`
|
||||||
|
- route chamfer feature to mode-specific executor.
|
||||||
|
|
||||||
|
3. `src/void/api/solids.js`
|
||||||
|
- preserve/refit canonical edge mappings after geometric chamfer.
|
||||||
|
- expose chamfer lineage for debug overlays.
|
||||||
|
|
||||||
|
4. `src/void/api/geometry_store.js`
|
||||||
|
- ensure chamfer outputs emit boundary/segment/provenance records consistently.
|
||||||
|
|
||||||
|
## Immediate Next Step
|
||||||
|
|
||||||
|
Implement Phase 1 + Phase 2 in parallel:
|
||||||
|
|
||||||
|
1. Add mode flag and new engine scaffolding.
|
||||||
|
2. Land deterministic single-edge geometric chamfer on planar solids.
|
||||||
150
docs/void/plan-constraints-v2.md
Normal file
150
docs/void/plan-constraints-v2.md
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
# Void Constraints V2 Plan (Planegcs-First, Fallback as Safety Net)
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Move sketch solving to a clean planegcs-first architecture, remove dual-solver behavioral drift, and improve drag/tangent stability.
|
||||||
|
|
||||||
|
## Current Findings
|
||||||
|
|
||||||
|
1. Constraint solving is currently dual-mode by default:
|
||||||
|
- `enforceWithPlanegcs()` runs, then a fallback settle pass is still applied.
|
||||||
|
- This can reintroduce different motion/priority behavior after planegcs already converged.
|
||||||
|
|
||||||
|
2. Drag interactions frequently force fallback:
|
||||||
|
- During drag, `useFallback: tangentDriven || !pointDrag` is used in pointer drag paths.
|
||||||
|
- This bypasses planegcs exactly where stable incremental behavior matters most.
|
||||||
|
|
||||||
|
3. Tangent constraints are not mapped into planegcs in the current mapper:
|
||||||
|
- `toPlanegcsConstraint()` covers many constraints but not sketch `tangent`.
|
||||||
|
- Tangency currently depends on fallback heuristics (`constraints_tangent.js`).
|
||||||
|
|
||||||
|
4. The planegcs wrapper already supports temporary constraints:
|
||||||
|
- Constraint objects with `temporary: true` are supported by the wrapper path.
|
||||||
|
- This enables proper drag-driving constraints with lower priority solving semantics.
|
||||||
|
|
||||||
|
5. `angle_via_point` is available in the solver bindings:
|
||||||
|
- Suitable for robust endpoint tangency encoding (angle = 0 at shared endpoint).
|
||||||
|
- This aligns with FreeCAD guidance for improved stability vs direct tangent formulations in corner cases.
|
||||||
|
|
||||||
|
## Root Cause Summary
|
||||||
|
|
||||||
|
1. Two solvers are actively shaping geometry during interaction.
|
||||||
|
2. Drag logic has explicit fallback preference in key paths.
|
||||||
|
3. Tangency is solved outside planegcs, creating inconsistent convergence and corner-case instability.
|
||||||
|
|
||||||
|
## Target Architecture
|
||||||
|
|
||||||
|
1. Planegcs is the primary and default solver for all live interaction and final settle.
|
||||||
|
2. Fallback solver is retained only as failure recovery.
|
||||||
|
3. Drag uses temporary constraints in planegcs:
|
||||||
|
- Temporary point-to-point/point-to-line style guidance constraints to cursor/ghost references.
|
||||||
|
- No permanent topology mutation from drag constraints.
|
||||||
|
4. Tangency uses planegcs-native representation:
|
||||||
|
- Shared-endpoint tangent: `angle_via_point` with angle = 0.
|
||||||
|
- Non-shared cases: use direct tangent primitives where stable (`tangent_la`, `tangent_aa`, etc.), with endpoint-angle fallback where needed.
|
||||||
|
|
||||||
|
## Phased Execution
|
||||||
|
|
||||||
|
## Phase 1: Instrumentation and Guardrails
|
||||||
|
|
||||||
|
1. Add solver telemetry per enforce call:
|
||||||
|
- planegcs used, fallback used, solve status, elapsed time.
|
||||||
|
2. Add debug toggle to display active temporary constraints during drag.
|
||||||
|
3. Add deterministic logs for tangent constraint path selection.
|
||||||
|
|
||||||
|
Exit Criteria:
|
||||||
|
|
||||||
|
1. We can observe when and why fallback is invoked.
|
||||||
|
|
||||||
|
## Phase 2: Temporary Drag Constraints
|
||||||
|
|
||||||
|
1. Add drag-time temporary constraints (`temporary: true`) in planegcs solve graph.
|
||||||
|
2. Remove drag-path forced fallback defaults.
|
||||||
|
3. Keep fallback only if planegcs solve fails or returns non-converged status.
|
||||||
|
|
||||||
|
Exit Criteria:
|
||||||
|
|
||||||
|
1. Drag no longer “snaps back” from dual-pass disagreement.
|
||||||
|
2. Solver path during normal drag is planegcs-only.
|
||||||
|
|
||||||
|
## Phase 3: Tangent Migration
|
||||||
|
|
||||||
|
1. Implement tangent mapping in `toPlanegcsConstraint()`:
|
||||||
|
- line-arc, arc-arc, line-circle as available.
|
||||||
|
2. For shared-endpoint tangent pairs, map to `angle_via_point` (angle=0).
|
||||||
|
3. Keep old tangent fallback path behind a temporary feature flag for rollback.
|
||||||
|
|
||||||
|
Exit Criteria:
|
||||||
|
|
||||||
|
1. Tangent drag corner cases no longer require tangent-specific fallback aggressiveness.
|
||||||
|
2. Shared-endpoint tangent cases are stable under repeated edits/drag.
|
||||||
|
|
||||||
|
## Phase 4: Remove Default Dual Settle
|
||||||
|
|
||||||
|
1. Remove unconditional fallback settle after successful planegcs solve.
|
||||||
|
2. Fallback runs only on explicit planegcs failure paths.
|
||||||
|
3. Keep compatibility switch (`constraints_v2_force_fallback`) for emergency rollback.
|
||||||
|
|
||||||
|
Exit Criteria:
|
||||||
|
|
||||||
|
1. Single primary solver behavior in normal operation.
|
||||||
|
2. Fewer constraint jitter/regressions from solver disagreement.
|
||||||
|
|
||||||
|
## Phase 5: Cleanup
|
||||||
|
|
||||||
|
1. Simplify pointer drag enforcement call sites.
|
||||||
|
2. Remove tangent-specific fallback tuning knobs that become obsolete.
|
||||||
|
3. Document canonical constraint mapping table and temporary-constraint rules.
|
||||||
|
|
||||||
|
Exit Criteria:
|
||||||
|
|
||||||
|
1. Constraint code paths are materially simpler and easier to reason about.
|
||||||
|
|
||||||
|
## Proposed Code Touchpoints
|
||||||
|
|
||||||
|
1. `src/void/sketch/constraints.js`
|
||||||
|
- Add temporary constraint plumbing and tangent mapping in `toPlanegcsConstraint()`.
|
||||||
|
- Remove unconditional fallback settle on success.
|
||||||
|
|
||||||
|
2. `src/void/sketch/pointer.js`
|
||||||
|
- Replace drag-time fallback preference with planegcs temporary constraints.
|
||||||
|
|
||||||
|
3. `src/void/sketch/constraints_actions.js`
|
||||||
|
- Ensure apply/edit flows use planegcs-first, fallback-on-failure behavior.
|
||||||
|
|
||||||
|
4. `src/void/sketch/constraints_tangent.js`
|
||||||
|
- Transition from primary solver role to compatibility fallback only.
|
||||||
|
|
||||||
|
5. `src/void/solver/sketch/gcs_wrapper.js`
|
||||||
|
- Confirm temporary constraint lifecycle handling and cleanup.
|
||||||
|
|
||||||
|
## Risks and Mitigations
|
||||||
|
|
||||||
|
1. Risk: Regression in legacy sketches tuned around fallback behavior.
|
||||||
|
- Mitigation: feature flag + staged rollout + telemetry.
|
||||||
|
|
||||||
|
2. Risk: Performance regressions during drag with added temporary constraints.
|
||||||
|
- Mitigation: limit temporary constraint count to active dragged subset; cap iterations.
|
||||||
|
|
||||||
|
3. Risk: Incorrect tangent mapping for mixed entity types.
|
||||||
|
- Mitigation: explicit mapping matrix tests per constraint subtype.
|
||||||
|
|
||||||
|
## Validation Plan
|
||||||
|
|
||||||
|
1. Unit tests:
|
||||||
|
- Tangent mapping (shared endpoint and non-shared).
|
||||||
|
- Temporary constraint injection/removal lifecycle.
|
||||||
|
- Planegcs success path without fallback pass.
|
||||||
|
|
||||||
|
2. Interaction tests:
|
||||||
|
- Drag with dimensions, coincident, perpendicular, and tangent combos.
|
||||||
|
- Repeated drag/release cycles without geometric drift.
|
||||||
|
- Circular/grid/polygon pattern interactions under drag.
|
||||||
|
|
||||||
|
3. Regression scenarios:
|
||||||
|
- Known tangent corner cases.
|
||||||
|
- Previously flaky dual-solver “snap back” sketches.
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
Start with Phase 2 (temporary drag constraints + fallback-on-failure only for drag) before full tangent migration. This yields immediate UX improvement and reduces dual-solver interference while keeping rollback safety.
|
||||||
81
docs/void/plan-derived.md
Normal file
81
docs/void/plan-derived.md
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
# Void Derived Sketch Entities Plan
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Make derived sketch entities deterministic, immutable references that rebuild from upstream geometry, like solids rebuild from sketches.
|
||||||
|
|
||||||
|
## Core Model
|
||||||
|
|
||||||
|
- Derived entities are read-only projections of upstream geometry.
|
||||||
|
- Upstream geometry is the source of truth.
|
||||||
|
- Derived entities store source links and projection metadata.
|
||||||
|
|
||||||
|
## Source Links
|
||||||
|
|
||||||
|
Each derived entity should keep:
|
||||||
|
|
||||||
|
- `source_kind` (`sketch-entity`, `solid-boundary`, `solid-face-loop`, etc.)
|
||||||
|
- `source_feature_id`
|
||||||
|
- `source_object_id` (entity/boundary/loop id)
|
||||||
|
- `target_sketch_id`
|
||||||
|
- projection mode/settings (including tessellation preference version)
|
||||||
|
|
||||||
|
## Immutability Rules
|
||||||
|
|
||||||
|
- Allowed: hover, select, delete/unlink.
|
||||||
|
- Disallowed: drag/move/reshape directly.
|
||||||
|
- Solver should not treat derived entities as DOF-driving geometry.
|
||||||
|
|
||||||
|
## Rebuild Rules
|
||||||
|
|
||||||
|
- On upstream change, mark dependent sketches dirty.
|
||||||
|
- Rebuild derived entities in dependency order.
|
||||||
|
- Regeneration is deterministic from source links + target sketch plane.
|
||||||
|
|
||||||
|
## Projection Policy
|
||||||
|
|
||||||
|
- For arcs/circles from non-coplanar sources, derive as projected polyline chains using sketch tessellation prefs.
|
||||||
|
- Do not force preserving source parametric arc/circle form across projection angles.
|
||||||
|
- For coplanar sources, preserving native type can be added later as an optimization.
|
||||||
|
|
||||||
|
## Interaction Rules
|
||||||
|
|
||||||
|
- Derived geometry has distinct visual style/state.
|
||||||
|
- Attempts to edit derived geometry should be blocked with clear UI feedback.
|
||||||
|
- Deleting derived geometry removes the link and generated entities.
|
||||||
|
|
||||||
|
## Failure Handling
|
||||||
|
|
||||||
|
- No silent degenerate fallbacks.
|
||||||
|
- If source cannot be resolved, mark stale/error and surface user-visible status.
|
||||||
|
- Keep structured diagnostic logging for source resolution failures.
|
||||||
|
|
||||||
|
## Pipeline Integration
|
||||||
|
|
||||||
|
- Reuse the existing feature rebuild pipeline model used by solids.
|
||||||
|
- Derived-geometry regeneration should happen as part of sketch feature rebuild.
|
||||||
|
- Dependents downstream of updated sketches should rebuild from regenerated derived geometry.
|
||||||
|
|
||||||
|
## Implementation Phases
|
||||||
|
|
||||||
|
1. Add explicit derived-link schema and immutable behavior gates.
|
||||||
|
2. Route derived entities through rebuild pass (not interactive mutation path).
|
||||||
|
3. Remove fallback editing/solver paths for derived entities.
|
||||||
|
4. Add stale/error UI and repair actions (rebind, delete link).
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- Current state: planned, not implemented end-to-end.
|
||||||
|
- Existing behavior: mixed interactive derive paths with mutable outcomes and fallback logic.
|
||||||
|
- Known pain points: unstable derive outcomes, degenerates, and inconsistent rebuild coupling.
|
||||||
|
|
||||||
|
## Next Milestone
|
||||||
|
|
||||||
|
1. Introduce derived-link schema in sketch entity storage.
|
||||||
|
2. Enforce immutability in interaction layer (block drag/edit, allow select/delete).
|
||||||
|
3. Rebuild derived entities from links during sketch rebuild.
|
||||||
|
4. Verify with regression scenario:
|
||||||
|
- Sketch A -> Extrude A
|
||||||
|
- Sketch B derived from A/solid boundaries
|
||||||
|
- Edit Sketch A
|
||||||
|
- Confirm Sketch B derived entities and downstream solids update deterministically.
|
||||||
234
docs/void/plan-face-provenance.md
Normal file
234
docs/void/plan-face-provenance.md
Normal file
|
|
@ -0,0 +1,234 @@
|
||||||
|
# Void Face Provenance Plan (Boundary-First, Split Faces)
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Track which portions of resulting solids come from which sketch extrude regions, including after union/subtract, while keeping storage compact and spline-ready.
|
||||||
|
|
||||||
|
## Execution Status (2026-03-02)
|
||||||
|
|
||||||
|
Completed:
|
||||||
|
|
||||||
|
1. Plan authored and staged into phased implementation.
|
||||||
|
2. Debug visualization toggles added to preferences and wired to solids runtime:
|
||||||
|
3. Boundary loop rendering from GeometryStore.
|
||||||
|
4. Segment rendering from GeometryStore.
|
||||||
|
5. Segment/surface/region ID labels (overlay text).
|
||||||
|
6. Fixed world/local debug overlay transform bug:
|
||||||
|
7. GeometryStore points are world-space; line geometry parented under solids root must convert world -> root local because `space.WORLD` is rotated -90deg on X.
|
||||||
|
8. Manifold relation passthrough wired through kernel/worker/rebuild (`runIndex`, `runOriginalID`, `faceID`, and source-run solid mapping).
|
||||||
|
9. GeometryStore now emits provenance-partitioned `surface_patches` from per-face triangle run attribution (not just per-face-loop seeds).
|
||||||
|
10. Topology now records `patch_to_tris` and `tri_to_patch` during snapshot build for downstream hover/selection cutover.
|
||||||
|
11. Debug boundary rendering now prefers patch boundaries when present, so visualization aligns with sketch-derived/provenance splits.
|
||||||
|
|
||||||
|
In progress:
|
||||||
|
|
||||||
|
1. Cut over hover/selection resolvers from face-loop heuristics to patch-first entities (`surface_patch_id` canonical path).
|
||||||
|
2. Improve partition quality from triangle boundary approximation to robust boundary arrangement where needed.
|
||||||
|
|
||||||
|
Next up:
|
||||||
|
|
||||||
|
1. Add explicit `surface_patch_id` in hit/canonical selection entities.
|
||||||
|
2. Bind extrude-profile hover/select directly to patch/source-region maps.
|
||||||
|
3. Add regression fixtures for boolean unions/subtracts with mixed curved + planar outputs.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
1. Primary provenance is `boundary/region/surface-patch`, not raw triangle ownership.
|
||||||
|
2. Triangle ownership is derived runtime index only (`tri -> surface_patch_id`) and can be rebuilt.
|
||||||
|
3. Multi-source output faces must be split into multiple bounded surface patches so each patch has one canonical source region.
|
||||||
|
4. Line/arc support ships first; segment model must support future spline kinds without schema redesign.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
In scope:
|
||||||
|
|
||||||
|
1. Extrude + boolean provenance tracking through rebuild pipeline.
|
||||||
|
2. Face splitting by source-region boundaries.
|
||||||
|
3. GeometryStore schema extension for stable patch-level IDs and source refs.
|
||||||
|
4. Runtime mapping from picks (`face/edge`) to canonical patch and source region.
|
||||||
|
|
||||||
|
Out of scope (initial pass):
|
||||||
|
|
||||||
|
1. Native spline feature authoring.
|
||||||
|
2. Long-lived persisted triangle provenance tables.
|
||||||
|
3. Non-planar sketch-on-surface expansion beyond current behavior.
|
||||||
|
|
||||||
|
## Data Model Changes
|
||||||
|
|
||||||
|
Add/extend document `geometry_store` entities:
|
||||||
|
|
||||||
|
1. `segments[]`
|
||||||
|
2. `kind: line | arc | spline`
|
||||||
|
3. `geom`: kind-specific payload
|
||||||
|
4. `sampled_polyline` (optional cache for hit testing/partitioning)
|
||||||
|
|
||||||
|
5. `boundaries[]`
|
||||||
|
6. Ordered `segment_ids`
|
||||||
|
7. `closed`, orientation, optional parent/child nesting
|
||||||
|
|
||||||
|
8. `regions[]`
|
||||||
|
9. `outer_boundary_id`
|
||||||
|
10. `hole_boundary_ids[]`
|
||||||
|
11. `source`: canonical source ref (`profile:<sketch>:<profile>`)
|
||||||
|
|
||||||
|
12. New `surface_patches[]`
|
||||||
|
13. `id`
|
||||||
|
14. `surface_id` (geometric carrier face)
|
||||||
|
15. `boundary_ids[]` (outer + holes)
|
||||||
|
16. `source_region_id` (single canonical owner)
|
||||||
|
17. `source_feature_id` (extrude feature)
|
||||||
|
18. `solid_id`
|
||||||
|
19. `status` (`direct`, `boolean-derived`, `rebound`)
|
||||||
|
|
||||||
|
20. New runtime-only `topology.patch_tri_index`
|
||||||
|
21. Maps mesh triangles to `surface_patch_id` for selection/render acceleration.
|
||||||
|
|
||||||
|
## Kernel Boundary Changes (Manifold)
|
||||||
|
|
||||||
|
Current kernel adapter only round-trips positions/indices; relation metadata is dropped.
|
||||||
|
|
||||||
|
Planned update:
|
||||||
|
|
||||||
|
1. Preserve Manifold mesh relation fields where available (`runOriginalID`, `faceID`, related run metadata).
|
||||||
|
2. Carry relation metadata through `extrudePolygons()` and `booleanMeshes()`.
|
||||||
|
3. Emit relation-aware intermediate records to rebuild stage (not directly persisted).
|
||||||
|
|
||||||
|
This enables deterministic attribution from boolean output back to input generated solids/regions before patch splitting.
|
||||||
|
|
||||||
|
## Provenance Build Pipeline
|
||||||
|
|
||||||
|
### Stage A: Sketch Region Capture
|
||||||
|
|
||||||
|
1. Keep current closed-loop profile extraction for line/arc.
|
||||||
|
2. Emit canonical `region_id = profile:<sketch>:<profile>`.
|
||||||
|
3. Record region boundaries using generic segment schema (`line|arc` now, `spline` later).
|
||||||
|
|
||||||
|
### Stage B: Extrude Seed Patches
|
||||||
|
|
||||||
|
1. Extrude each selected sketch region.
|
||||||
|
2. Seed cap/side patch candidates with direct source region refs.
|
||||||
|
3. Preserve manifold relation fields in intermediate mesh record.
|
||||||
|
|
||||||
|
### Stage C: Boolean Attribution
|
||||||
|
|
||||||
|
1. Perform add/subtract/intersect with relation-carrying meshes.
|
||||||
|
2. Build attribution map from output primitives/runs to source seed patches.
|
||||||
|
3. Mark ambiguous/mixed carrier surfaces for partitioning.
|
||||||
|
|
||||||
|
### Stage D: Face Partitioning (Split Multi-Source Faces)
|
||||||
|
|
||||||
|
1. For each mixed carrier surface, project contributing source boundaries to surface-local space.
|
||||||
|
2. Build planar arrangement, split into disjoint bounded cells.
|
||||||
|
3. Assign each cell a single `source_region_id` by relation majority + geometric tie-break.
|
||||||
|
4. Emit one `surface_patch` per bounded cell.
|
||||||
|
|
||||||
|
### Stage E: Runtime Topology Index
|
||||||
|
|
||||||
|
1. Build `tri -> surface_patch_id` map from patch partition output.
|
||||||
|
2. Use map for selection hit resolution and hover highlighting.
|
||||||
|
3. Rebuild index each solids rebuild; do not persist large triangle maps.
|
||||||
|
|
||||||
|
## Selection/Interaction Integration
|
||||||
|
|
||||||
|
1. Face pick resolves to `surface_patch_id` first, then `source_region_id`.
|
||||||
|
2. Edge/boundary pick resolves to `boundary_id`/`segment_id` that belongs to a patch.
|
||||||
|
3. Extrude-profile hover/highlight uses `source_region_id -> surface_patch[]` mapping.
|
||||||
|
4. Remove fallback heuristics that infer provenance only from coarse `source.profile_keys`.
|
||||||
|
|
||||||
|
## Storage and Performance
|
||||||
|
|
||||||
|
1. Persist compact canonical graph (`segments/boundaries/regions/surface_patches`).
|
||||||
|
2. Keep triangle-level maps runtime-only to avoid doc bloat and instability across remeshes.
|
||||||
|
3. Cache partition signatures per carrier surface to avoid full repartition when unchanged.
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
### Phase 1: Schema and Adapters
|
||||||
|
|
||||||
|
1. Add `surface_patches` schema and runtime index container.
|
||||||
|
2. Introduce generic segment schema (`kind + geom`) with current line/arc emitters.
|
||||||
|
3. Add compatibility normalizer for older docs (missing `surface_patches`).
|
||||||
|
|
||||||
|
### Phase 2: Kernel Metadata Plumbing
|
||||||
|
|
||||||
|
1. Extend solid kernel adapter to preserve manifold relation metadata.
|
||||||
|
2. Pass relation metadata through worker/main rebuild paths.
|
||||||
|
|
||||||
|
### Phase 3: Patch Builder
|
||||||
|
|
||||||
|
1. Implement mixed-face detection.
|
||||||
|
2. Implement local-space boundary arrangement and patch emission.
|
||||||
|
3. Add deterministic patch IDs/signatures.
|
||||||
|
|
||||||
|
### Phase 4: Resolver Cutover
|
||||||
|
|
||||||
|
1. Switch face selection from coarse face groups to `surface_patch` entities.
|
||||||
|
2. Update properties/tree hover mapping to patch/source-region links.
|
||||||
|
|
||||||
|
### Phase 5: Cleanup
|
||||||
|
|
||||||
|
1. Remove coarse provenance fallbacks once parity is validated.
|
||||||
|
2. Keep compatibility reader for older docs without `surface_patches`.
|
||||||
|
|
||||||
|
## Validation Plan
|
||||||
|
|
||||||
|
Unit tests:
|
||||||
|
|
||||||
|
1. Region extraction determinism (line/arc).
|
||||||
|
2. Mixed-face partitioning into disjoint bounded patches.
|
||||||
|
3. Single-owner assignment per patch.
|
||||||
|
4. Deterministic patch IDs under stable input.
|
||||||
|
|
||||||
|
Integration tests:
|
||||||
|
|
||||||
|
1. Two extrudes unioned: top face splits by source boundary and highlights per profile.
|
||||||
|
2. Subtract operation: surviving walls/caps retain correct source region refs.
|
||||||
|
3. Edit upstream sketch profile: downstream patch mapping updates without manual repair.
|
||||||
|
4. Rebuild in worker vs main thread yields identical patch/source mapping.
|
||||||
|
|
||||||
|
Regression guardrails:
|
||||||
|
|
||||||
|
1. No persisted triangle tables in doc snapshots.
|
||||||
|
2. No schema changes required to add spline segment kind later.
|
||||||
|
3. Selection never reports mixed-source face entities.
|
||||||
|
4. Any debug/runtime geometry under solids root must explicitly convert GeometryStore world coordinates to root-local coordinates.
|
||||||
|
5. Boundary debug rendering must use GeometryStore (`boundaries` / `surface_patches`) as source of truth, not reconstructed sketch-plane loops.
|
||||||
|
|
||||||
|
## Implementation Checklist
|
||||||
|
|
||||||
|
Phase 1:
|
||||||
|
|
||||||
|
1. Done: update `src/void/api/geometry_store.js` schema to include `surface_patches` and runtime topology patch map container.
|
||||||
|
2. Done: extend `buildGeometryStoreSnapshot()` in `src/void/api/solids.js` to emit seeded `surface_patches` per face-loop with source-region candidate fields.
|
||||||
|
3. In progress: keep current face-key canonical mapping intact while adding optional patch ID fields.
|
||||||
|
|
||||||
|
Phase 2:
|
||||||
|
|
||||||
|
1. Update `src/void/solid/kernel.js` mesh conversion to preserve manifold relation fields:
|
||||||
|
2. Input pass-through where provided (`runOriginalID`, `runIndex`, `faceID`, etc.).
|
||||||
|
3. Output pass-through into rebuild intermediate structures.
|
||||||
|
4. Add worker payload support for relation arrays when present.
|
||||||
|
|
||||||
|
Phase 3:
|
||||||
|
|
||||||
|
1. Implement mixed-source detection in `src/void/solid/rebuild.js`.
|
||||||
|
2. Add per-surface local partition pass and emit patch boundaries.
|
||||||
|
3. Assign one canonical `source_region_id` per patch.
|
||||||
|
|
||||||
|
Phase 4:
|
||||||
|
|
||||||
|
1. Extend selection resolver and solids hit mapping to prefer patch IDs over raw face IDs.
|
||||||
|
2. Add properties/tree hover mapping from extrude profile -> patch IDs.
|
||||||
|
3. Remove coarse fallback once parity checks pass.
|
||||||
|
|
||||||
|
Phase 5:
|
||||||
|
|
||||||
|
1. Add deterministic integration tests for union/subtract split-face provenance.
|
||||||
|
2. Remove temporary migration branches and finalize docs.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
1. Every selectable resulting face area maps to exactly one `source_region_id`.
|
||||||
|
2. Multi-source carrier faces are visibly and topologically split at source boundaries.
|
||||||
|
3. Extrude profile hover/select maps accurately to resulting solid patches after booleans.
|
||||||
|
4. GeometryStore stays compact and stable; triangle mapping is derived at runtime.
|
||||||
185
docs/void/plan-geomgraph.md
Normal file
185
docs/void/plan-geomgraph.md
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
# Void Geometry Graph Plan (Surfaces + Boundaries)
|
||||||
|
|
||||||
|
## Progress Checkpoint (2026-02-13)
|
||||||
|
|
||||||
|
1. Completed:
|
||||||
|
2. `GeometryStore` is persisted in document state and populated from solids runtime snapshots.
|
||||||
|
3. Selection pipeline was split to `interact/selection_resolver.js` and now emits typed candidates (`profile/solid-face/solid-edge`) with canonical entity descriptors.
|
||||||
|
4. Extrude targets carry `region_id`; chamfer edge refs carry `boundary_segment_id` + entity metadata.
|
||||||
|
5. New canonical mapping bridge is active in solids selection:
|
||||||
|
6. face key -> canonical `surface` id
|
||||||
|
7. edge key -> canonical `boundary-segment` id
|
||||||
|
8. loop edge key -> canonical `boundary` id
|
||||||
|
9. Resolver now consumes these mappings (`resolveCanonicalFaceEntity`, `resolveCanonicalEdgeEntity`) as primary IDs.
|
||||||
|
|
||||||
|
## Resume Pointers
|
||||||
|
|
||||||
|
1. Canonical mapping source:
|
||||||
|
2. `src/void/api/solids.js`:
|
||||||
|
3. `buildGeometryStoreSnapshot()` map population
|
||||||
|
4. `resolveCanonicalFaceEntity()`
|
||||||
|
5. `resolveCanonicalEdgeEntity()`
|
||||||
|
6. Canonical mapping consumer:
|
||||||
|
7. `src/void/interact/selection_resolver.js`
|
||||||
|
8. `resolvePrimarySurfaceHit()` entity assignment for face/edge candidates.
|
||||||
|
9. Canonical hard cutover completed:
|
||||||
|
10. Extrude profile refs resolve via `region_id` only.
|
||||||
|
11. Chamfer refs resolve via canonical `segment:*` / `boundary:*` ids only.
|
||||||
|
12. `input.solids` compatibility branches removed from solid-op edit paths.
|
||||||
|
13. Boundary-hover correction applied:
|
||||||
|
14. Face-edge resolution now prioritizes hovered-face boundary lookup before global solid edge intersections.
|
||||||
|
15. Boundary extraction uses the same crease threshold as rendered edges (`SOLID_CREASE_ANGLE_DEG`) to reduce partial/extra loop mismatches.
|
||||||
|
16. `getFaceEdgeHit()` now prefers closed-loop boundaries over open seam chains when both are near cursor.
|
||||||
|
17. Follow-up TODO:
|
||||||
|
18. During sketch editing, `Use (u)` should derive from other visible sketch entities (not only solids).
|
||||||
|
|
||||||
|
## Target Architecture
|
||||||
|
|
||||||
|
1. Adopt a single geometric graph centered on `surfaces` and `boundaries`, with solids as derived artifacts only.
|
||||||
|
2. Treat every selectable thing as one of:
|
||||||
|
3. `SurfaceRegion` (planar/non-planar bounded patch).
|
||||||
|
4. `Boundary` (closed loop on a surface).
|
||||||
|
5. `BoundarySegment` (line/arc/spline edge piece inside a boundary).
|
||||||
|
6. `BoundaryPoint` (segment endpoint, midpoint, center, intersection, projected point).
|
||||||
|
|
||||||
|
## Core Data Model
|
||||||
|
|
||||||
|
1. Add `GeometryStore` (document-persisted, versioned):
|
||||||
|
2. `surface_id`, `type` (`planar|curved`), `frame` (for planar), `source` provenance.
|
||||||
|
3. `boundary_id`, `surface_id`, ordered `segment_ids`, orientation, closure, area sign, nesting depth.
|
||||||
|
4. `segment_id`, `kind` (`line|arc|circle|polyline|nurbs`), param data, `owner_boundary_id`.
|
||||||
|
5. `point_id`, world/local coords, role (`endpoint|midpoint|center|derived|intersection`), references.
|
||||||
|
6. `region_id`, `surface_id`, boundary rings (`outer + holes`), selectable extrusion/chamfer profile token.
|
||||||
|
7. Add stable identity layer:
|
||||||
|
8. Every entity gets deterministic `geom_sig` + persistent `id`.
|
||||||
|
9. Derived entities keep `origin_ref` and dependency chain for rebind/rebuild.
|
||||||
|
10. Add `TopologyIndex` (runtime cache):
|
||||||
|
11. Maps solid mesh triangles/edges to source `surface_id` and `boundary_segment_id`.
|
||||||
|
12. Supports nearest-hit resolution with tolerance and tie-break rules.
|
||||||
|
|
||||||
|
## Selection and Hover System
|
||||||
|
|
||||||
|
1. Replace mode-specific picking with one `SelectionResolver` pipeline.
|
||||||
|
2. Input: ray hits + current mode + intent mask.
|
||||||
|
3. Output: ranked candidates of `point/segment/boundary/surface/region`.
|
||||||
|
4. Ranking rules:
|
||||||
|
5. Point proximity always wins near threshold.
|
||||||
|
6. Segment wins next when distance-to-curve below threshold.
|
||||||
|
7. Boundary/region wins when inside region and not near point/segment.
|
||||||
|
8. Surface wins otherwise.
|
||||||
|
9. Add per-mode intent masks:
|
||||||
|
10. Sketch mode: `point,segment,boundary,surface(region projection)`.
|
||||||
|
11. Extrude mode: `region` only.
|
||||||
|
12. Chamfer mode: `segment` only (plus face-to-edge expansion helper).
|
||||||
|
13. Boolean mode: `surface/body` selection groups.
|
||||||
|
14. Add consistent multi-select semantics:
|
||||||
|
15. Plain click toggles in active picker.
|
||||||
|
16. `space/esc` clear picker-local selection.
|
||||||
|
17. No cross-picker stealing while dialog active.
|
||||||
|
|
||||||
|
## Geometry Build Pipeline
|
||||||
|
|
||||||
|
1. Introduce staged rebuild in worker:
|
||||||
|
2. Stage A: Feature evaluation -> sketch geometry on target surfaces.
|
||||||
|
3. Stage B: Boundary graph build per surface (loop extraction, splitting, nesting).
|
||||||
|
4. Stage C: Region synthesis (`outer/holes`) with fill-rule consistency.
|
||||||
|
5. Stage D: Solid operations (extrude/boolean/chamfer) from region/surface references.
|
||||||
|
6. Stage E: Topology annotation back into `GeometryStore` for interaction.
|
||||||
|
7. Cache keys:
|
||||||
|
8. `feature_sig`, `surface_sig`, `boundary_sig`, `region_sig`.
|
||||||
|
9. Recompute only invalidated downstream stages.
|
||||||
|
|
||||||
|
## Sketch Integration
|
||||||
|
|
||||||
|
1. Sketches bind to `surface_id` + local frame, not transient face index.
|
||||||
|
2. `Use (u)` creates `derived segment/point` referencing source `segment_id/point_id`.
|
||||||
|
3. Derived geometry stores transform relation to host surface frame.
|
||||||
|
4. Midpoints become first-class `point_id` with constraint targetability.
|
||||||
|
5. Closed-area detection uses `boundary/region` graph directly (no separate ad-hoc fill path).
|
||||||
|
|
||||||
|
## Extrude Integration
|
||||||
|
|
||||||
|
1. Extrude input stores selected `region_id`s, not ad-hoc loops.
|
||||||
|
2. Region hover/selection always from boundary graph.
|
||||||
|
3. Live preview reads from current region snapshots.
|
||||||
|
4. Targets/tools in add/subtract reference resulting body ids, but source remains region-driven.
|
||||||
|
|
||||||
|
## Chamfer Integration
|
||||||
|
|
||||||
|
1. Chamfer input stores `boundary_segment_id`s (or section ids for partial edges).
|
||||||
|
2. Face click expands to boundary segments by adjacency policy.
|
||||||
|
3. No selection against regenerated transient mesh during edit.
|
||||||
|
4. Preview and final solve read same boundary refs to avoid drift.
|
||||||
|
|
||||||
|
## Projection / Derive Reliability
|
||||||
|
|
||||||
|
1. Stop pre-projecting everything.
|
||||||
|
2. Resolve hovered source entity first.
|
||||||
|
3. Project only selected/hovered entity into current sketch frame.
|
||||||
|
4. Keep source and projected visuals separate but linked by shared entity id.
|
||||||
|
|
||||||
|
## Planar/Non-Planar Classification
|
||||||
|
|
||||||
|
1. Surface classification at creation:
|
||||||
|
2. Planar stores orthonormal frame and scalar offset.
|
||||||
|
3. Curved stores param evaluator and principal directions where available.
|
||||||
|
4. Boundary segments on curved surfaces can still be selected/extruded/chamfered as references.
|
||||||
|
5. Sketch-on-face initially allowed only on planar surfaces; curved support can be staged later.
|
||||||
|
|
||||||
|
## Document Persistence
|
||||||
|
|
||||||
|
1. Persist `GeometryStore` plus feature list and timeline.
|
||||||
|
2. Persist only canonical geometry entities, not transient mesh selections.
|
||||||
|
3. Add schema version bump and hard reset path (allowed in this project stage).
|
||||||
|
4. Undo/redo stores deltas against `GeometryStore` entities for atomic operations.
|
||||||
|
|
||||||
|
## Migration Strategy
|
||||||
|
|
||||||
|
1. Phase 0: Add new store in parallel, keep existing runtime behavior.
|
||||||
|
2. Phase 1: Route hover/selection to `SelectionResolver` while existing builders stay.
|
||||||
|
3. Phase 2: Route sketch profiles/areas to `boundary/region`.
|
||||||
|
4. Phase 3: Route extrude inputs to `region_id`.
|
||||||
|
5. Phase 4: Route chamfer inputs to `segment_id`.
|
||||||
|
6. Phase 5: Remove legacy face/edge ad-hoc paths.
|
||||||
|
7. Forward-only: remove rollout toggles and legacy branching once parity is reached.
|
||||||
|
|
||||||
|
## Performance and Worker Plan
|
||||||
|
|
||||||
|
1. Keep all heavy geometry graph and topology steps in worker.
|
||||||
|
2. Main thread receives compact immutable snapshots and draw buffers.
|
||||||
|
3. Use incremental invalidation by dependency DAG from edited feature forward.
|
||||||
|
4. Add rebuild budget logging per stage to catch regressions early.
|
||||||
|
|
||||||
|
## Testing Plan
|
||||||
|
|
||||||
|
1. Unit tests:
|
||||||
|
2. Boundary extraction and nesting.
|
||||||
|
3. Region fill-rule behavior (self-intersecting + nested bulls-eye cases).
|
||||||
|
4. Selection ranking with tolerance.
|
||||||
|
5. Projection correctness for points/segments/surfaces.
|
||||||
|
6. Integration tests:
|
||||||
|
7. Sketch-on-face propagation after upstream edits.
|
||||||
|
8. Extrude profile row hover maps to produced solids.
|
||||||
|
9. Chamfer edit stability with multi-select.
|
||||||
|
10. Undo/redo atomicity in dialogs.
|
||||||
|
11. Fuzz tests:
|
||||||
|
12. Random sketch mutations + constraints + rebuild consistency checks.
|
||||||
|
13. Determinism check: same doc state yields same entity ids/topology signatures.
|
||||||
|
|
||||||
|
## Deliverables Sequence
|
||||||
|
|
||||||
|
1. `GeometryStore` + ids + schema.
|
||||||
|
2. `SelectionResolver` + mode masks.
|
||||||
|
3. `BoundaryGraphBuilder` + `RegionBuilder`.
|
||||||
|
4. Extrude refactor to `region_id`.
|
||||||
|
5. Chamfer refactor to `segment_id`.
|
||||||
|
6. Projection/use refactor to source-first selection.
|
||||||
|
7. Legacy path removal and cleanup docs.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
1. Hover/select behavior is identical and predictable across sketch/solid/chamfer modes.
|
||||||
|
2. Derived sketches follow upstream changes without requiring manual edit-open.
|
||||||
|
3. Extrude/chamfer operate on stable references, not transient mesh hits.
|
||||||
|
4. Nested/self-intersecting regions behave correctly for selection and extrusion.
|
||||||
|
5. No mode where selection silently switches entity class unexpectedly.
|
||||||
0
mods/proxy/.debug
Normal file
0
mods/proxy/.debug
Normal file
0
mods/proxy/.electron
Normal file
0
mods/proxy/.electron
Normal file
55
mods/proxy/init.js
Normal file
55
mods/proxy/init.js
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
module.exports = async (server) => {
|
||||||
|
|
||||||
|
const { api, env, handler, path, util } = server;
|
||||||
|
|
||||||
|
server.inject("kiri", "main.js");
|
||||||
|
|
||||||
|
if (!(env.debug || env.electron)) {
|
||||||
|
util.log('not a valid context for proxy');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
path.full({
|
||||||
|
"/printer/print/start": proxy_post,
|
||||||
|
"/server/files/upload": proxy_post,
|
||||||
|
"/api/files/local": proxy_post,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function proxy_post(req, res, next) {
|
||||||
|
handler.addCORS(req, res);
|
||||||
|
if (req.method === 'POST') {
|
||||||
|
let { url, headers } = req;
|
||||||
|
let chunks = [];
|
||||||
|
let host = headers['x-host'];
|
||||||
|
let apik = headers['x-api-key'] ?? '';
|
||||||
|
let cont = headers['content-type'] ?? 'application/binary';
|
||||||
|
req
|
||||||
|
.on('data', data => chunks.push(data) )
|
||||||
|
.on('end', () => {
|
||||||
|
req.app.post = Buffer.concat(chunks);
|
||||||
|
if (host) {
|
||||||
|
fetch(host + url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': cont,
|
||||||
|
'X-Api-Key': apik
|
||||||
|
},
|
||||||
|
body: req.app.post
|
||||||
|
}).then(result => {
|
||||||
|
if (result.ok) {
|
||||||
|
res.writeHead(200, 'OK');
|
||||||
|
} else {
|
||||||
|
res.writeHead(500, 'Failed to proxy');
|
||||||
|
console.log({ result });
|
||||||
|
}
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('drop proxy due to lack of host');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
3
mods/proxy/main.js
Normal file
3
mods/proxy/main.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
self.kiri.load(api => {
|
||||||
|
api.feature.proxy = true;
|
||||||
|
}, 'Proxy');
|
||||||
119
notes.md
119
notes.md
|
|
@ -1,119 +0,0 @@
|
||||||
# Grid:Apps Future Notes
|
|
||||||
|
|
||||||
## `C` cosmetic, `F` functional, `P` performance, `B` bug fix
|
|
||||||
|
|
||||||
* `F` option to hide platform when viewing from below
|
|
||||||
|
|
||||||
# Kiri:Moto
|
|
||||||
|
|
||||||
* `B` origin (and bed size) bug (Onshape?) when switching device modes
|
|
||||||
* `B` can't drag slider bar on ipad / ios -- touch pad scrolling dodgy
|
|
||||||
* `B` prevent or ask for really large models when scaling (crash ui)
|
|
||||||
|
|
||||||
* `P` refactor vertex replacement and widget update matrix tracking
|
|
||||||
* `P` duplicate objects should share same slice data unless rotated or scaled
|
|
||||||
* `P` move all persisted / workspace settings/data to IndexedDB (LS limitations)
|
|
||||||
* `P` faster ray intersect https://github.com/gkjohnson/three-mesh-bvh/
|
|
||||||
* `P` try material clipping planes for slice range selection
|
|
||||||
* `P` switch png lib to https://github.com/photopea/UPNG.js
|
|
||||||
|
|
||||||
* `F` edit in Mesh:Tool
|
|
||||||
* `F` custom device vars for profiles / ranges / gcode
|
|
||||||
* `F` show slider range values in workspace units (on hover?)
|
|
||||||
* `F` allow select of a range by typing in values in slices or workspace units
|
|
||||||
* `F` complete and expose grouping feature
|
|
||||||
* `F` add svgnest-like arrange algorithm
|
|
||||||
* `F` date column and sorting in recent files list
|
|
||||||
* `F` highlight part outside workspace bounds (like can neg Z shader)
|
|
||||||
|
|
||||||
# FDM
|
|
||||||
|
|
||||||
* `B` fix support projection/end with grouped parts
|
|
||||||
* `B` multi-extruder rendering of raft fails to offset the rest of the print
|
|
||||||
* `B` multi-extruder purge blocks fail to generate properly for rafts
|
|
||||||
|
|
||||||
* `F` new parameters for bridging speed / bridge fan control
|
|
||||||
* `F` convert ranges to z offsets while continuing to show layer #
|
|
||||||
* `F` support pillar top/bottom should conform to part
|
|
||||||
* `F` more explicit line width control with ranges and min/max adaptive
|
|
||||||
* `F` test outlining solid projected areas (internally)
|
|
||||||
* `F` gradient infill https://www.youtube.com/watch?v=hq53gsYREHU&feature=emb_logo
|
|
||||||
* `F` segment large flat areas on first layer to mitigate peeling
|
|
||||||
* `F` option to support interior bridges when 0% infill
|
|
||||||
* `F` calculate filament use per extruder per print
|
|
||||||
* `F` expand internal supporting flats / solids before projection (threshold)
|
|
||||||
|
|
||||||
* `P` auto purge pillars when quick layers are detected for extra cooling
|
|
||||||
* `P` solid fill the tops of supports for down facing flats
|
|
||||||
|
|
||||||
# FDM - BELT
|
|
||||||
|
|
||||||
* `B` auto bed resizing leaves the origin in the wrong place at first
|
|
||||||
* `B` re-add progress calls for all work units
|
|
||||||
|
|
||||||
* `F` test and enable arcs in belt more
|
|
||||||
* `F` slightly angle supports to lean into the Z of the part
|
|
||||||
* `F` anchors should be generated anywhere needed in the print, not just head
|
|
||||||
|
|
||||||
# CAM
|
|
||||||
|
|
||||||
* `B` rapid moves should be max of terrain zmax and last cut layer height (roughing)
|
|
||||||
* `B` feed rate for next tool set before tool change (push/pop feed rates?)
|
|
||||||
* `B` tabs do not properly track widget mirror events
|
|
||||||
* `B` contour does not honor clip to stock
|
|
||||||
|
|
||||||
* `F` add lathe step down to eliminate the need for roughing
|
|
||||||
* `F` allow import, rotation, scaling of stock
|
|
||||||
* `F` get gcode coordinates off a part with point/click or hover?
|
|
||||||
* `F` include tools in default devices (Carvera)
|
|
||||||
* `F` add `match faces` option in `outline` operation
|
|
||||||
* `F` add {progress} substitution and maybe {time-remaining} if can be calc'd
|
|
||||||
* `F` import and follow 2D paths (conformed like pocket contours)
|
|
||||||
* `F` add `plunge max` to contouring that can override z feed limit
|
|
||||||
* `F` add lead-in milling (requires adding clamp / no go areas)
|
|
||||||
* `F` add linear clearing strategy
|
|
||||||
* `F` add adaptive clearing strategy
|
|
||||||
* `F` add support for tapered ball mills
|
|
||||||
* `F` change color of line selection in trace op when not a closed poly
|
|
||||||
* `F` limit cut depth to flute length of selected tool (or warn)
|
|
||||||
* `F` validate muti-part layout and spacing exceeds largest outside tool diameter
|
|
||||||
* `F` trapezoidal tabs in Z
|
|
||||||
* `F` ease-in and ease-out especially on tab cut-out start/stop
|
|
||||||
* `F` maintain several part orientations + op chains in a single profile
|
|
||||||
|
|
||||||
* `P` outer outside corners as arc moves
|
|
||||||
* `P` improve parser - do not require spaced tokens and support implied G0 / G1
|
|
||||||
* `P` log Z interpolation for contour XYZ moves
|
|
||||||
* `P` option to start with the smallest poly by area on layer change
|
|
||||||
* `P` redo all path route / planning in prepare to account for terrain before camOut
|
|
||||||
|
|
||||||
# Laser
|
|
||||||
|
|
||||||
* `F` add PLT / HP-GL output format (https://en.wikipedia.org/wiki/HP-GL)
|
|
||||||
|
|
||||||
# OctoPrint plugin
|
|
||||||
|
|
||||||
* subfolder parameter for dropped files
|
|
||||||
* auto-kick check box in preferences
|
|
||||||
|
|
||||||
# Mesh:Tool
|
|
||||||
|
|
||||||
* add undo/redo
|
|
||||||
* add TextGeometry
|
|
||||||
* https://threejs.org/docs/#examples/en/geometries/TextGeometry
|
|
||||||
* https://dustinpfister.github.io/2023/07/05/threejs-text-geometry/
|
|
||||||
* font to path with https://github.com/paulzi/svg-text-to-path
|
|
||||||
* click to repair normals based on a known good (selected) face
|
|
||||||
* send to Kiri:Moto workspace (or update model vertices in place)
|
|
||||||
* better z snap using just vertexes from face intersected
|
|
||||||
* add section view. local clip. raycast skip points above plane
|
|
||||||
* add decimate op = face reduction
|
|
||||||
* add flatten/crush op: for z bottoms (or surfaces?)
|
|
||||||
* allow setting model/group origin for scale/rotate
|
|
||||||
* fix mirror to work with groups (just models currently)
|
|
||||||
* add analyze results dialog
|
|
||||||
* remove/hide auto-repair function
|
|
||||||
|
|
||||||
# Other
|
|
||||||
|
|
||||||
* preferred icons https://icons.getbootstrap.com/
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "grid-apps",
|
"name": "grid-apps",
|
||||||
"version": "4.6.0",
|
"version": "4.7.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",
|
||||||
|
|
@ -41,6 +41,7 @@
|
||||||
"@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.1.1",
|
"@gridspace/raster-path": "^1.1.1",
|
||||||
|
"@salusoft89/planegcs": "^1.1.7",
|
||||||
"@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",
|
||||||
|
|
@ -77,7 +78,7 @@
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
"docusaurus-lunr-search": "^3.6.0",
|
"docusaurus-lunr-search": "^3.6.0",
|
||||||
"dotenv": "latest",
|
"dotenv": "latest",
|
||||||
"electron": "^35.0.1",
|
"electron": "^40.0.0-beta.6",
|
||||||
"electron-builder": "^24.9.1",
|
"electron-builder": "^24.9.1",
|
||||||
"esbuild": "^0.25.5",
|
"esbuild": "^0.25.5",
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
|
|
@ -103,6 +104,7 @@
|
||||||
"clean": "rm -rf alt build data dist src/pack src.old tmp web.old .bcache",
|
"clean": "rm -rf alt build data dist src/pack src.old tmp web.old .bcache",
|
||||||
"dev": "npm run pack-dev && gs-app-server --debug --single",
|
"dev": "npm run pack-dev && gs-app-server --debug --single",
|
||||||
"docs-build": "docusaurus build --config conf/docusaurus.config.js",
|
"docs-build": "docusaurus build --config conf/docusaurus.config.js",
|
||||||
|
"docs-clean": "prettier --config ./conf/prettier.config.js ./docs --write",
|
||||||
"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 --port 4004",
|
"docs-serve": "docusaurus serve --config conf/docusaurus.config.js --port 4004",
|
||||||
|
|
@ -121,7 +123,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 && npm run webpack-qjs",
|
"webpack-ext": "npm run webpack-three && npm run webpack-zip && npm run webpack-qjs && npm run webpack-pgcs",
|
||||||
|
"webpack-pgcs": "mkdir -p src/void/solver/planegcs_dist src/void/solver/sketch && cp node_modules/@salusoft89/planegcs/dist/index.js src/void/solver/planegcs.js && cp -R node_modules/@salusoft89/planegcs/dist/planegcs_dist/. src/void/solver/planegcs_dist/ && cp -R node_modules/@salusoft89/planegcs/dist/sketch/. src/void/solver/sketch/",
|
||||||
"webpack-qjs": "npx webpack --config bin/webpack-quickjs-esm.js",
|
"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",
|
||||||
|
|
|
||||||
401
release.md
401
release.md
|
|
@ -1,401 +0,0 @@
|
||||||
# Release Notes
|
|
||||||
|
|
||||||
Full docs @ https://docs.grid.space/projects/kiri-moto
|
|
||||||
|
|
||||||
# Release 4.4.0
|
|
||||||
|
|
||||||
## General
|
|
||||||
|
|
||||||
* add version numbering utility script
|
|
||||||
* split out help/info menu and language menus
|
|
||||||
* move install/uninstall/quit menu to center app menu
|
|
||||||
* add help menu visual callout
|
|
||||||
* improve language translation coverage
|
|
||||||
* improve build and serve workflow for alternate builds
|
|
||||||
* update service worker configuration
|
|
||||||
* add install/uninstall options in setup menu
|
|
||||||
|
|
||||||
## CAM
|
|
||||||
|
|
||||||
* implement tab hopping - toolpath goes up and over tab instead of interrupting cut (fixes #207)
|
|
||||||
* add GPU accelerated contouring for faster operations
|
|
||||||
* add radial GPU raster support (experimental)
|
|
||||||
* add new register mode that marks bottom cutout tabs
|
|
||||||
* add curves-only support to GPU contour
|
|
||||||
* add zsafe and move to safe z at start of new operations
|
|
||||||
* fix 'safe' moves during operation changes to be moves, not cuts
|
|
||||||
* fix tolerance/resolution for GPU raster mesh calculations
|
|
||||||
* fix NullPointerException with tabs in contour
|
|
||||||
* improve merge/co-planar point handling
|
|
||||||
* add slice layer naming using operation notes (fixes #447)
|
|
||||||
* fix level step down units (fixes #446)
|
|
||||||
|
|
||||||
## FDM
|
|
||||||
|
|
||||||
* document remain_time macro variable (refs #339)
|
|
||||||
* re-add base flow rate multiplier for belt mode (fixes #444)
|
|
||||||
* fix belt fan control ordering (fixes #438)
|
|
||||||
* exclude belt from bridge fan layer
|
|
||||||
* add centering feature for fill areas (fixes #448)
|
|
||||||
|
|
||||||
## Mesh:Tool
|
|
||||||
|
|
||||||
* add STEP export capability
|
|
||||||
* add STEP import with face generation
|
|
||||||
* improve face generation with inner holes
|
|
||||||
* add mesh scripting tool and persistence
|
|
||||||
* add plane and plane.loft operations
|
|
||||||
* add global edge map and edge reuse for better topology
|
|
||||||
* add devel device export option
|
|
||||||
|
|
||||||
## Electron
|
|
||||||
|
|
||||||
* clean up electron build process
|
|
||||||
* merge electron mod into core
|
|
||||||
* add service routes when available
|
|
||||||
* set proper content-type for appended code bodies
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
* update @gridspace/raster-path to latest version with GPU acceleration
|
|
||||||
* update @gridspace/app-server for proper fallthrough handling
|
|
||||||
* switch from npmjs.org to git repos for @gridspace packages
|
|
||||||
* update WebGPU implementation
|
|
||||||
|
|
||||||
## Bug Fixes
|
|
||||||
|
|
||||||
* fix Bambu Lab local URL and module load order
|
|
||||||
* fix progress reporting for raster operations
|
|
||||||
* fix GPU lathe progress and contour API usage
|
|
||||||
* fix tool offsets when using GPU acceleration
|
|
||||||
* fix indexed rotations epsilon value for accurate zflat calculation
|
|
||||||
* fix traceload debug option
|
|
||||||
* sanitize topo3 resolution to avoid GPU floating-point errors
|
|
||||||
|
|
||||||
|
|
||||||
# Release 4.3
|
|
||||||
|
|
||||||
## General
|
|
||||||
|
|
||||||
* add lathe mode for CAM operations
|
|
||||||
* add volumetric flow calculations for FDM
|
|
||||||
* improve bundler with better dependency tracking
|
|
||||||
* migrate to ESM module format across codebase
|
|
||||||
* improve arc support in gcode generation
|
|
||||||
|
|
||||||
## FDM
|
|
||||||
|
|
||||||
* add scarf seams for better surface finish
|
|
||||||
* add hole compensation feature
|
|
||||||
* add spiral layer start option
|
|
||||||
* overhaul thin wall detection and handling
|
|
||||||
* improve volumetric flow rate controls
|
|
||||||
* add retraction tuning for better print quality
|
|
||||||
|
|
||||||
## CAM
|
|
||||||
|
|
||||||
* add lathe operation support with threading
|
|
||||||
* improve arc support for smoother toolpaths
|
|
||||||
* add drill from stock top option
|
|
||||||
* improve tab positioning and generation
|
|
||||||
* enhance surface selection and filtering
|
|
||||||
* add 4th axis lathe debug and testing mode
|
|
||||||
* improve tool path optimization
|
|
||||||
|
|
||||||
## Devices
|
|
||||||
|
|
||||||
* add new machine profiles
|
|
||||||
* improve device profile management
|
|
||||||
|
|
||||||
|
|
||||||
# Release 4.2
|
|
||||||
|
|
||||||
## CAM
|
|
||||||
|
|
||||||
* add arc support to gcode output for smoother paths
|
|
||||||
* add drill from stock top feature
|
|
||||||
* improve contour operations
|
|
||||||
* fix animation issues with shared tool numbers
|
|
||||||
* enhance trace operations with arc support
|
|
||||||
* improve pocket smoothing algorithms
|
|
||||||
|
|
||||||
## FDM
|
|
||||||
|
|
||||||
* improve support generation
|
|
||||||
* add new retraction options
|
|
||||||
* enhance layer time controls
|
|
||||||
|
|
||||||
## General
|
|
||||||
|
|
||||||
* improve gcode arc import and export
|
|
||||||
* fix file loading edge cases
|
|
||||||
* update device profiles
|
|
||||||
|
|
||||||
|
|
||||||
# Release 4.1
|
|
||||||
|
|
||||||
## CAM
|
|
||||||
|
|
||||||
* improve drill operations and positioning
|
|
||||||
* fix multi-part drilling bugs
|
|
||||||
* enhance trace line selection
|
|
||||||
* add dogbone support to trace ops
|
|
||||||
* improve pocket operation reliability
|
|
||||||
* fix trace selection with flip operations
|
|
||||||
|
|
||||||
## FDM
|
|
||||||
|
|
||||||
* add RatRig machine profiles
|
|
||||||
* improve Bambu Lab device control integration
|
|
||||||
* enhance support placement algorithms
|
|
||||||
* fix belt mode support generation
|
|
||||||
|
|
||||||
## General
|
|
||||||
|
|
||||||
* improve file import handling
|
|
||||||
* fix workspace restore for complex projects
|
|
||||||
* enhance mobile touch interactions
|
|
||||||
* update machine profiles for popular devices
|
|
||||||
|
|
||||||
|
|
||||||
# Release 4.0 (2024-01-21)
|
|
||||||
|
|
||||||
* major UI refactor
|
|
||||||
* add profile cloud sync
|
|
||||||
* add electron desktop binary builds
|
|
||||||
* add timelines for all cnc op chains
|
|
||||||
* add dragknife, wire-edm, waterjet device types
|
|
||||||
* add gerber import file support
|
|
||||||
* add SVG import dialog, more options
|
|
||||||
* add arbitrary belt slice angle
|
|
||||||
* add mesh sketching, new ui, menus, gears, patterns
|
|
||||||
* fix 3MF imports lacking mesh translation
|
|
||||||
* improve cam animation
|
|
||||||
* clean up and normalize dark mode
|
|
||||||
* optimize docker image size
|
|
||||||
* new machine profiles
|
|
||||||
* dozens of bug fixes
|
|
||||||
|
|
||||||
|
|
||||||
# Release 3.9 (2023-03-19)
|
|
||||||
|
|
||||||
* more graceful handling of security contexts blocking SharedArrayBuffer
|
|
||||||
* FDM refactor the 'detect' support feature for auto-placing manual supports
|
|
||||||
* FDM fix supports in belt mode
|
|
||||||
* CAM fix issue #230 shared tool numbers cause animation errors
|
|
||||||
* CAM fix surface / trace selection copy on hover/pop/new op
|
|
||||||
* CAM decrease cutting speed when entire tool is engaged in roughing
|
|
||||||
* CAM add dogbones support to traces ops
|
|
||||||
* CAM add scripted contour filtering (to be extended to other ops)
|
|
||||||
* CAM add calculation of taper length from angle
|
|
||||||
* CAM add open poly-line offsetting with trace op
|
|
||||||
* CAM clearly delineate ops reachable or not on timeline
|
|
||||||
* CAM add 4th axis lathe operation for debug and testing (can be optimized)
|
|
||||||
* CAM add rough all stock to aid lathe mode
|
|
||||||
* CAM stock is now always on, whether offset or absolute
|
|
||||||
* CAM add lathe worker parallelization (2x - 6x speedup)
|
|
||||||
* CAM fix pocket/trace selection with flip op
|
|
||||||
- CAM fix lathe yellow path in light mode
|
|
||||||
|
|
||||||
|
|
||||||
# Release 3.8 (2023-01-21)
|
|
||||||
|
|
||||||
* update server-side sample module code
|
|
||||||
* various fixes and improvements to CAM indexing
|
|
||||||
* various fixes and improvements to gcode variables
|
|
||||||
* improve gcode arc import decoding
|
|
||||||
* removed auto-decimation on object import
|
|
||||||
* FDM slicing memory reduction from team lychee
|
|
||||||
* FDM add new parameters, range overrides
|
|
||||||
* FDM fix raft line fill strategy
|
|
||||||
* CAM improve trace line selection with zoom adaptive thresholds
|
|
||||||
* CAM option to control first output point order
|
|
||||||
* CAM move to save Z between ops, refresh spindle speed
|
|
||||||
* CAM add origin offset optional parameters
|
|
||||||
* CAM add control to omit initial tool change
|
|
||||||
* CAM fix vertical face selection and step over defaults
|
|
||||||
* CAM fix multi-part object import / grouping
|
|
||||||
* CAM fix tracing nested polyline offset ordering
|
|
||||||
* CAM refactor of contouring yields 2x - 20x speedup
|
|
||||||
* CAM update traces to async slicing, fix use with flip
|
|
||||||
* CAM add threading support for all slicing ops
|
|
||||||
* CAM add omit pocket option to outline op
|
|
||||||
* CAM add trace support for taper tip diameter
|
|
||||||
* CAM add trace offset override parameter
|
|
||||||
* CAM add leave stock parameter to contour
|
|
||||||
* CAM add contour output density control (reduction)
|
|
||||||
* CAM improve parsing and visualization of large files
|
|
||||||
|
|
||||||
|
|
||||||
# Release 3.7 (2022-11-12)
|
|
||||||
|
|
||||||
* add CAM axes scaling gcode header directive
|
|
||||||
* add CAM 4th axis indexing support, timeline op, updated visuals
|
|
||||||
* update most CAM ops to work in 4th axis indexing mode
|
|
||||||
* align FDM top/bottom layer options with convention
|
|
||||||
* change Kiri:Moto SVG import to default to boolean repair
|
|
||||||
* add Mesh:Tool SVG import options for extrusion depth and boolean repair
|
|
||||||
* replace jscad/modeling with Manifold project for faster mesh boolean
|
|
||||||
* various CAM gcode, preview, animation fixes (3 axis)
|
|
||||||
* various mobile touch, file load fixes
|
|
||||||
* add FDM slice support growth option to help merging pillars
|
|
||||||
* add CAM tools export / import parity with devices and settings
|
|
||||||
|
|
||||||
|
|
||||||
# Release 3.6 (2022-10-22)
|
|
||||||
|
|
||||||
## Kiri:Moto
|
|
||||||
|
|
||||||
* add new Carvera machine target in CAM mode with laser support
|
|
||||||
* add laser output operators and device settings in CAM mode
|
|
||||||
* add fullscreen option. button next to user profile
|
|
||||||
* mobile pinch zoom and layer slider usability improvements
|
|
||||||
* update CLI to work in CAM mode and add working samples
|
|
||||||
* improved FDM preview rendering speed and reduced memory usage
|
|
||||||
* threaded task and message passing performance improvements
|
|
||||||
* refactor FDM supports and synthetic widgets to use more common code
|
|
||||||
* allow FDM mixing of automatic and manual / detected supports
|
|
||||||
* improve CAM animation speeds using shared array buffers
|
|
||||||
* improve CAM render quality using solids instead of lines
|
|
||||||
* add CAM leveling part offset parameters for XY and Z
|
|
||||||
* add CAM pocket smoothing and contouring which is closer to true 3 axis
|
|
||||||
* add CAM 3D engraving and marking with the pocket contouring operation
|
|
||||||
* fix CAM invalidation of tabs on scale and traces on scale or rotate
|
|
||||||
* fix belt fan override for base extrusions touching belt
|
|
||||||
* fix belt X axis label order
|
|
||||||
|
|
||||||
|
|
||||||
# Release 3.5 (2022-10-07)
|
|
||||||
|
|
||||||
## Kiri:Moto
|
|
||||||
|
|
||||||
* add optional service workers and manifest to support full PWA + install
|
|
||||||
* add support to run as Progressive Web Apps for installation and offline use
|
|
||||||
* add assembly import when KM used inside of onshape
|
|
||||||
* add configurable flatness for contour clipping
|
|
||||||
* add faster render mode for FDM slices
|
|
||||||
* add axis label remapping in FDM
|
|
||||||
* add new path rendering engine
|
|
||||||
* add bridging option in CAM contouring
|
|
||||||
* add option to force z max routing in CAM
|
|
||||||
* add option to ignore z bottom in CAM contouring
|
|
||||||
* add CAM pocket option to ignore interior features (outline only)
|
|
||||||
* add CAM Z bottom visualization, make it relative to stock instead of part
|
|
||||||
* add CAM Z bottom inversion option to flip operator
|
|
||||||
* add CAM custom gcode operator (can be used for pausing, too)
|
|
||||||
* add CAM z extend option on registration op independent of "Z Thru" global
|
|
||||||
* add CAM pocket surface selection filter by angle
|
|
||||||
* add optional CAM operation notes (helps with many similar ops)
|
|
||||||
* add option to limit CAM trace ops to Z bottom limit (when in use)
|
|
||||||
* extend url loading of workspaces to all formats
|
|
||||||
* alert when healing is enabled and non-manifold geometries are detected
|
|
||||||
* fix thin output start and end point tracking which broke retraction
|
|
||||||
* fix for importing with some obj formatting
|
|
||||||
* fix profile seeding for newer device record formats
|
|
||||||
* fix workspace import / restore for some file formats
|
|
||||||
* fix potential crash into stock during moves when parts are z bottom anchored
|
|
||||||
|
|
||||||
|
|
||||||
# Release 3.4 (2022-05-14)
|
|
||||||
|
|
||||||
## Kiri:Moto
|
|
||||||
|
|
||||||
* added batch processing to object adds/removes to speedup complex workspace restore
|
|
||||||
* substitute some prusa slicer [variables] with KM {variables} on import
|
|
||||||
* fix CNC output order for tool changes an spindle speed updates
|
|
||||||
* add CNC pocket operation using surface selection
|
|
||||||
* fix dog-bones on outlines cut by tabs
|
|
||||||
* skip pockets that resolve to null
|
|
||||||
* fix CNC contour path collision
|
|
||||||
* 10x speedup for true shadow generation
|
|
||||||
* add FDM gcode feature macros for transitions
|
|
||||||
* add FDM option to alternate shell winding direction
|
|
||||||
* add FDM print time estimate fudge factor for devices
|
|
||||||
* add `clear top` option to CNC outline operation
|
|
||||||
* add FDM layer retraction as a range option
|
|
||||||
|
|
||||||
## Mesh:Tool (1.2.0)
|
|
||||||
|
|
||||||
* auto-fog in wireframe view to aid close mesh inspections
|
|
||||||
* significant speed-up for large surface selections
|
|
||||||
* add boolean operations for subtract and intersect
|
|
||||||
* multi-body identification and isolation
|
|
||||||
* quick add primitives: cube, cylinder
|
|
||||||
* control wireframe transparency
|
|
||||||
* parameterize png image import
|
|
||||||
* code added to show camera focal point
|
|
||||||
* better Z split snapping using vertex closest to mouse
|
|
||||||
|
|
||||||
|
|
||||||
# Release 3.3 (2022-04-01)
|
|
||||||
|
|
||||||
## Kiri:Moto
|
|
||||||
|
|
||||||
* reorganization of code to use updated dependency loader (gapp)
|
|
||||||
* refactor main into supporting classes (part of a larger ui re-org)
|
|
||||||
* group "main" entry points under "kiri-run"
|
|
||||||
* extract and group utilities from print and other modules
|
|
||||||
* add thin wall pull-down & allow for newer strategies
|
|
||||||
* extract preview render engine from FDM
|
|
||||||
* allow loading of workspaces from url on page load
|
|
||||||
* properly import profiles attached to devices
|
|
||||||
* improved routing on "fast" layers & layers with multiple islands
|
|
||||||
* start/stop minions depending on whether threading enabled
|
|
||||||
* abstract file loading (onshape import, mesh replace, etc)
|
|
||||||
* enable/disable ray intersect path on feature state change
|
|
||||||
* new and updated device profiles: Prusa MK2S/MK3S+, Ender 3
|
|
||||||
* trigger solid layer when transitions lead to 50% projected areas
|
|
||||||
* limit non-manifold solution search depth
|
|
||||||
* refactor slicers to use single improved slice core (cnc deferred)
|
|
||||||
* add parameterized solid projection expansion (infill -> solid expand)
|
|
||||||
* add parameterized control of bridge/flat and infill print speeds
|
|
||||||
* add api control over threading workloads and use of wasm
|
|
||||||
* updates to raft generation: add border, connect infill lines
|
|
||||||
* fix phantom support generation off part or under bed
|
|
||||||
* template vars: nozzles used and layers until next use (IDEX)
|
|
||||||
* fdm export control of preamble comments position (for ultimaker)
|
|
||||||
|
|
||||||
## Mesh:Tool (1.1.0)
|
|
||||||
|
|
||||||
* add surface selection mode
|
|
||||||
* add preferences for normal length and color
|
|
||||||
* add preferences for face selection and surface matching (radians/radius)
|
|
||||||
* add svg and image import conversion (created shared load. libs)
|
|
||||||
* replace triangulation algorithm that was causing some union failures
|
|
||||||
* add pinned log busy spinner
|
|
||||||
* add version chooser
|
|
||||||
* add welcome menu
|
|
||||||
|
|
||||||
|
|
||||||
# Release 3.2 (2022-02-12)
|
|
||||||
|
|
||||||
https://forum.grid.space/t/kiri-moto-version-3-2/580
|
|
||||||
|
|
||||||
## General
|
|
||||||
|
|
||||||
* Better memory management
|
|
||||||
* Rendering speedups
|
|
||||||
* 3MF instancing support
|
|
||||||
* SVG import improvements
|
|
||||||
* Enable/Disable individual models
|
|
||||||
|
|
||||||
## FDM
|
|
||||||
|
|
||||||
* Improved non-manifold handling
|
|
||||||
* Gcode macro if/then/else code flow
|
|
||||||
* Updated CLI utility
|
|
||||||
* Draft Shields
|
|
||||||
|
|
||||||
## Belt
|
|
||||||
|
|
||||||
* Height-based spacing
|
|
||||||
* Random X layout
|
|
||||||
|
|
||||||
## CNC
|
|
||||||
|
|
||||||
* Drill marking option
|
|
||||||
* Numerous bug fixes
|
|
||||||
|
|
||||||
## Onshape
|
|
||||||
|
|
||||||
* Improved session management
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
// this is called from the `app.js` function `initModule()` around line `200`
|
|
||||||
// from there you can get the structure passed in the "server" object
|
|
||||||
// which includes and `api` and other helper functions
|
|
||||||
|
|
||||||
module.exports = function(server) {
|
|
||||||
|
|
||||||
server.util.log("--- sample server-side module installed ---");
|
|
||||||
|
|
||||||
// insert script after all others in kiri main code
|
|
||||||
server.inject("kiri", "kiri.js", {end: true});
|
|
||||||
|
|
||||||
// insert script after all others in kiri worker code
|
|
||||||
server.inject("kiri_work", "work.js", {end: true});
|
|
||||||
|
|
||||||
server.onload(() => {
|
|
||||||
server.util.log("--- called after all modules loaded ---");
|
|
||||||
});
|
|
||||||
|
|
||||||
// adding URL endpoints on the server
|
|
||||||
server.path.full({
|
|
||||||
// register the endpoint "/postit"
|
|
||||||
"/postit": (req, res, next) => {
|
|
||||||
let chunks = [];
|
|
||||||
req.on('data', data => {
|
|
||||||
chunks.push(data.toString());
|
|
||||||
});
|
|
||||||
req.on('end', () => {
|
|
||||||
let data = chunks.join('');
|
|
||||||
server.util.log({server_received: data});
|
|
||||||
res.end(`received ${data.length} bytes`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
console.log('--- kiri main module start ---');
|
|
||||||
|
|
||||||
// the kiri api starts around line `260` of `src/kiri/main.js`
|
|
||||||
kiri.load(function(api) {
|
|
||||||
console.log('--- kiri main module started ---');
|
|
||||||
|
|
||||||
// send data to our "/postit" endpoint
|
|
||||||
fetch("/postit", {
|
|
||||||
method: "POST",
|
|
||||||
body: "this is a test of the POST url endpoint"
|
|
||||||
}).then(res => res.text()).then(text => {
|
|
||||||
console.log({server_said: text});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
# Application modules
|
|
||||||
|
|
||||||
This allows for the loading of server-side modules which can, in turn,
|
|
||||||
inject browser modules into applications like Kiri:Moto.
|
|
||||||
|
|
||||||
To enable the sample module:
|
|
||||||
|
|
||||||
* create a `mod` directory at the root of grid-apps
|
|
||||||
* copy the sample directory into the mod directory: `mod/sample`
|
|
||||||
* (re)start `gs-app-server`
|
|
||||||
* you will see log lines at start-up similar to this
|
|
||||||
|
|
||||||
```
|
|
||||||
220128.120432 '[head]' { module: './mod/sample/init.js' }
|
|
||||||
220128.120432 '[head]' '--- sample server-side module loaded ---'
|
|
||||||
```
|
|
||||||
|
|
||||||
* reloading Kiri:Moto from localhost, you will see these javascript
|
|
||||||
* console log messages indicaing the new module code is running
|
|
||||||
|
|
||||||
```
|
|
||||||
--- kiri main module start ---
|
|
||||||
--- kiri main module started ---
|
|
||||||
{server_said: 'received 39 bytes'}
|
|
||||||
--- kiri worker module start ---
|
|
||||||
--- kiri worker module started ---
|
|
||||||
```
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
console.log('--- kiri worker module start ---');
|
|
||||||
|
|
||||||
kiri.load(function(api) {
|
|
||||||
console.log('--- kiri worker module started ---');
|
|
||||||
|
|
||||||
// augment worker code here. for example,
|
|
||||||
// to ovveride fdm extrusion calculations
|
|
||||||
// kiri.driver.FDM.extrudeMM = function(dist, perMM, factor) {
|
|
||||||
// return dist * perMM * factor;
|
|
||||||
// };
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
@ -121,7 +121,7 @@ export function poly2polyEmit(array, startPoint, emitter, opt = {}) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let area = poly.open ? 1 : poly.area();
|
let area = poly.open ? 1 : poly.area();
|
||||||
poly.forEachPoint(function (point, index) {
|
poly.forEachPoint((point, index) => {
|
||||||
dist = opt.weight ?
|
dist = opt.weight ?
|
||||||
startPoint.distTo3D(point) * area * area :
|
startPoint.distTo3D(point) * area * area :
|
||||||
startPoint.distTo2D(point);
|
startPoint.distTo2D(point);
|
||||||
|
|
|
||||||
|
|
@ -74,10 +74,23 @@ const POLYS = {
|
||||||
union,
|
union,
|
||||||
unionFaces,
|
unionFaces,
|
||||||
xor,
|
xor,
|
||||||
|
verify
|
||||||
};
|
};
|
||||||
|
|
||||||
export { POLYS };
|
export { POLYS };
|
||||||
|
|
||||||
|
export function verify(polys) {
|
||||||
|
polys.forEach(p => {
|
||||||
|
if (!p.open && p.length < 3) console.trace('SHORT', p);
|
||||||
|
if (!p.open && p.area() < 0.001) console.trace('SMALL', p);
|
||||||
|
p.points.forEach(p => {
|
||||||
|
if (isNaN(p.x) || isNaN(p.y) ||isNaN(p.z)) {
|
||||||
|
console.trace('NaN', p);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function outer(polys) {
|
export function outer(polys) {
|
||||||
for (let p of polys) {
|
for (let p of polys) {
|
||||||
p.inner = undefined;
|
p.inner = undefined;
|
||||||
|
|
@ -136,7 +149,7 @@ export function fromClipperNode(tnode, z) {
|
||||||
export function fromClipperTree(tnode, z, tops, parent, minarea) {
|
export function fromClipperTree(tnode, z, tops, parent, minarea) {
|
||||||
let poly,
|
let poly,
|
||||||
polys = tops || [],
|
polys = tops || [],
|
||||||
min = numOrDefault(minarea, 0.1);
|
min = minarea ?? 0.1;
|
||||||
|
|
||||||
for (let child of tnode.m_Childs) {
|
for (let child of tnode.m_Childs) {
|
||||||
poly = fromClipperNode(child, z);
|
poly = fromClipperNode(child, z);
|
||||||
|
|
@ -150,7 +163,7 @@ export function fromClipperTree(tnode, z, tops, parent, minarea) {
|
||||||
polys.push(poly);
|
polys.push(poly);
|
||||||
}
|
}
|
||||||
if (child.m_Childs) {
|
if (child.m_Childs) {
|
||||||
fromClipperTree(child, z, polys, parent ? null : poly, minarea);
|
fromClipperTree(child, z, polys, parent ? null : poly, min);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -641,13 +654,15 @@ export function xor(set, z) {
|
||||||
* @param {Polygon[]} setB mask set
|
* @param {Polygon[]} setB mask set
|
||||||
* @returns {Polygon[]}
|
* @returns {Polygon[]}
|
||||||
*/
|
*/
|
||||||
export function trimTo(setA, setB) {
|
export function trimTo(setA, setB, opt = {}) {
|
||||||
// handle null/empty slices
|
// handle null/empty slices
|
||||||
if (setA === setB || setA === null || setB === null) return null;
|
if (setA === setB || setA === null || setB === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
let out = [], tmp;
|
let out = [], tmp;
|
||||||
util.doCombinations(setA, setB, {}, function(a, b) {
|
util.doCombinations(setA, setB, {}, (a, b) => {
|
||||||
if (tmp = a.mask(b)) {
|
if (tmp = a.mask(b, opt.nullEq, opt.minArea)) {
|
||||||
out.appendAll(tmp);
|
out.appendAll(tmp);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ export async function slice(points, options = {}) {
|
||||||
zSum = 0.0, // sanity check that points enclose non-zere volume
|
zSum = 0.0, // sanity check that points enclose non-zere volume
|
||||||
buckets = [], // banded/grouped faces to speed up slice/search
|
buckets = [], // banded/grouped faces to speed up slice/search
|
||||||
overlapMax = options.overlap || 0.75,
|
overlapMax = options.overlap || 0.75,
|
||||||
|
bucketMin = options.bucketMin ?? 1,
|
||||||
bucketMax = options.bucketMax || 100,
|
bucketMax = options.bucketMax || 100,
|
||||||
onupdate = options.onupdate || function() {},
|
onupdate = options.onupdate || function() {},
|
||||||
sliceFn = dval(options.slicer, sliceZ),
|
sliceFn = dval(options.slicer, sliceZ),
|
||||||
|
|
@ -138,7 +139,7 @@ export async function slice(points, options = {}) {
|
||||||
let zSpan = zMax - zMin;
|
let zSpan = zMax - zMin;
|
||||||
let zSpanAvg = zSum / points.length;
|
let zSpanAvg = zSum / points.length;
|
||||||
let bucketCount = options.bucket !== false ?
|
let bucketCount = options.bucket !== false ?
|
||||||
Math.min(bucketMax, Math.max(1, Math.floor(zSpan / zSpanAvg))) : 1;
|
Math.min(bucketMax, Math.max(bucketMin, Math.floor(zSpan / zSpanAvg))) : 1;
|
||||||
|
|
||||||
zScale = 1 / (zMax / bucketCount);
|
zScale = 1 / (zMax / bucketCount);
|
||||||
|
|
||||||
|
|
@ -211,7 +212,6 @@ export async function slice(points, options = {}) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
let opt = { ...options, zMin, zMax, zIndexes };
|
let opt = { ...options, zMin, zMax, zIndexes };
|
||||||
let ps = [];
|
let ps = [];
|
||||||
|
|
||||||
for (let i = 0, l = buckets.length; i < l; i++) {
|
for (let i = 0, l = buckets.length; i < l; i++) {
|
||||||
let bucket = buckets[i];
|
let bucket = buckets[i];
|
||||||
let { points, slices } = bucket;
|
let { points, slices } = bucket;
|
||||||
|
|
|
||||||
|
|
@ -40,12 +40,14 @@ import { visuals } from './visuals.js';
|
||||||
import { widgets } from './widgets.js';
|
import { widgets } from './widgets.js';
|
||||||
import { workspace } from './workspace.js';
|
import { workspace } from './workspace.js';
|
||||||
|
|
||||||
|
import { OPFS } from '../../moto/opfs.js';
|
||||||
|
|
||||||
// environment setup
|
// environment setup
|
||||||
let LOC = self.location,
|
let LOC = self.location,
|
||||||
EVENT = broker,
|
EVENT = broker,
|
||||||
SETUP = utils.parseOpt(LOC.search.substring(1)),
|
SETUP = utils.parseOpt(LOC.search.substring(1)),
|
||||||
FILES = openFiles(new Index(SETUP.d ? SETUP.d[0] : 'kiri')),
|
FILES = openFiles(new Index(SETUP.d ? SETUP.d[0] : 'kiri')),
|
||||||
LOCAL = self.debug && !SETUP.remote,
|
LOCAL = (LOC.host.startsWith('localhost') || self.debug) && !SETUP.remote,
|
||||||
SECURE = isSecure(LOC.protocol);
|
SECURE = isSecure(LOC.protocol);
|
||||||
|
|
||||||
// todo: fix in widget.js b/c front-end and back-end do not share api
|
// todo: fix in widget.js b/c front-end and back-end do not share api
|
||||||
|
|
@ -163,6 +165,7 @@ export const api = {
|
||||||
alerts(clr) { alerts.update(clr) },
|
alerts(clr) { alerts.update(clr) },
|
||||||
bind(t,m,o) { return EVENT.bind(t,m,o) },
|
bind(t,m,o) { return EVENT.bind(t,m,o) },
|
||||||
emit(t,m,o) { return EVENT.publish(t,m,o) },
|
emit(t,m,o) { return EVENT.publish(t,m,o) },
|
||||||
|
emitDefer(t,m,d) { setTimeout(() => EVENT.publish(t,m), d ?? 100) },
|
||||||
import() { api.ui.load.click() },
|
import() { api.ui.load.click() },
|
||||||
listeners(topic) { return EVENT.targets(topic) },
|
listeners(topic) { return EVENT.targets(topic) },
|
||||||
on(t,l) { EVENT.on(t,l); return api.event },
|
on(t,l) { EVENT.on(t,l); return api.event },
|
||||||
|
|
@ -227,6 +230,7 @@ export const api = {
|
||||||
onkey(fn) {
|
onkey(fn) {
|
||||||
api.feature.on_key2.push(fn);
|
api.feature.on_key2.push(fn);
|
||||||
},
|
},
|
||||||
|
opfs: OPFS,
|
||||||
platform,
|
platform,
|
||||||
process: processModule,
|
process: processModule,
|
||||||
sdb: dataLocal,
|
sdb: dataLocal,
|
||||||
|
|
@ -234,6 +238,12 @@ export const api = {
|
||||||
settings,
|
settings,
|
||||||
show: {
|
show: {
|
||||||
alert() { return alerts.show(...arguments) },
|
alert() { return alerts.show(...arguments) },
|
||||||
|
busy(msg) {
|
||||||
|
if (msg === false || msg === null || msg === 0 || msg === '') {
|
||||||
|
return visuals.set_progress(0);
|
||||||
|
}
|
||||||
|
return visuals.set_progress(-1, typeof msg === 'string' ? msg : undefined);
|
||||||
|
},
|
||||||
controls() { console.trace('deprecated') },
|
controls() { console.trace('deprecated') },
|
||||||
devices: showDevices,
|
devices: showDevices,
|
||||||
import() { api.ui.import.style.display = '' },
|
import() { api.ui.import.style.display = '' },
|
||||||
|
|
|
||||||
|
|
@ -341,11 +341,11 @@ export const conf = {
|
||||||
sliceSupportAngle: 50,
|
sliceSupportAngle: 50,
|
||||||
sliceSupportDensity: 0.1,
|
sliceSupportDensity: 0.1,
|
||||||
sliceSupportExtra: 0,
|
sliceSupportExtra: 0,
|
||||||
sliceSupportGap: 1,
|
sliceSupportGap: true,
|
||||||
sliceSupportNozzle: 0,
|
sliceSupportNozzle: 0,
|
||||||
sliceSupportOffset: 1.0,
|
sliceSupportOffset: 1.0,
|
||||||
sliceSupportOutline: true,
|
sliceSupportOutline: true,
|
||||||
sliceSupportSpan: 5,
|
// sliceSupportSpan: 5,
|
||||||
sliceSupportType: "disabled",
|
sliceSupportType: "disabled",
|
||||||
sliceSupportTree: false,
|
sliceSupportTree: false,
|
||||||
sliceTopLayers: 3,
|
sliceTopLayers: 3,
|
||||||
|
|
@ -604,9 +604,11 @@ export const conf = {
|
||||||
camToolInit: true,
|
camToolInit: true,
|
||||||
camTraceDogbone: false,
|
camTraceDogbone: false,
|
||||||
camTraceDown: 0,
|
camTraceDown: 0,
|
||||||
|
camTraceIgnore: false,
|
||||||
camTraceLines: false,
|
camTraceLines: false,
|
||||||
camTraceMerge: false,
|
camTraceMerge: false,
|
||||||
camTraceOffOver: 0,
|
camTraceOffOver: 0,
|
||||||
|
camTraceOffZ: 0,
|
||||||
camTraceOffset: "none",
|
camTraceOffset: "none",
|
||||||
camTraceOver: 0.5,
|
camTraceOver: 0.5,
|
||||||
camTracePlunge: 200,
|
camTracePlunge: 200,
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,8 @@ function updateProcessList() {
|
||||||
load.onclick = (ev) => {
|
load.onclick = (ev) => {
|
||||||
api.conf.load(undefined, sk);
|
api.conf.load(undefined, sk);
|
||||||
updateProcessList();
|
updateProcessList();
|
||||||
|
// update pulldowns
|
||||||
|
api.devices.refresh();
|
||||||
modal.hide();
|
modal.hide();
|
||||||
}
|
}
|
||||||
load.appendChild(DOC.createTextNode(sk));
|
load.appendChild(DOC.createTextNode(sk));
|
||||||
|
|
|
||||||
|
|
@ -172,9 +172,6 @@ function updateSettings(opt = {}) {
|
||||||
same = false;
|
same = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$('mode-device').innerText = device.deviceName;
|
|
||||||
$('mode-profile').innerText = `${cproc[mode]}${same ? '' : ' *'}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSettingsFromFields(setrec, uirec = api.ui, changes) {
|
function updateSettingsFromFields(setrec, uirec = api.ui, changes) {
|
||||||
|
|
@ -279,6 +276,7 @@ function updateFieldsFromSettings(setrec, uirec = api.ui, opt = {}) {
|
||||||
let opt = document.createElement('option');
|
let opt = document.createElement('option');
|
||||||
opt.appendChild(document.createTextNode(el.name));
|
opt.appendChild(document.createTextNode(el.name));
|
||||||
opt.setAttribute('value', ev);
|
opt.setAttribute('value', ev);
|
||||||
|
if (id === '#') opt.setAttribute('disabled', true);
|
||||||
uie.appendChild(opt);
|
uie.appendChild(opt);
|
||||||
});
|
});
|
||||||
if (chosen) {
|
if (chosen) {
|
||||||
|
|
@ -526,7 +524,7 @@ function settingsExport(opts = {}) {
|
||||||
const shot = opts.work || opts.screen ? space.screenshot() : undefined;
|
const shot = opts.work || opts.screen ? space.screenshot() : undefined;
|
||||||
const work = opts.work ? codec.encode(widgets,{_json_:true}) : undefined;
|
const work = opts.work ? codec.encode(widgets,{_json_:true}) : undefined;
|
||||||
const view = opts.work ? space.view.save() : undefined;
|
const view = opts.work ? space.view.save() : undefined;
|
||||||
const setn = Object.clone(settings);
|
const setn = Object.clone(opts.engine ?? settings);
|
||||||
// stuff in legacy annotations for re-import
|
// stuff in legacy annotations for re-import
|
||||||
for (let w of widgets) {
|
for (let w of widgets) {
|
||||||
setn.widget[w.id] = w.anno;
|
setn.widget[w.id] = w.anno;
|
||||||
|
|
@ -539,7 +537,7 @@ function settingsExport(opts = {}) {
|
||||||
note: note,
|
note: note,
|
||||||
work: work,
|
work: work,
|
||||||
view: view,
|
view: view,
|
||||||
moto: moto.id,
|
moto: self.moto?.id,
|
||||||
init: local.getItem('kiri-init'),
|
init: local.getItem('kiri-init'),
|
||||||
time: Date.now()
|
time: Date.now()
|
||||||
};
|
};
|
||||||
|
|
@ -559,6 +557,7 @@ function settingsImport(data, ask) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (api.const.LOCAL) console.log('import', data);
|
if (api.const.LOCAL) console.log('import', data);
|
||||||
|
|
||||||
let isSettings = (data.settings && data.time);
|
let isSettings = (data.settings && data.time);
|
||||||
let isProcess = (data.process && data.time && data.mode && data.name);
|
let isProcess = (data.process && data.time && data.mode && data.name);
|
||||||
let isDevice = (data.device && data.time);
|
let isDevice = (data.device && data.time);
|
||||||
|
|
|
||||||
|
|
@ -301,9 +301,7 @@ function setDeviceCode(code, devicename) {
|
||||||
*/
|
*/
|
||||||
function renderDevices(devices) {
|
function renderDevices(devices) {
|
||||||
let selected = api.device.get() || devices[0],
|
let selected = api.device.get() || devices[0],
|
||||||
features = api.feature,
|
devs = setconf.get().devices;
|
||||||
devs = setconf.get().devices,
|
|
||||||
dfilter = typeof(features.device_filter) === 'function' ? features.device_filter : undefined;
|
|
||||||
|
|
||||||
for (let local in devs) {
|
for (let local in devs) {
|
||||||
if (!(devs.hasOwnProperty(local) && devs[local])) {
|
if (!(devs.hasOwnProperty(local) && devs[local])) {
|
||||||
|
|
@ -367,6 +365,31 @@ function renderDevices(devices) {
|
||||||
api.device.export(exp, selected, { event, record });
|
api.device.export(exp, selected, { event, record });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
updateDeviceSelector(devices, api.ui.modeDevice, selected);
|
||||||
|
updateDeviceSelector(devices, $('dev-list'), selected);
|
||||||
|
selectDevice(selected);
|
||||||
|
|
||||||
|
// update related settings list
|
||||||
|
let curr = settings.get();
|
||||||
|
let slist = Object.keys(curr.sproc[settings.mode()]);
|
||||||
|
let cproc = settings.proc().processName;
|
||||||
|
|
||||||
|
h.bind(api.ui.modeProfile, slist.map(profile => {
|
||||||
|
return h.option({
|
||||||
|
_: profile,
|
||||||
|
selected: profile === cproc ? 1 : undefined,
|
||||||
|
onclick() { console.log({ select: profile })}
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
api.ui.modeProfile.onchange = () => {
|
||||||
|
api.conf.load(undefined, api.ui.modeProfile.value);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDeviceSelector(devices, selector, selected) {
|
||||||
|
let features = api.feature;
|
||||||
|
let dfilter = typeof(features.device_filter) === 'function' ? features.device_filter : undefined;
|
||||||
let dedup = {};
|
let dedup = {};
|
||||||
let list_cdev = [];
|
let list_cdev = [];
|
||||||
let list_mdev = [];
|
let list_mdev = [];
|
||||||
|
|
@ -388,7 +411,7 @@ function renderDevices(devices) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let dev_list = $('dev-list');
|
let dev_list = selector;
|
||||||
h.bind(dev_list, [
|
h.bind(dev_list, [
|
||||||
h.option({ _: '-- My Devices --', disabled: true }),
|
h.option({ _: '-- My Devices --', disabled: true }),
|
||||||
...list_mdev,
|
...list_mdev,
|
||||||
|
|
@ -401,7 +424,6 @@ function renderDevices(devices) {
|
||||||
const seldev = dev_list.options[dev_list.selectedIndex];
|
const seldev = dev_list.options[dev_list.selectedIndex];
|
||||||
selectDevice(seldev.innerText);
|
selectDevice(seldev.innerText);
|
||||||
api.platform.layout();
|
api.platform.layout();
|
||||||
|
updateDeviceList();
|
||||||
}
|
}
|
||||||
selectDevice(selected);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -247,13 +247,16 @@ function exportGCodeDialog(gcode, sections, info, names) {
|
||||||
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;
|
type = octo_type.value,
|
||||||
|
targetLocal = !api.util.isSecure(host),
|
||||||
|
siteSecure = api.const.SECURE,
|
||||||
|
proxy = targetLocal && api.feature.proxy ? host : undefined;
|
||||||
|
|
||||||
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://)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (api.const.SECURE && !api.util.isSecure(host)) {
|
if (siteSecure && targetLocal && !proxy) {
|
||||||
api.show.alert("host must begin with 'https' on a secure site");
|
api.show.alert("host must begin with 'https' on a secure site");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -262,6 +265,11 @@ function exportGCodeDialog(gcode, sections, info, names) {
|
||||||
localSet('octo-apik', apik.trim());
|
localSet('octo-apik', apik.trim());
|
||||||
localSet('octo-type', type.trim());
|
localSet('octo-type', type.trim());
|
||||||
|
|
||||||
|
if (proxy) {
|
||||||
|
console.log('proxying request to', host);
|
||||||
|
host = 'http://127.0.0.1:5309';
|
||||||
|
}
|
||||||
|
|
||||||
filename = $('print-filename').value + "." + fileext;
|
filename = $('print-filename').value + "." + fileext;
|
||||||
form.append("file", getBlob(), filename);
|
form.append("file", getBlob(), filename);
|
||||||
ajax.onreadystatechange = function() {
|
ajax.onreadystatechange = function() {
|
||||||
|
|
@ -297,6 +305,9 @@ function exportGCodeDialog(gcode, sections, info, names) {
|
||||||
} else {
|
} else {
|
||||||
ajax.open("POST", host + "/api/files/local");
|
ajax.open("POST", host + "/api/files/local");
|
||||||
}
|
}
|
||||||
|
if (proxy) {
|
||||||
|
ajax.setRequestHeader("X-Host", proxy);
|
||||||
|
}
|
||||||
if (apik) {
|
if (apik) {
|
||||||
ajax.setRequestHeader("X-Api-Key", apik);
|
ajax.setRequestHeader("X-Api-Key", apik);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,36 +22,58 @@ function loadImageDialog(image, name, force) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const opt = {pre: [
|
const rnd = Date.now().toString(36);
|
||||||
"<div class='f-col a-center'>",
|
const host = $('mod-any');
|
||||||
" <h3>Image Conversion</h3>",
|
host.innerHTML = [
|
||||||
" <p class='t-just' style='width:300px;line-height:1.5em'>",
|
`<div class="image-convert-dialog f-col a-center">`,
|
||||||
" This will create a 3D model from a 2D PNG image. Photos must",
|
` <h3 class="image-convert-title">Image Conversion</h3>`,
|
||||||
" be blurred to be usable. Values from 0=off to 50=high are suggested.",
|
` <p class="image-convert-copy t-just">`,
|
||||||
" Higher values incur more processing time.",
|
` This will create a 3D model from a 2D PNG image. Photos must`,
|
||||||
" </p>",
|
` be blurred to be usable. Values from 0=off to 50=high are suggested.`,
|
||||||
" <div class='f-row t-right'><table>",
|
` Higher values incur more processing time.`,
|
||||||
" <tr><th>blur value</th><td><input id='png-blur' value='0' size='3'></td>",
|
` </p>`,
|
||||||
" <th> invert image</th><td><input id='png-inv' type='checkbox'></td></tr>",
|
` <div class="f-row t-right image-convert-fields"><table>`,
|
||||||
" <tr><th>base size</th><td><input id='png-base' value='0' size='3'></td>",
|
` <tr><th>blur value</th><td><input id="png-blur-${rnd}" value="0" size="3"></td>`,
|
||||||
" <th> invert alpha</th><td><input id='alpha-inv' type='checkbox'></td></tr>",
|
` <th>invert image</th><td><input id="png-inv-${rnd}" type="checkbox"></td></tr>`,
|
||||||
" <tr><th>border size</th><td><input id='png-border' value='0' size='3'></td>",
|
` <tr><th>base size</th><td><input id="png-base-${rnd}" value="0" size="3"></td>`,
|
||||||
" <th></th><td></td></tr>",
|
` <th>invert alpha</th><td><input id="alpha-inv-${rnd}" type="checkbox"></td></tr>`,
|
||||||
" </table></div>",
|
` <tr><th>border size</th><td><input id="png-border-${rnd}" value="0" size="3"></td>`,
|
||||||
"</div>"
|
` <th></th><td></td></tr>`,
|
||||||
]};
|
` </table></div>`,
|
||||||
api.uc.confirm(undefined, {convert:true, cancel:false}, undefined, opt).then((ok) => {
|
` <div class="f-row j-end image-convert-actions">`,
|
||||||
if (ok) {
|
` <button id="img-convert-ok-${rnd}">convert</button>`,
|
||||||
|
` <button id="img-convert-cancel-${rnd}">cancel</button>`,
|
||||||
|
` </div>`,
|
||||||
|
`</div>`
|
||||||
|
].join('');
|
||||||
|
|
||||||
|
const blur = $(`png-blur-${rnd}`);
|
||||||
|
const base = $(`png-base-${rnd}`);
|
||||||
|
const border = $(`png-border-${rnd}`);
|
||||||
|
const invImage = $(`png-inv-${rnd}`);
|
||||||
|
const invAlpha = $(`alpha-inv-${rnd}`);
|
||||||
|
const okBtn = $(`img-convert-ok-${rnd}`);
|
||||||
|
const cancelBtn = $(`img-convert-cancel-${rnd}`);
|
||||||
|
|
||||||
|
okBtn.onclick = () => {
|
||||||
|
api.modal.hide();
|
||||||
|
setTimeout(() => {
|
||||||
loadImage(image, {
|
loadImage(image, {
|
||||||
file: name,
|
file: name,
|
||||||
blur: parseInt($('png-blur').value) || 0,
|
blur: parseInt(blur.value) || 0,
|
||||||
base: parseInt($('png-base').value) || 0,
|
base: parseInt(base.value) || 0,
|
||||||
border: parseInt($('png-border').value) || 0,
|
border: parseInt(border.value) || 0,
|
||||||
inv_image: $('png-inv').checked,
|
inv_image: invImage.checked,
|
||||||
inv_alpha: $('alpha-inv').checked
|
inv_alpha: invAlpha.checked
|
||||||
});
|
});
|
||||||
}
|
},50);
|
||||||
});
|
};
|
||||||
|
cancelBtn.onclick = () => api.modal.hide();
|
||||||
|
blur.onkeypress = (ev) => {
|
||||||
|
if (ev.key === 'Enter' || ev.charCode === 13) okBtn.click();
|
||||||
|
};
|
||||||
|
api.modal.show('any');
|
||||||
|
setTimeout(() => blur.focus(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -62,10 +84,10 @@ function loadImageDialog(image, name, force) {
|
||||||
*/
|
*/
|
||||||
function loadImage(image, opt = {}) {
|
function loadImage(image, opt = {}) {
|
||||||
const info = Object.assign({settings: settings.get(), png:image}, opt);
|
const info = Object.assign({settings: settings.get(), png:image}, opt);
|
||||||
api.client.image2mesh(info, progress => {
|
api.client.image2mesh(info, () => {
|
||||||
api.show.progress(progress, "converting");
|
api.show.busy('converting');
|
||||||
}, vertices => {
|
}, vertices => {
|
||||||
api.show.progress(0);
|
api.show.busy(false);
|
||||||
const widget = newWidget().loadVertices(vertices);
|
const widget = newWidget().loadVertices(vertices);
|
||||||
widget.meta.file = opt.file;
|
widget.meta.file = opt.file;
|
||||||
platform.add(widget);
|
platform.add(widget);
|
||||||
|
|
|
||||||
|
|
@ -105,8 +105,10 @@ export function bind() {
|
||||||
range: $('slider-center'),
|
range: $('slider-center'),
|
||||||
},
|
},
|
||||||
|
|
||||||
loading: $('progress').style,
|
loading: $('progress-overlay')?.style,
|
||||||
progress: $('progbar').style,
|
progressOverlay: $('progress-overlay'),
|
||||||
|
progress: $('progress-ring'),
|
||||||
|
progressPct: $('progress-pct'),
|
||||||
prostatus: $('progtxt'),
|
prostatus: $('progtxt'),
|
||||||
selection: $('selection'),
|
selection: $('selection'),
|
||||||
|
|
||||||
|
|
|
||||||
23
src/kiri/app/init/build.js
Normal file
23
src/kiri/app/init/build.js
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
import { menubar } from './menu.js';
|
||||||
|
import { api } from '../api.js';
|
||||||
|
|
||||||
|
const surfaces = {
|
||||||
|
build(actions = {}) {
|
||||||
|
const stop = (fn) => (ev) => {
|
||||||
|
ev?.stopPropagation?.();
|
||||||
|
return fn?.(ev);
|
||||||
|
};
|
||||||
|
menubar.build({
|
||||||
|
...actions,
|
||||||
|
'view-arrange': stop(() => api.platform.layout()),
|
||||||
|
'act-slice': stop(() => api.function.slice()),
|
||||||
|
'act-preview': stop(() => api.function.print()),
|
||||||
|
'act-animate': stop(() => api.function.animate()),
|
||||||
|
'act-export': stop(() => api.function.export())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { surfaces };
|
||||||
|
|
@ -87,9 +87,6 @@ function checkSeed(then) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// upon restore, seed presets
|
|
||||||
api.event.emit('preset', api.conf.dbo());
|
|
||||||
|
|
||||||
// api.event.on("set.threaded", bool => setThreaded(bool));
|
// api.event.on("set.threaded", bool => setThreaded(bool));
|
||||||
|
|
||||||
export function onBooleanClick(el) {
|
export function onBooleanClick(el) {
|
||||||
|
|
@ -142,6 +139,11 @@ function onResize() {
|
||||||
} else {
|
} else {
|
||||||
ui.modalBox.classList.remove('mh85');
|
ui.modalBox.classList.remove('mh85');
|
||||||
}
|
}
|
||||||
|
if (WIN.innerWidth < 800) {
|
||||||
|
$('app').classList.add('slideshow');
|
||||||
|
} else {
|
||||||
|
api.prefs.updateDrawer();
|
||||||
|
}
|
||||||
api.view.update_slider();
|
api.view.update_slider();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,6 +176,7 @@ export function init_input() {
|
||||||
event.on('resize', onResize);
|
event.on('resize', onResize);
|
||||||
|
|
||||||
// configure moto.space
|
// configure moto.space
|
||||||
|
space.view.setFitPadding({ perspective: 0.8 });
|
||||||
space.sky.showGrid(false);
|
space.sky.showGrid(false);
|
||||||
space.sky.setColor(controller.dark ? 0 : 0xffffff);
|
space.sky.setColor(controller.dark ? 0 : 0xffffff);
|
||||||
space.setAntiAlias(controller.antiAlias);
|
space.setAntiAlias(controller.antiAlias);
|
||||||
|
|
@ -211,6 +214,10 @@ export function init_input() {
|
||||||
// api augmentation with local functions
|
// api augmentation with local functions
|
||||||
api.device.export = settingsOps.export_device;
|
api.device.export = settingsOps.export_device;
|
||||||
|
|
||||||
|
let driven = true,
|
||||||
|
hideable = true,
|
||||||
|
separator = true;
|
||||||
|
|
||||||
Object.assign(ui, {
|
Object.assign(ui, {
|
||||||
tracker: tracker,
|
tracker: tracker,
|
||||||
container: container,
|
container: container,
|
||||||
|
|
@ -320,6 +327,10 @@ export function init_input() {
|
||||||
|
|
||||||
prefadd: uc.checkpoint($('prefs-add')),
|
prefadd: uc.checkpoint($('prefs-add')),
|
||||||
|
|
||||||
|
_____: newGroup('Machine Profile', $('all-devpro'), { driven, hideable, separator, group: "devpro" }),
|
||||||
|
modeDevice: newSelect('machine', {title: 'device', class: "tiny"}, "_"),
|
||||||
|
modeProfile: newSelect('profile', {title: 'profile', class: "tiny"}, "_"),
|
||||||
|
|
||||||
/** FDM Settings */
|
/** FDM Settings */
|
||||||
...menuFDM(),
|
...menuFDM(),
|
||||||
|
|
||||||
|
|
@ -349,6 +360,8 @@ export function init_input() {
|
||||||
// override old style settings two-button menu
|
// override old style settings two-button menu
|
||||||
ui.settingsSave.onclick = () => {
|
ui.settingsSave.onclick = () => {
|
||||||
settingsOps.settings_save(undefined, ui.settingsName.value);
|
settingsOps.settings_save(undefined, ui.settingsName.value);
|
||||||
|
// update pulldowns
|
||||||
|
api.devices.refresh();
|
||||||
};
|
};
|
||||||
|
|
||||||
// initialize and expose modal to API
|
// initialize and expose modal to API
|
||||||
|
|
|
||||||
267
src/kiri/app/init/menu.js
Normal file
267
src/kiri/app/init/menu.js
Normal file
|
|
@ -0,0 +1,267 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
import { $, h } from '../../../moto/webui.js';
|
||||||
|
import { api } from '../api.js';
|
||||||
|
|
||||||
|
const { div, span, label, input, button, i, hr } = h;
|
||||||
|
|
||||||
|
function icon(cls) {
|
||||||
|
return i({ class: cls });
|
||||||
|
}
|
||||||
|
|
||||||
|
function on(actions, id) {
|
||||||
|
return actions && typeof actions[id] === 'function' ? { onclick: actions[id] } : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function tr(key, fallback) {
|
||||||
|
return api.language?.current?.[key] || fallback || key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function menuItem(actions, { id, lk, xlk, text, title, iconClass, className, children, onclick }) {
|
||||||
|
const attr = {
|
||||||
|
...(id ? { id } : {}),
|
||||||
|
...(title ? { title } : {}),
|
||||||
|
...(className ? { class: className } : {}),
|
||||||
|
...(onclick ? { onclick } : {}),
|
||||||
|
...on(actions, id)
|
||||||
|
};
|
||||||
|
const lblAttr = {
|
||||||
|
...(lk ? { lk } : {}),
|
||||||
|
...(xlk ? { xlk } : {})
|
||||||
|
};
|
||||||
|
const resolved = lk ? tr(lk, text) : xlk ? tr(xlk, text) : text;
|
||||||
|
return div(attr, [
|
||||||
|
resolved !== undefined ? label({ ...lblAttr, _: resolved }) : undefined,
|
||||||
|
iconClass ? span([icon(iconClass)]) : undefined,
|
||||||
|
children
|
||||||
|
].filter(v => v !== undefined));
|
||||||
|
}
|
||||||
|
|
||||||
|
function dropMenu(actions, side, items) {
|
||||||
|
return div({ class: `top-menu-drop top-menu-${side}` }, [
|
||||||
|
div({ class: 'content' }, items)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function topMenu(actions, { text, lk, iconClass, side = 'left', right = false, items }) {
|
||||||
|
const resolved = lk ? tr(lk, text) : text;
|
||||||
|
return span({ class: right ? 'menu-right' : undefined }, [
|
||||||
|
resolved !== undefined ? label({ ...(lk ? { lk } : {}), _: resolved }) : undefined,
|
||||||
|
iconClass ? icon(iconClass) : undefined,
|
||||||
|
dropMenu(actions, side, items)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotatePanel(actions) {
|
||||||
|
return div({ id: 'panel-rotate', class: 'selection-panel hide' }, [
|
||||||
|
div({ id: 'panel-rotate-head', class: 'selection-panel-head' }, [
|
||||||
|
div({ class: 'selection-panel-head-title' }, [
|
||||||
|
i({ class: 'fas fa-rotate-right' }),
|
||||||
|
label({ _: 'Rotate' })
|
||||||
|
]),
|
||||||
|
button({ id: 'panel-rotate-close', class: 'selection-panel-close', title: 'close' }, [
|
||||||
|
i({ class: 'fas fa-times' })
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
div({ id: 'ft-rotate', class: 'grid selection-panel-body' }, [
|
||||||
|
div({ id: 'rot_x_lt', ...on(actions, 'rot_x_lt') }, icon('fas fa-chevron-left')),
|
||||||
|
label({ _: 'X' }),
|
||||||
|
div({ id: 'rot_x_gt', ...on(actions, 'rot_x_gt') }, icon('fas fa-chevron-right')),
|
||||||
|
input({ id: 'rot_x', class: 'value center', size: '6', value: '90' }),
|
||||||
|
div({ id: 'rot_y_lt', ...on(actions, 'rot_y_lt') }, icon('fas fa-chevron-left')),
|
||||||
|
label({ _: 'Y' }),
|
||||||
|
div({ id: 'rot_y_gt', ...on(actions, 'rot_y_gt') }, icon('fas fa-chevron-right')),
|
||||||
|
input({ id: 'rot_y', class: 'value center', size: '6', value: '90' }),
|
||||||
|
div({ id: 'rot_z_lt', ...on(actions, 'rot_z_lt') }, icon('fas fa-chevron-left')),
|
||||||
|
label({ _: 'Z' }),
|
||||||
|
div({ id: 'rot_z_gt', ...on(actions, 'rot_z_gt') }, icon('fas fa-chevron-right')),
|
||||||
|
input({ id: 'rot_z', class: 'value center', size: '6', value: '90' }),
|
||||||
|
div({ class: 'buttons f-row' }, [
|
||||||
|
button({ id: 'unrotate', class: 'grow', lk: 'reset', _: 'reset', ...on(actions, 'unrotate') })
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scalePanel(actions) {
|
||||||
|
return div({ id: 'panel-scale', class: 'selection-panel hide' }, [
|
||||||
|
div({ id: 'panel-scale-head', class: 'selection-panel-head' }, [
|
||||||
|
div({ class: 'selection-panel-head-title' }, [
|
||||||
|
i({ class: 'fas fa-expand' }),
|
||||||
|
label({ _: 'Scale / Size' })
|
||||||
|
]),
|
||||||
|
button({ id: 'panel-scale-close', class: 'selection-panel-close', title: 'close' }, [
|
||||||
|
i({ class: 'fas fa-times' })
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
div({ id: 'ft-scale', class: 'grid selection-panel-body' }, [
|
||||||
|
div([label({ _: 'X' }), input({ id: 'lock_x', type: 'checkbox', _checked: true })]),
|
||||||
|
div([label({ _: 'Y' }), input({ id: 'lock_y', type: 'checkbox', _checked: true })]),
|
||||||
|
div([label({ _: 'Z' }), input({ id: 'lock_z', type: 'checkbox', _checked: true })]),
|
||||||
|
label({ id: 'lab-axis', lk: 'axis', _: 'axis', ...on(actions, 'lab-axis') }),
|
||||||
|
input({ id: 'size_x', size: '8', class: 'value' }),
|
||||||
|
input({ id: 'size_y', size: '8', class: 'value' }),
|
||||||
|
input({ id: 'size_z', size: '8', class: 'value' }),
|
||||||
|
label({ id: 'lab-size', lk: 'size', _: 'size' }),
|
||||||
|
input({ id: 'scale_x', size: '8', class: 'value', value: '1' }),
|
||||||
|
input({ id: 'scale_y', size: '8', class: 'value', value: '1' }),
|
||||||
|
input({ id: 'scale_z', size: '8', class: 'value', value: '1' }),
|
||||||
|
label({ id: 'lab-scale', lk: 'scale', _: 'scale', ...on(actions, 'lab-scale') }),
|
||||||
|
div({ class: 'buttons f-row' }, [
|
||||||
|
button({ id: 'scale-reset', class: 'grow j-center', _: 'reset', ...on(actions, 'scale-reset') })
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function content(actions) {
|
||||||
|
return [
|
||||||
|
div({ class: 'menubar-appname el-app-hide', _: 'Kiri:Moto' }),
|
||||||
|
div({ class: 'menubar-separator el-app-hide' }),
|
||||||
|
div({ class: 'f-row top-menu grow' }, [
|
||||||
|
topMenu(actions, {
|
||||||
|
text: 'files', lk: 'fe_menu', items: [
|
||||||
|
menuItem(actions, { id: 'file-new', lk: 'new', text: 'new', iconClass: 'fas fa-file' }),
|
||||||
|
hr(),
|
||||||
|
menuItem(actions, { id: 'file-recent', lk: 'recent', text: 'recent', iconClass: 'fas fa-list' }),
|
||||||
|
menuItem(actions, {
|
||||||
|
id: 'file-import', lk: 'import', text: 'import', iconClass: 'fas fa-file-upload', children:
|
||||||
|
input({ id: 'load-file', type: 'file', name: 'loadme', style: 'display:none', accept: '.km,.kmz,.stl,.obj,.svg,.dxf,.png,.jpg,.jpeg,.gcode,.nc' })
|
||||||
|
}),
|
||||||
|
hr(),
|
||||||
|
menuItem(actions, { id: 'mesh-export-obj', lk: 'export-obj', text: 'save as OBJ', iconClass: 'fas fa-dice-d20' }),
|
||||||
|
menuItem(actions, { id: 'mesh-export-stl', lk: 'export-stl', text: 'save as STL', iconClass: 'fas fa-dice-d20' }),
|
||||||
|
hr(),
|
||||||
|
menuItem(actions, { id: 'app-export', lk: 'rc_xpws', text: 'export work', iconClass: 'fas fa-download' }),
|
||||||
|
hr({ class: "app-hide" }),
|
||||||
|
menuItem(actions, { id: 'app-quit', lk: 'quit', text: 'quit', className: 'hide', onclick() { window.close() } })
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
topMenu(actions, {
|
||||||
|
text: 'edit', lk: 'ed_menu', items: [
|
||||||
|
menuItem(actions, { id: 'context-layflat', lk: 'rc_lafl', text: 'face down', iconClass: 'fas fa-angle-double-down' }),
|
||||||
|
menuItem(actions, { id: 'context-lefty', lk: 'face_left', text: 'face left', iconClass: 'fas fa-angle-double-left' }),
|
||||||
|
hr(),
|
||||||
|
menuItem(actions, { id: 'context-mirror', lk: 'rc_mirr', text: 'mirror', iconClass: 'fas fa-arrows-left-right-to-line' }),
|
||||||
|
menuItem(actions, { id: 'context-duplicate', lk: 'rc_dupl', text: 'duplicate', iconClass: 'fas fa-copy' }),
|
||||||
|
hr(),
|
||||||
|
menuItem(actions, { id: 'context-rotate-panel', text: 'rotate', iconClass: 'fas fa-rotate-right' }),
|
||||||
|
menuItem(actions, { id: 'context-scale-panel', text: 'scale / size', iconClass: 'fas fa-expand' }),
|
||||||
|
hr(),
|
||||||
|
menuItem(actions, { id: 'mesh-merge', lk: 'rc_merg', text: 'merge meshes' }),
|
||||||
|
menuItem(actions, { id: 'mesh-split', lk: 'rc_splt', text: 'isolate meshes' }),
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
topMenu(actions, {
|
||||||
|
text: 'view', lk: 'vu_menu', items: [
|
||||||
|
menuItem(actions, { id: 'context-setfocus', lk: 'rc_focs', text: 'focal point', iconClass: 'fas fa-eye' }),
|
||||||
|
hr(),
|
||||||
|
menuItem(actions, { id: 'view-fit', lk: 'contents', text: 'contents', iconClass: 'fas fa-arrows-to-circle' }),
|
||||||
|
menuItem(actions, { id: 'view-home', lk: 'home', text: 'home', iconClass: 'fas fa-home' }),
|
||||||
|
menuItem(actions, { id: 'view-top', lk: 'top', text: 'top', iconClass: 'fas fa-square' }),
|
||||||
|
hr(),
|
||||||
|
menuItem(actions, { id: 'app-xpnd', lk: 'fullscreen', text: 'fullscreen', iconClass: 'fas fa-maximize' })
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
topMenu(actions, {
|
||||||
|
text: 'render', lk: 're_menu', items: [
|
||||||
|
menuItem(actions, { id: 'render-solid', lk: 'solid', text: 'solid', iconClass: 'fas fa-square' }),
|
||||||
|
menuItem(actions, { id: 'render-wire', lk: 'wire', text: 'wireframe', iconClass: 'fas fa-border-all' }),
|
||||||
|
menuItem(actions, { id: 'render-ghost', lk: 'ghost', text: 'transparent', iconClass: 'fas fa-border-none' }),
|
||||||
|
hr(),
|
||||||
|
menuItem(actions, { id: 'render-edges', lk: 're_edgs', text: 'toggle edges', iconClass: 'fa-regular fa-square' })
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
div({ class: 'f-row top-menu' }, [
|
||||||
|
span({ id: 'tool-nozzle' }, [
|
||||||
|
label({ lk: 'tool', _: tr('tool', 'tool') }),
|
||||||
|
div({ id: 'ft-nozzle', class: 'f-col pop' })
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
div({ class: 'grow' }),
|
||||||
|
topMenu(actions, {
|
||||||
|
text: 'info', lk: 'info', side: 'right', right: true, items: [
|
||||||
|
menuItem(actions, { id: 'app-help', lk: 'help', text: 'help' }),
|
||||||
|
menuItem(actions, { id: 'app-don8', lk: 'donate', text: 'donate' }),
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
topMenu(actions, {
|
||||||
|
text: 'mode', lk: 'mo_menu', side: 'right', right: true, items: [
|
||||||
|
menuItem(actions, { id: 'mode-fdm', text: 'FDM', title: '3D Additive Printing Processes', iconClass: 'fas fa-layer-group' }),
|
||||||
|
menuItem(actions, { id: 'mode-cam', text: 'CNC', title: 'CNC Mills and Subtractive Processes', iconClass: 'fas fa-bore-hole' }),
|
||||||
|
menuItem(actions, { id: 'mode-sla', text: 'SLA', title: 'mSLA Resin Printing', iconClass: 'fas fa-cube' }),
|
||||||
|
hr(),
|
||||||
|
menuItem(actions, { id: 'mode-laser', text: 'Laser', title: 'Laser Cutting and Engraving', iconClass: 'fas fa-bolt' }),
|
||||||
|
menuItem(actions, { id: 'mode-wjet', text: 'Water', title: 'WaterJet Cutting', iconClass: 'fas fa-location-pin' }),
|
||||||
|
menuItem(actions, { id: 'mode-wedm', text: 'Wire', title: 'Wire EDM Cutting', iconClass: 'fas fa-ellipsis-vertical' }),
|
||||||
|
menuItem(actions, { id: 'mode-drag', text: 'Drag', title: 'Drag Knife Cutting', iconClass: 'fas fa-caret-left' })
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
topMenu(actions, {
|
||||||
|
text: 'setup', lk: 'su_menu', side: 'right', right: true, items: [
|
||||||
|
menuItem(actions, { id: 'set-device', lk: 'machines', text: 'machines', iconClass: 'fas fa-cube' }),
|
||||||
|
menuItem(actions, { id: 'set-profs', lk: 'profs', text: 'profiles', iconClass: 'fas fa-sliders-h' }),
|
||||||
|
menuItem(actions, { id: 'set-tools', lk: 'tools', text: 'tools', iconClass: 'fas fa-tools' }),
|
||||||
|
menuItem(actions, { id: 'set-prefs', lk: 'prefs', text: 'prefs', iconClass: 'fa-solid fa-square-check' }),
|
||||||
|
hr({ class: "el-app-hide" }),
|
||||||
|
menuItem(actions, { id: 'install', lk: 'install', text: 'install' }),
|
||||||
|
menuItem(actions, { id: 'uninstall', lk: 'uninstall', text: 'uninstall', className: 'hide' })
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
topMenu(actions, {
|
||||||
|
iconClass: 'fas fa-language', side: 'right', right: true, items: [
|
||||||
|
menuItem(actions, { children: label({ id: 'lset-zh', _: '简体中文' }) }),
|
||||||
|
menuItem(actions, { children: label({ id: 'lset-da', _: 'dansk' }) }),
|
||||||
|
menuItem(actions, { children: label({ id: 'lset-de', _: 'deutsch' }) }),
|
||||||
|
menuItem(actions, { children: label({ id: 'lset-en', _: 'english' }) }),
|
||||||
|
menuItem(actions, { children: label({ id: 'lset-es', _: 'español' }) }),
|
||||||
|
menuItem(actions, { children: label({ id: 'lset-fr', _: 'français' }) }),
|
||||||
|
menuItem(actions, { children: label({ id: 'lset-pl', class: 'nocap', _: 'polski' }) }),
|
||||||
|
menuItem(actions, { children: label({ id: 'lset-pt', _: 'português' }) })
|
||||||
|
]
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
rotatePanel(actions),
|
||||||
|
scalePanel(actions)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function modeTools(actions) {
|
||||||
|
const t = (key, fallback) => tr(key, fallback);
|
||||||
|
return [
|
||||||
|
span({ id: 'view-arrange', ...on(actions, 'view-arrange') }, [
|
||||||
|
span([icon('fas fa-shapes')]),
|
||||||
|
span({ lk: 'arrange', _: t('arrange', 'arrange') })
|
||||||
|
]),
|
||||||
|
span({ id: 'act-slice', ...on(actions, 'act-slice') }, [
|
||||||
|
span([icon('fas fa-bars')]),
|
||||||
|
label({ id: 'label-slice', title: 'generate layer slices', lk: 'slice', _: t('slice', 'slice') })
|
||||||
|
]),
|
||||||
|
span({ id: 'act-preview', ...on(actions, 'act-preview') }, [
|
||||||
|
span([icon('fas fa-layer-group')]),
|
||||||
|
label({ id: 'label-preview', title: 'show routing and paths', lk: 'preview', _: t('preview', 'preview') })
|
||||||
|
]),
|
||||||
|
span({ id: 'act-animate', ...on(actions, 'act-animate') }, [
|
||||||
|
span([icon('fas fa-film')]),
|
||||||
|
label({ id: 'label-animate', title: 'render routing on a mesh', lk: 'animate', _: t('animate', 'animate') })
|
||||||
|
]),
|
||||||
|
span({ id: 'act-export', ...on(actions, 'act-export') }, [
|
||||||
|
span([icon('fas fa-file-download')]),
|
||||||
|
label({ id: 'label-export', title: 'generate gcode', lk: 'export', _: t('export', 'export') })
|
||||||
|
])
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const menubar = {
|
||||||
|
build(actions = {}) {
|
||||||
|
const menubarNode = $('menubar');
|
||||||
|
const modeToolsNode = $('mode-tools');
|
||||||
|
if (!menubarNode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
h.bind(menubarNode, content(actions));
|
||||||
|
if (modeToolsNode) {
|
||||||
|
h.bind(modeToolsNode, modeTools(actions));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -120,7 +120,9 @@ export async function init_sync() {
|
||||||
setup_keybd_nav();
|
setup_keybd_nav();
|
||||||
|
|
||||||
// show topline separator when iframed
|
// show topline separator when iframed
|
||||||
try { if (WIN.self !== WIN.top) $('top-sep').style.display = 'flex' } catch (e) { console.log(e) }
|
if (WIN.self !== WIN.top) {
|
||||||
|
$('menubar').classList.add('top');
|
||||||
|
}
|
||||||
|
|
||||||
// warn users they are running a beta release
|
// warn users they are running a beta release
|
||||||
if (beta && beta > 0 && sdb.kiri_beta != beta) {
|
if (beta && beta > 0 && sdb.kiri_beta != beta) {
|
||||||
|
|
@ -143,6 +145,9 @@ export async function init_sync() {
|
||||||
history.replaceState({}, '', wlp.substring(0,kio + 6));
|
history.replaceState({}, '', wlp.substring(0,kio + 6));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// upon restore, seed presets
|
||||||
|
api.event.emitDefer('preset', api.conf.get());
|
||||||
|
|
||||||
// lift curtain
|
// lift curtain
|
||||||
$('curtain').style.display = 'none';
|
$('curtain').style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
@ -215,22 +220,126 @@ function ui_sync() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setup_keybd_nav() {
|
function setup_keybd_nav() {
|
||||||
|
const panelPosKeys = {
|
||||||
|
'panel-rotate': 'win.roate',
|
||||||
|
'panel-scale': 'win.scale'
|
||||||
|
};
|
||||||
|
|
||||||
|
function parsePanelPos(key) {
|
||||||
|
if (!key) return null;
|
||||||
|
const raw = api.local.get(key);
|
||||||
|
if (!raw) return null;
|
||||||
|
if (typeof raw === 'object' && raw.left !== undefined && raw.top !== undefined) {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
if (typeof raw === 'string') {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (parsed && parsed.left !== undefined && parsed.top !== undefined) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore malformed stored values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function placePanel(panel, pos) {
|
||||||
|
if (!panel || !pos) return;
|
||||||
|
panel.style.left = `${Math.round(pos.left)}px`;
|
||||||
|
panel.style.top = `${Math.round(pos.top)}px`;
|
||||||
|
panel.style.right = 'auto';
|
||||||
|
panel.style.bottom = 'auto';
|
||||||
|
}
|
||||||
|
|
||||||
|
function placePanelDefault(panel) {
|
||||||
|
if (!panel) return;
|
||||||
|
const rect = panel.getBoundingClientRect();
|
||||||
|
const width = rect.width || 260;
|
||||||
|
const modeToolsRect = $('mode-tools')?.getBoundingClientRect();
|
||||||
|
const baseTop = modeToolsRect ? (modeToolsRect.bottom + 10) : 92;
|
||||||
|
const minPad = 8;
|
||||||
|
const maxLeft = Math.max(minPad, window.innerWidth - width - minPad);
|
||||||
|
const left = Math.min(maxLeft, Math.max(minPad, (window.innerWidth - width) / 2));
|
||||||
|
placePanel(panel, { left, top: baseTop });
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSelectionPanel(pid) {
|
||||||
|
const panel = $(pid);
|
||||||
|
if (!panel) return;
|
||||||
|
panel.classList.remove('hide');
|
||||||
|
const key = panelPosKeys[pid];
|
||||||
|
const saved = parsePanelPos(key);
|
||||||
|
if (saved) {
|
||||||
|
placePanel(panel, saved);
|
||||||
|
} else {
|
||||||
|
placePanelDefault(panel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideSelectionPanel(pid) {
|
||||||
|
const panel = $(pid);
|
||||||
|
if (!panel) return;
|
||||||
|
panel.classList.add('hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSelectionPanel(pid) {
|
||||||
|
const el = $(pid);
|
||||||
|
if (!el) return;
|
||||||
|
if (el.classList.contains('hide')) {
|
||||||
|
showSelectionPanel(pid);
|
||||||
|
} else {
|
||||||
|
hideSelectionPanel(pid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makePanelDraggable(panelId, handleId, storageKey) {
|
||||||
|
const panel = $(panelId);
|
||||||
|
const handle = $(handleId);
|
||||||
|
if (!panel || !handle) return;
|
||||||
|
let sx = 0, sy = 0, px = 0, py = 0, dragging = false;
|
||||||
|
handle.onmousedown = (ev) => {
|
||||||
|
if (ev.button !== 0) return;
|
||||||
|
dragging = true;
|
||||||
|
sx = ev.clientX;
|
||||||
|
sy = ev.clientY;
|
||||||
|
const rect = panel.getBoundingClientRect();
|
||||||
|
px = rect.left;
|
||||||
|
py = rect.top;
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
};
|
||||||
|
document.addEventListener('mousemove', (ev) => {
|
||||||
|
if (!dragging) return;
|
||||||
|
const nx = px + (ev.clientX - sx);
|
||||||
|
const ny = py + (ev.clientY - sy);
|
||||||
|
panel.style.left = `${Math.round(nx)}px`;
|
||||||
|
panel.style.top = `${Math.round(ny)}px`;
|
||||||
|
panel.style.right = 'auto';
|
||||||
|
panel.style.bottom = 'auto';
|
||||||
|
});
|
||||||
|
document.addEventListener('mouseup', () => {
|
||||||
|
if (dragging && storageKey) {
|
||||||
|
const rect = panel.getBoundingClientRect();
|
||||||
|
api.local.set(storageKey, JSON.stringify({
|
||||||
|
left: Math.round(rect.left),
|
||||||
|
top: Math.round(rect.top)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
dragging = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// bind interface action elements
|
// bind interface action elements
|
||||||
ui.acct.help.onclick = (ev) => { ev.stopPropagation(); api.help.show() };
|
ui.acct.help.onclick = (ev) => { ev.stopPropagation(); api.help.show() };
|
||||||
ui.acct.don8.onclick = (ev) => { ev.stopPropagation(); api.modal.show('don8') };
|
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.onclick = (ev) => { ev.stopPropagation(); settingsOps.export_profile() };
|
||||||
ui.acct.export.title = LANG.acct_xpo;
|
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
|
// prevent modal input from propagating to parents
|
||||||
ui.modalBox.onclick = (ev) => { ev.stopPropagation() };
|
ui.modalBox.onclick = (ev) => { ev.stopPropagation() };
|
||||||
|
|
||||||
$('export-support-a').onclick = (ev) => { ev.stopPropagation(); api.modal.show('don8') };
|
$('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-fdm').onclick = () => api.mode.set('FDM');
|
||||||
$('mode-cam').onclick = () => api.mode.set('CAM');
|
$('mode-cam').onclick = () => api.mode.set('CAM');
|
||||||
$('mode-sla').onclick = () => api.mode.set('SLA');
|
$('mode-sla').onclick = () => api.mode.set('SLA');
|
||||||
|
|
@ -245,13 +354,9 @@ function setup_keybd_nav() {
|
||||||
$('file-new').onclick = (ev) => { ev.stopPropagation(); settingsOps.new_workspace() };
|
$('file-new').onclick = (ev) => { ev.stopPropagation(); settingsOps.new_workspace() };
|
||||||
$('file-recent').onclick = () => { api.modal.show('files') };
|
$('file-recent').onclick = () => { api.modal.show('files') };
|
||||||
$('file-import').onclick = (ev) => { api.event.import(ev); };
|
$('file-import').onclick = (ev) => { api.event.import(ev); };
|
||||||
$('view-arrange').onclick = api.platform.layout;
|
|
||||||
$('view-top').onclick = space.view.top;
|
$('view-top').onclick = space.view.top;
|
||||||
$('view-home').onclick = space.view.home;
|
$('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 = () => {
|
$('unrotate').onclick = () => {
|
||||||
api.widgets.for(w => w.unrotate());
|
api.widgets.for(w => w.unrotate());
|
||||||
selection.update_info();
|
selection.update_info();
|
||||||
|
|
@ -274,7 +379,10 @@ function setup_keybd_nav() {
|
||||||
$('rot_z_gt').onclick = () => { selection.rotate(0,0,-d * $('rot_z').value) };
|
$('rot_z_gt').onclick = () => { selection.rotate(0,0,-d * $('rot_z').value) };
|
||||||
|
|
||||||
// rendering options
|
// rendering options
|
||||||
$('render-edges').onclick = () => { api.view.set_edges({ toggle: true }); api.conf.save() };
|
$('render-edges').onclick = () => {
|
||||||
|
api.view.set_edges({ toggle: true });
|
||||||
|
api.conf.save()
|
||||||
|
};
|
||||||
$('render-ghost').onclick = () => {
|
$('render-ghost').onclick = () => {
|
||||||
const opacity = api.view.is_arrange() ? 0.4 : 0.25;
|
const opacity = api.view.is_arrange() ? 0.4 : 0.25;
|
||||||
api.view.set_wireframe(false);
|
api.view.set_wireframe(false);
|
||||||
|
|
@ -285,7 +393,7 @@ function setup_keybd_nav() {
|
||||||
};
|
};
|
||||||
$('render-wire').onclick = () => {
|
$('render-wire').onclick = () => {
|
||||||
api.view.set_wireframe(true, 0, api.space.is_dark() ? 0.25 : 0.5);
|
api.view.set_wireframe(true, 0, api.space.is_dark() ? 0.25 : 0.5);
|
||||||
api.visuals.set_opacity(1.0);
|
api.visuals.set_opacity(0.25);
|
||||||
api.conf.save();
|
api.conf.save();
|
||||||
};
|
};
|
||||||
$('render-solid').onclick = () => {
|
$('render-solid').onclick = () => {
|
||||||
|
|
@ -301,15 +409,24 @@ function setup_keybd_nav() {
|
||||||
$('mesh-split').onclick = selection.isolateBodies;
|
$('mesh-split').onclick = selection.isolateBodies;
|
||||||
$('context-duplicate').onclick = selection.duplicate;
|
$('context-duplicate').onclick = selection.duplicate;
|
||||||
$('context-mirror').onclick = selection.mirror;
|
$('context-mirror').onclick = selection.mirror;
|
||||||
|
$('context-rotate-panel').onclick = () => toggleSelectionPanel('panel-rotate');
|
||||||
|
$('context-scale-panel').onclick = () => toggleSelectionPanel('panel-scale');
|
||||||
|
$('panel-rotate-close').onmousedown = (ev) => { ev.stopPropagation(); };
|
||||||
|
$('panel-scale-close').onmousedown = (ev) => { ev.stopPropagation(); };
|
||||||
|
$('panel-rotate-close').onclick = (ev) => { ev.stopPropagation(); hideSelectionPanel('panel-rotate'); };
|
||||||
|
$('panel-scale-close').onclick = (ev) => { ev.stopPropagation(); hideSelectionPanel('panel-scale'); };
|
||||||
$('context-layflat').onclick = view_tools.startLayFlat;
|
$('context-layflat').onclick = view_tools.startLayFlat;
|
||||||
$('context-lefty').onclick = view_tools.startLeftAlign;
|
$('context-lefty').onclick = view_tools.startLeftAlign;
|
||||||
$('context-setfocus').onclick = () => {
|
$('context-setfocus').onclick = () => {
|
||||||
view_tools.startFocus(ev => api.space.set_focus(undefined, ev.object.point));
|
view_tools.startFocus(ev => api.space.set_focus(undefined, ev.object.point));
|
||||||
};
|
};
|
||||||
$('context-contents').onclick = api.const.SPACE.view.fit;
|
// $('context-contents').onclick = api.const.SPACE.view.fit;
|
||||||
$('view-fit').onclick = api.const.SPACE.view.fit;
|
$('view-fit').onclick = api.const.SPACE.view.fit;
|
||||||
$('wassup').onmouseover = () => { $('suppopp').classList.remove('hide') };
|
$('wassup').onmouseover = () => { $('suppopp').classList.remove('hide') };
|
||||||
|
|
||||||
|
makePanelDraggable('panel-rotate', 'panel-rotate-head', 'win.roate');
|
||||||
|
makePanelDraggable('panel-scale', 'panel-scale-head', 'win.scale');
|
||||||
|
|
||||||
// enable modal hiding
|
// enable modal hiding
|
||||||
$('mod-x').onclick = api.modal.hide;
|
$('mod-x').onclick = api.modal.hide;
|
||||||
|
|
||||||
|
|
@ -319,6 +436,13 @@ function setup_keybd_nav() {
|
||||||
sdb.gdpr = Date.now();
|
sdb.gdpr = Date.now();
|
||||||
};
|
};
|
||||||
|
|
||||||
// add app name hover info
|
// fix file input on iOS
|
||||||
$('app-info').innerText = version;
|
try {
|
||||||
|
if (/iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
||||||
|
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)) {
|
||||||
|
$('load-file').removeAttribute('accept');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('iOS remediation fail', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -889,7 +889,7 @@ function newSelect(label, options = {}, source) {
|
||||||
} else {
|
} else {
|
||||||
row.setAttribute("source", source || "tools");
|
row.setAttribute("source", source || "tools");
|
||||||
}
|
}
|
||||||
row.setAttribute("class", "var-row");
|
row.classList.add('var-row');
|
||||||
row.style.display = hide ? 'none' : '';
|
row.style.display = hide ? 'none' : '';
|
||||||
if (options.id) ip.setAttribute("id", options.id);
|
if (options.id) ip.setAttribute("id", options.id);
|
||||||
if (options.convert) ip.convert = options.convert.bind(ip);
|
if (options.convert) ip.convert = options.convert.bind(ip);
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ function setMode(mode, lock, then) {
|
||||||
// change mode constants
|
// change mode constants
|
||||||
current.mode = mode;
|
current.mode = mode;
|
||||||
MODE = MODES[mode];
|
MODE = MODES[mode];
|
||||||
|
document.title = 'Kiri:Moto | ' + mode;
|
||||||
// gcode edit area for any non-SLA mode
|
// gcode edit area for any non-SLA mode
|
||||||
api.uc.setVisible($('gcode-edit'), mode !== 'SLA');
|
api.uc.setVisible($('gcode-edit'), mode !== 'SLA');
|
||||||
// highlight selected mode menu item
|
// highlight selected mode menu item
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { api } from './api.js';
|
||||||
import { MODES } from './consts.js';
|
import { MODES } from './consts.js';
|
||||||
import { colorSchemeRegistry } from './color/schemes.js';
|
import { colorSchemeRegistry } from './color/schemes.js';
|
||||||
import { load as file_load } from '../../load/file.js';
|
import { load as file_load } from '../../load/file.js';
|
||||||
|
import { load_url as url_load } from '../../load/url.js';
|
||||||
import { newBounds } from '../../geo/bounds.js';
|
import { newBounds } from '../../geo/bounds.js';
|
||||||
import { Packer } from './pack.js';
|
import { Packer } from './pack.js';
|
||||||
import { space } from '../../moto/space.js';
|
import { space } from '../../moto/space.js';
|
||||||
|
|
@ -171,14 +172,14 @@ function update_size(updateDark = true) {
|
||||||
space.platform.setGrid(gridMajor, gridMinor, scheme.grid.major, scheme.grid.minor);
|
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.documentElement.setAttribute('data-theme', '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, scheme.grid.major, scheme.grid.minor);
|
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.documentElement.setAttribute('data-theme', 'light');
|
||||||
}
|
}
|
||||||
space.platform.setSize();
|
space.platform.setSize();
|
||||||
}
|
}
|
||||||
|
|
@ -491,7 +492,7 @@ function load_stl(url, onload, formdata, credentials, headers) {
|
||||||
*/
|
*/
|
||||||
function load_url(url, options = {}) {
|
function load_url(url, options = {}) {
|
||||||
platform.group();
|
platform.group();
|
||||||
file_load.URL.load(url, options).then(objects => {
|
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);
|
||||||
|
|
@ -978,6 +979,7 @@ function load_files(files, group) {
|
||||||
isobj = lower.endsWith(".obj"),
|
isobj = lower.endsWith(".obj"),
|
||||||
is3mf = lower.endsWith(".3mf"),
|
is3mf = lower.endsWith(".3mf"),
|
||||||
issvg = lower.endsWith(".svg"),
|
issvg = lower.endsWith(".svg"),
|
||||||
|
isdxf = lower.endsWith(".dxf"),
|
||||||
ispng = lower.endsWith(".png"),
|
ispng = lower.endsWith(".png"),
|
||||||
isjpg = lower.endsWith(".jpg"),
|
isjpg = lower.endsWith(".jpg"),
|
||||||
iskmz = lower.endsWith(".kmz"),
|
iskmz = lower.endsWith(".kmz"),
|
||||||
|
|
@ -1079,6 +1081,19 @@ function load_files(files, group) {
|
||||||
}
|
}
|
||||||
load_dec();
|
load_dec();
|
||||||
});
|
});
|
||||||
|
} else if (isdxf) {
|
||||||
|
loadDXFDialog(opt => {
|
||||||
|
group = group || [];
|
||||||
|
let dxf = file_load.DXF.parse(data.textDecode('utf-8'), opt);
|
||||||
|
let ind = 0;
|
||||||
|
if (dxf.length === 0) {
|
||||||
|
api.show.alert(`DXF contains no supported entities`, 10);
|
||||||
|
}
|
||||||
|
for (let v of dxf) {
|
||||||
|
load_verts(group, dxf[ind++], ind ? `${name}-${ind}` : name);
|
||||||
|
}
|
||||||
|
load_dec();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else if (iskmz) api.settings.import_zip(data, true);
|
else if (iskmz) api.settings.import_zip(data, true);
|
||||||
else if (isset) api.settings.import(data.textDecode('utf-8'), true);
|
else if (isset) api.settings.import(data.textDecode('utf-8'), true);
|
||||||
|
|
@ -1124,6 +1139,64 @@ function loadSVGDialog(doit) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show dialog to configure DXF import settings.
|
||||||
|
* Prompts for extrusion depth, arc segment size, and nesting.
|
||||||
|
* @param {Function} doit - Callback with options: {soup, depth, segmentSize, minSegments}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function loadDXFDialog(doit) {
|
||||||
|
const rnd = Date.now().toString(36);
|
||||||
|
const host = $('mod-any');
|
||||||
|
host.innerHTML = [
|
||||||
|
`<div class="image-convert-dialog f-col a-center">`,
|
||||||
|
` <h3 class="image-convert-title">Import DXF</h3>`,
|
||||||
|
` <p class="image-convert-copy t-just">`,
|
||||||
|
` Extrude a 3D model from a 2D DXF.`,
|
||||||
|
` Supports POLYLINE, LWPOLYLINE, LINE, CIRCLE, ARC, and SPLINE entities.`,
|
||||||
|
` </p>`,
|
||||||
|
` <div class="f-row t-right image-convert-fields"><table>`,
|
||||||
|
` <tr><th>units</th><td><select id="dxf-units-${rnd}"><option value="auto" selected>auto</option><option value="mm">millimeters</option><option value="inch">inches</option></select></td></tr>`,
|
||||||
|
` <tr><th>z height</th><td><input id="dxf-depth-${rnd}" value="5" size="3"></td></tr>`,
|
||||||
|
` <tr><th title="target length of each line segment when converting arcs and circles">arc segment size</th><td><input id="dxf-seg-${rnd}" value="1" size="3"></td></tr>`,
|
||||||
|
` <tr><th title="minimum number of segments for very small arcs to avoid degenerate geometry">minimum arc segments</th><td><input id="dxf-min-${rnd}" value="4" size="3"></td></tr>`,
|
||||||
|
` <tr><th>nest shapes</th><td><input id="dxf-nest-${rnd}" type="checkbox" checked></td></tr>`,
|
||||||
|
` </table></div>`,
|
||||||
|
` <div class="f-row j-end image-convert-actions">`,
|
||||||
|
` <button id="dxf-convert-ok-${rnd}">import</button>`,
|
||||||
|
` <button id="dxf-convert-cancel-${rnd}">cancel</button>`,
|
||||||
|
` </div>`,
|
||||||
|
`</div>`
|
||||||
|
].join('');
|
||||||
|
|
||||||
|
const units = $(`dxf-units-${rnd}`);
|
||||||
|
const depth = $(`dxf-depth-${rnd}`);
|
||||||
|
const segmentSize = $(`dxf-seg-${rnd}`);
|
||||||
|
const minSegments = $(`dxf-min-${rnd}`);
|
||||||
|
const nest = $(`dxf-nest-${rnd}`);
|
||||||
|
const okBtn = $(`dxf-convert-ok-${rnd}`);
|
||||||
|
const cancelBtn = $(`dxf-convert-cancel-${rnd}`);
|
||||||
|
|
||||||
|
okBtn.onclick = () => {
|
||||||
|
api.modal.hide();
|
||||||
|
setTimeout(() => {
|
||||||
|
doit({
|
||||||
|
soup: nest.checked,
|
||||||
|
depth: Math.max(0.1, parseFloat(depth.value)),
|
||||||
|
segmentSize: Math.max(0.01, parseFloat(segmentSize.value)),
|
||||||
|
minSegments: Math.max(3, parseInt(minSegments.value)),
|
||||||
|
units: units.value
|
||||||
|
});
|
||||||
|
}, 50);
|
||||||
|
};
|
||||||
|
cancelBtn.onclick = () => api.modal.hide();
|
||||||
|
depth.onkeypress = (ev) => {
|
||||||
|
if (ev.key === 'Enter' || ev.charCode === 13) okBtn.click();
|
||||||
|
};
|
||||||
|
api.modal.show('any');
|
||||||
|
setTimeout(() => depth.focus(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expand platform bed depth to fit widgets (belt mode only).
|
* Expand platform bed depth to fit widgets (belt mode only).
|
||||||
* Finds maximum Y dimension of all widgets and expands bed if needed.
|
* Finds maximum Y dimension of all widgets and expands bed if needed.
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ function booleanSave() {
|
||||||
control.assembly = ui.assembly.checked;
|
control.assembly = ui.assembly.checked;
|
||||||
control.autoLayout = ui.autoLayout.checked;
|
control.autoLayout = ui.autoLayout.checked;
|
||||||
control.autoSave = ui.autoSave.checked;
|
control.autoSave = ui.autoSave.checked;
|
||||||
control.dark = ui.dark.checked;
|
control.dark = api.sdb['kiri-dark'] = ui.dark.checked;
|
||||||
control.devel = ui.devel.checked;
|
control.devel = ui.devel.checked;
|
||||||
control.drawer = ui.drawer.checked;
|
control.drawer = ui.drawer.checked;
|
||||||
control.exportOcto = ui.exportOcto.checked;
|
control.exportOcto = ui.exportOcto.checked;
|
||||||
|
|
@ -109,6 +109,7 @@ function booleanSave() {
|
||||||
updateDrawer();
|
updateDrawer();
|
||||||
api.event.emit('boolean.update');
|
api.event.emit('boolean.update');
|
||||||
space.view.setProjection(control.ortho ? 'orthographic' : 'perspective');
|
space.view.setProjection(control.ortho ? 'orthographic' : 'perspective');
|
||||||
|
setDarkLight(control.dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDrawer() {
|
function updateDrawer() {
|
||||||
|
|
@ -127,3 +128,5 @@ export const preferences = {
|
||||||
booleanSave,
|
booleanSave,
|
||||||
updateDrawer
|
updateDrawer
|
||||||
};
|
};
|
||||||
|
|
||||||
|
api.prefs = preferences;
|
||||||
|
|
@ -6,6 +6,7 @@ import { THREE } from '../../ext/three.js';
|
||||||
import { tool as MeshTool } from '../../mesh/tool.js';
|
import { tool as MeshTool } from '../../mesh/tool.js';
|
||||||
import { encode as objEncode } from '../../load/obj.js';
|
import { encode as objEncode } from '../../load/obj.js';
|
||||||
import { encode as stlEncode } from '../../load/stl.js';
|
import { encode as stlEncode } from '../../load/stl.js';
|
||||||
|
import { $ } from '../../moto/webui.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Array of currently selected widget meshes.
|
* Array of currently selected widget meshes.
|
||||||
|
|
|
||||||
|
|
@ -42,11 +42,11 @@ function setViewMode(mode) {
|
||||||
['view-arrange','act-slice','act-preview','act-animate'].forEach(el => {
|
['view-arrange','act-slice','act-preview','act-animate'].forEach(el => {
|
||||||
$(el).classList.remove('selected')
|
$(el).classList.remove('selected')
|
||||||
});
|
});
|
||||||
$('render-tools').classList.add('hide');
|
// $('render-tools').classList.add('hide');
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case VIEWS.ARRANGE:
|
case VIEWS.ARRANGE:
|
||||||
$('view-arrange').classList.add('selected');
|
$('view-arrange').classList.add('selected');
|
||||||
$('render-tools').classList.remove('hide');
|
// $('render-tools').classList.remove('hide');
|
||||||
api.function.clear_progress();
|
api.function.clear_progress();
|
||||||
api.client.clear();
|
api.client.clear();
|
||||||
STACKS.clear();
|
STACKS.clear();
|
||||||
|
|
|
||||||
|
|
@ -315,17 +315,41 @@ function applyVisualState(widget) {
|
||||||
* @param {string} [msg] - Optional status message to display
|
* @param {string} [msg] - Optional status message to display
|
||||||
*/
|
*/
|
||||||
function setProgress(value = 0, msg) {
|
function setProgress(value = 0, msg) {
|
||||||
value = (value * 100).round(4);
|
const overlay = api.ui.progressOverlay;
|
||||||
api.ui.progress.width = value+'%';
|
const ring = api.ui.progress;
|
||||||
if (self.debug) {
|
const pct = api.ui.progressPct;
|
||||||
// console.log(msg, value.round(2));
|
const text = api.ui.prostatus;
|
||||||
api.ui.prostatus.style.display = 'flex';
|
|
||||||
if (msg) {
|
if (!overlay || !ring || !text) {
|
||||||
api.ui.prostatus.innerHTML = msg;
|
return;
|
||||||
} else {
|
|
||||||
api.ui.prostatus.innerHTML = '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasNumeric = typeof value === 'number' && Number.isFinite(value);
|
||||||
|
const indeterminate = !hasNumeric || (hasNumeric && value < 0);
|
||||||
|
const isHidden = hasNumeric && value === 0;
|
||||||
|
|
||||||
|
if (isHidden) {
|
||||||
|
overlay.classList.add('hide');
|
||||||
|
ring.classList.remove('indeterminate');
|
||||||
|
if (pct) pct.innerText = '';
|
||||||
|
text.innerText = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay.classList.remove('hide');
|
||||||
|
|
||||||
|
if (!indeterminate) {
|
||||||
|
const clamped = Math.max(0, Math.min(1, value));
|
||||||
|
ring.classList.remove('indeterminate');
|
||||||
|
ring.style.setProperty('--progress-deg', `${(clamped * 360).toFixed(2)}deg`);
|
||||||
|
if (pct) pct.innerText = `${Math.round(clamped * 100)}%`;
|
||||||
|
} else {
|
||||||
|
ring.classList.add('indeterminate');
|
||||||
|
ring.style.removeProperty('--progress-deg');
|
||||||
|
if (pct) pct.innerText = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
text.innerText = msg || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
let statsTimer;
|
let statsTimer;
|
||||||
|
|
|
||||||
|
|
@ -288,11 +288,18 @@ class Print {
|
||||||
if (safeEval) {
|
if (safeEval) {
|
||||||
safeEval.setContext(consts);
|
safeEval.setContext(consts);
|
||||||
}
|
}
|
||||||
|
function doSafeEval(tok) {
|
||||||
|
try {
|
||||||
|
return safeEval ? safeEval.eval(tok) : undefined;
|
||||||
|
} catch (e) {
|
||||||
|
console.log({ macro_error: tok });
|
||||||
|
}
|
||||||
|
}
|
||||||
function tryeval(str) {
|
function tryeval(str) {
|
||||||
try {
|
try {
|
||||||
return eval(`{ ${str} }`)
|
return eval(`{ ${str} }`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log({ eval_error: e, str });
|
console.log({ macro_error: e, str });
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -319,7 +326,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 = safeEval ? safeEval.eval(tok) : tryeval(eva.join(''));
|
let evl = doSafeEval(tok) ?? tryeval(eva.join(''));
|
||||||
nutok = evl;
|
nutok = evl;
|
||||||
if (pad === 666) {
|
if (pad === 666) {
|
||||||
return evl;
|
return evl;
|
||||||
|
|
|
||||||
|
|
@ -786,7 +786,7 @@ class Widget {
|
||||||
return this.cache.shadow = stack;
|
return this.cache.shadow = stack;
|
||||||
}
|
}
|
||||||
|
|
||||||
async computeShadowStack(zlist, progress, pocket) {
|
async computeShadowStack(zlist, progress, pocket, up = 1) {
|
||||||
let shadow_stack = this.cache.shadow_stack;
|
let shadow_stack = this.cache.shadow_stack;
|
||||||
if (!shadow_stack) {
|
if (!shadow_stack) {
|
||||||
shadow_stack = this.cache.shadow_stack = {};
|
shadow_stack = this.cache.shadow_stack = {};
|
||||||
|
|
@ -803,7 +803,7 @@ class Widget {
|
||||||
let p = work.minions.queueAsync({
|
let p = work.minions.queueAsync({
|
||||||
cmd: 'cam_shadow_z',
|
cmd: 'cam_shadow_z',
|
||||||
z: z - 0.005,
|
z: z - 0.005,
|
||||||
t: z + 1
|
t: z + up
|
||||||
}).then(reply => {
|
}).then(reply => {
|
||||||
shadow_stack[z - 0.005] = decode(reply.data);
|
shadow_stack[z - 0.005] = decode(reply.data);
|
||||||
pval += pinc;
|
pval += pinc;
|
||||||
|
|
|
||||||
182
src/kiri/dev/cam/Makera.Carvera.Air.json
Normal file
182
src/kiri/dev/cam/Makera.Carvera.Air.json
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
{
|
||||||
|
"mode": "CAM",
|
||||||
|
"internal": 0,
|
||||||
|
"bedHeight": 2.5,
|
||||||
|
"bedWidth": 300,
|
||||||
|
"bedDepth": 200,
|
||||||
|
"originCenter": false,
|
||||||
|
"spindleMax": 13000,
|
||||||
|
"gcodePre": [
|
||||||
|
"G21 ; set units to MM (required)",
|
||||||
|
"G90 ; absolute position mode (required)",
|
||||||
|
"G0 F2000 ; default rapid move speed",
|
||||||
|
"G1 F1000 ; default cutting speed"
|
||||||
|
],
|
||||||
|
"gcodePost": [
|
||||||
|
"M5 ; spindle off",
|
||||||
|
"G0 F4000 ; default rapid move speed",
|
||||||
|
"G1 F1000 ; default cutting speed",
|
||||||
|
"M30 ; program end"
|
||||||
|
],
|
||||||
|
"gcodeDwell": [
|
||||||
|
"G4 P{time} ; dwell for {time}ms"
|
||||||
|
],
|
||||||
|
"gcodeSpindle": [
|
||||||
|
"M3 S{spindle}",
|
||||||
|
"G4 P4000"
|
||||||
|
],
|
||||||
|
"gcodeChange": [
|
||||||
|
"M6 T{tool} ; change tool to '{tool_name}'"
|
||||||
|
],
|
||||||
|
"gcodeFExt": "nc",
|
||||||
|
"gcodeSpace": true,
|
||||||
|
"gcodeStrip": false,
|
||||||
|
"gcodeResetA": ["G92.4 A0 R0"],
|
||||||
|
"new": false,
|
||||||
|
"deviceName": "Makera Carvera Air",
|
||||||
|
"maxHeight": 130,
|
||||||
|
"useLaser": true,
|
||||||
|
"useIndexed": true,
|
||||||
|
"imageURL": "",
|
||||||
|
"bedBelt": false,
|
||||||
|
"bedRound": false,
|
||||||
|
"fwRetract": false,
|
||||||
|
"profiles": [
|
||||||
|
{
|
||||||
|
"processName": "default",
|
||||||
|
"camLevelTool": 1000,
|
||||||
|
"camLevelSpindle": 1000,
|
||||||
|
"camLevelOver": 0,
|
||||||
|
"camLevelSpeed": 1000,
|
||||||
|
"camLevelDown": 0,
|
||||||
|
"camRoughTool": 1666284436865,
|
||||||
|
"camRoughSpindle": 20000,
|
||||||
|
"camRoughDown": 3,
|
||||||
|
"camRoughOver": 0.25,
|
||||||
|
"camRoughSpeed": 1000,
|
||||||
|
"camRoughPlunge": 250,
|
||||||
|
"camRoughStock": 0,
|
||||||
|
"camRoughVoid": false,
|
||||||
|
"camRoughFlat": false,
|
||||||
|
"camRoughTop": false,
|
||||||
|
"camRoughIn": false,
|
||||||
|
"camRoughOn": true,
|
||||||
|
"camOutlineTool": 1665437735549,
|
||||||
|
"camOutlineSpindle": 0,
|
||||||
|
"camOutlineDown": 1,
|
||||||
|
"camOutlineOver": 0.4,
|
||||||
|
"camOutlineSpeed": 2000,
|
||||||
|
"camOutlinePlunge": 500,
|
||||||
|
"camOutlineWide": false,
|
||||||
|
"camOutlineDogbone": false,
|
||||||
|
"camOutlineOmitThru": false,
|
||||||
|
"camOutlineOut": false,
|
||||||
|
"camOutlineIn": false,
|
||||||
|
"camOutlineOn": true,
|
||||||
|
"camContourTool": 1666284436865,
|
||||||
|
"camContourSpindle": 20000,
|
||||||
|
"camContourOver": 0.15,
|
||||||
|
"camContourSpeed": 1500,
|
||||||
|
"camContourAngle": 85,
|
||||||
|
"camContourCurves": true,
|
||||||
|
"camContourIn": false,
|
||||||
|
"camContourXOn": true,
|
||||||
|
"camContourYOn": true,
|
||||||
|
"camTraceTool": 1002,
|
||||||
|
"camTraceSpindle": 1000,
|
||||||
|
"camTraceType": "clear",
|
||||||
|
"camTraceOver": 0.5,
|
||||||
|
"camTraceDown": 1,
|
||||||
|
"camTraceSpeed": 750,
|
||||||
|
"camTracePlunge": 200,
|
||||||
|
"camTraceLines": false,
|
||||||
|
"camDrillTool": 1000,
|
||||||
|
"camDrillSpindle": 1000,
|
||||||
|
"camDrillDownSpeed": 250,
|
||||||
|
"camDrillDown": 2,
|
||||||
|
"camDrillDwell": 250,
|
||||||
|
"camDrillLift": 2,
|
||||||
|
"camDrillingOn": false,
|
||||||
|
"camRegisterSpeed": 1000,
|
||||||
|
"camFlipAxis": "X",
|
||||||
|
"camFlipOther": "",
|
||||||
|
"camTabsWidth": 20,
|
||||||
|
"camTabsHeight": 5,
|
||||||
|
"camTabsDepth": 10,
|
||||||
|
"camTabsMidline": false,
|
||||||
|
"camDepthFirst": true,
|
||||||
|
"camEaseDown": false,
|
||||||
|
"camOriginTop": true,
|
||||||
|
"camZAnchor": "middle",
|
||||||
|
"camZOffset": 0,
|
||||||
|
"camZBottom": 0,
|
||||||
|
"camZClearance": 1,
|
||||||
|
"camZThru": 0,
|
||||||
|
"camFastFeed": 3000,
|
||||||
|
"camFastFeedZ": 500,
|
||||||
|
"camTolerance": 0,
|
||||||
|
"camStockX": 0,
|
||||||
|
"camStockY": 0,
|
||||||
|
"camStockZ": 0,
|
||||||
|
"camStockOffset": true,
|
||||||
|
"camStockClipTo": false,
|
||||||
|
"camStockOn": true,
|
||||||
|
"camConventional": false,
|
||||||
|
"camOriginCenter": false,
|
||||||
|
"outputInvertX": false,
|
||||||
|
"outputInvertY": false,
|
||||||
|
"camExpertFast": false,
|
||||||
|
"ops": [],
|
||||||
|
"op2": [
|
||||||
|
{
|
||||||
|
"type": "flip",
|
||||||
|
"axis": "X",
|
||||||
|
"invert": true,
|
||||||
|
"disabled": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"camTrueShadow": false,
|
||||||
|
"camDrillMark": true,
|
||||||
|
"camPocketSpindle": 10000,
|
||||||
|
"camPocketTool": 1001,
|
||||||
|
"camPocketOver": 0.25,
|
||||||
|
"camPocketDown": 5,
|
||||||
|
"camPocketSpeed": 2000,
|
||||||
|
"camPocketPlunge": 500,
|
||||||
|
"camPocketExpand": 0,
|
||||||
|
"camContourBottom": false,
|
||||||
|
"camTraceBottom": false,
|
||||||
|
"cmaPocketOutline": false,
|
||||||
|
"camRegisterThru": 5,
|
||||||
|
"camFlatness": 0.001,
|
||||||
|
"camContourBridge": 10,
|
||||||
|
"camForceZMax": false,
|
||||||
|
"camPocketSmooth": 1,
|
||||||
|
"camPocketContour": true,
|
||||||
|
"cmaPocketRefine": 20,
|
||||||
|
"camPocketEngrave": false,
|
||||||
|
"camLaserEnable": [
|
||||||
|
"M321"
|
||||||
|
],
|
||||||
|
"camLaserDisable": "M322",
|
||||||
|
"camLaserOn": [
|
||||||
|
"M3"
|
||||||
|
],
|
||||||
|
"camLaserOff": [
|
||||||
|
"M5"
|
||||||
|
],
|
||||||
|
"camLaserSpeed": 100,
|
||||||
|
"camLaserPower": 1,
|
||||||
|
"camLaserAdaptive": true,
|
||||||
|
"camLaserAdaptMod": true,
|
||||||
|
"camLaserFlatten": false,
|
||||||
|
"camLaserFlatZ": 0,
|
||||||
|
"camLaserPowerMin": 1,
|
||||||
|
"camLaserPowerMax": 0.1,
|
||||||
|
"camLaserZMin": 0,
|
||||||
|
"camLaserZMax": 0,
|
||||||
|
"camOutlineTop": false,
|
||||||
|
"X": "X"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
"gcodeStrip": false,
|
"gcodeStrip": false,
|
||||||
"gcodeResetA": ["G92.4 A0 R0"],
|
"gcodeResetA": ["G92.4 A0 R0"],
|
||||||
"new": false,
|
"new": false,
|
||||||
"deviceName": "My Makera Carvera",
|
"deviceName": "Makera Carvera",
|
||||||
"maxHeight": 150,
|
"maxHeight": 150,
|
||||||
"useLaser": true,
|
"useLaser": true,
|
||||||
"useIndexed": true,
|
"useIndexed": true,
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export function animate_clear(api) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function animate(api, delay) {
|
export function animate(api, delay) {
|
||||||
let alert = api.alerts.show("building animation");
|
api.show.busy("building animation");
|
||||||
let settings = api.conf.get();
|
let settings = api.conf.get();
|
||||||
client.animate_setup(settings, data => {
|
client.animate_setup(settings, data => {
|
||||||
checkMeshCommands(data);
|
checkMeshCommands(data);
|
||||||
|
|
@ -80,7 +80,7 @@ export function animate(api, delay) {
|
||||||
button.pause.style.display = 'none';
|
button.pause.style.display = 'none';
|
||||||
|
|
||||||
api.event.emit('animate', 'CAM');
|
api.event.emit('animate', 'CAM');
|
||||||
api.alerts.hide(alert);
|
api.show.busy(false);
|
||||||
space.platform.showGridBelow(false);
|
space.platform.showGridBelow(false);
|
||||||
toggleTrans(0,api.local.getBoolean('cam.anim.trans', true));
|
toggleTrans(0,api.local.getBoolean('cam.anim.trans', true));
|
||||||
toggleModel(0,api.local.getBoolean('cam.anim.model', false));
|
toggleModel(0,api.local.getBoolean('cam.anim.model', false));
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ export function animate_clear2(api) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function animate2(api, delay) {
|
export function animate2(api, delay) {
|
||||||
let alert = api.alerts.show("building animation");
|
api.show.busy("building animation");
|
||||||
let settings = api.conf.get();
|
let settings = api.conf.get();
|
||||||
dark = settings.controller.dark;
|
dark = settings.controller.dark;
|
||||||
manifold = settings.controller.manifold;
|
manifold = settings.controller.manifold;
|
||||||
|
|
@ -92,7 +92,7 @@ export function animate2(api, delay) {
|
||||||
button.pause.style.display = 'none';
|
button.pause.style.display = 'none';
|
||||||
|
|
||||||
api.event.emit('animate', 'CAM');
|
api.event.emit('animate', 'CAM');
|
||||||
api.alerts.hide(alert);
|
api.show.busy(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -485,8 +485,10 @@ export function createPopOps() {
|
||||||
rate: 'camTraceSpeed',
|
rate: 'camTraceSpeed',
|
||||||
plunge: 'camTracePlunge',
|
plunge: 'camTracePlunge',
|
||||||
offover: 'camTraceOffOver',
|
offover: 'camTraceOffOver',
|
||||||
|
offz: 'camTraceOffZ',
|
||||||
dogbone: 'camTraceDogbone',
|
dogbone: 'camTraceDogbone',
|
||||||
revbone: 'camTraceDogbone',
|
revbone: 'camTraceDogbone',
|
||||||
|
ignore: 'camTraceIgnore',
|
||||||
merge: 'camTraceMerge',
|
merge: 'camTraceMerge',
|
||||||
ov_topz: 0,
|
ov_topz: 0,
|
||||||
ov_botz: 0,
|
ov_botz: 0,
|
||||||
|
|
@ -500,9 +502,11 @@ export function createPopOps() {
|
||||||
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" }),
|
||||||
|
offz: UC.newInput(LANG.cc_offz_s, { title: LANG.cc_offz_l, convert: toFloat, units, show: () => env.poppedRec.mode === "follow" }),
|
||||||
sep: UC.newBlank({ class: "pop-sep", modes: MODES.CAM, xshow: zDogSep }),
|
sep: UC.newBlank({ class: "pop-sep", modes: MODES.CAM, xshow: zDogSep }),
|
||||||
thru: UC.newBoolean(LANG.cc_thru_s, undefined, { title: LANG.cc_thru_l }),
|
thru: UC.newBoolean(LANG.cc_thru_s, undefined, { title: LANG.cc_thru_l }),
|
||||||
merge: UC.newBoolean(LANG.co_merg_s, undefined, { title: LANG.co_merg_l, show: () => !env.popOp.trace.rec.down }),
|
ignore: UC.newBoolean(LANG.co_igno_s, undefined, { title: LANG.co_igno_l, show: () => env.poppedRec.mode === 'clear' }),
|
||||||
|
// 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("feeds & speeds", { }),
|
exp: UC.newExpand("feeds & speeds", { }),
|
||||||
|
|
@ -774,6 +778,7 @@ export function createPopOps() {
|
||||||
revbones: 'camAreaRevbones',
|
revbones: 'camAreaRevbones',
|
||||||
ov_topz: 0,
|
ov_topz: 0,
|
||||||
ov_botz: 0,
|
ov_botz: 0,
|
||||||
|
finish_cut: 0,
|
||||||
}).inputs = {
|
}).inputs = {
|
||||||
mode: UC.newSelect(LANG.mo_menu, { post: opRender }, "opmode"),
|
mode: UC.newSelect(LANG.mo_menu, { post: opRender }, "opmode"),
|
||||||
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"),
|
||||||
|
|
@ -801,6 +806,7 @@ export function createPopOps() {
|
||||||
sr_angle: UC.newInput(LANG.ca_sang_s, { title: LANG.ca_sang_l, 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() }),
|
||||||
|
finish_cut: UC.newInput(LANG.ca_fini_s, { title: LANG.ca_fini_l, convert: toFloat, bound: UC.bound(0, 10.0), units, show: () => isClear() }), //todo: needs to check camInnerFirst
|
||||||
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 }),
|
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 }),
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ function zAnchorSave() {
|
||||||
api.platform.update_top_z();
|
api.platform.update_top_z();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hideable = true;
|
||||||
const bottom = true;
|
const bottom = true;
|
||||||
const top = true;
|
const top = true;
|
||||||
|
|
||||||
|
|
@ -118,7 +119,7 @@ export function menu() {
|
||||||
|
|
||||||
/** Left Side Menu */
|
/** Left Side Menu */
|
||||||
|
|
||||||
_____: newGroup(LANG.ct_menu, $('cam-tabs'), { modes:CAM, marker:true, driven, separator }),
|
_____: newGroup(LANG.ct_menu, $('cam-tabs'), { modes:CAM, marker:true, driven, separator, hideable, group:"cam-tabs" }),
|
||||||
camTabsWidth: newInput(LANG.ct_wdth_s, {title:LANG.ct_wdth_l, convert:toFloat, bound:bound(0.005,100), units}),
|
camTabsWidth: newInput(LANG.ct_wdth_s, {title:LANG.ct_wdth_l, convert:toFloat, bound:bound(0.005,100), units}),
|
||||||
camTabsHeight: newInput(LANG.ct_hght_s, {title:LANG.ct_hght_l, convert:toFloat, bound:bound(0.005,100), units}),
|
camTabsHeight: newInput(LANG.ct_hght_s, {title:LANG.ct_hght_l, convert:toFloat, bound:bound(0.005,100), units}),
|
||||||
camTabsDepth: newInput(LANG.ct_dpth_s, {title:LANG.ct_dpth_l, convert:toFloat, bound:bound(0.005,100), units}),
|
camTabsDepth: newInput(LANG.ct_dpth_s, {title:LANG.ct_dpth_l, convert:toFloat, bound:bound(0.005,100), units}),
|
||||||
|
|
@ -130,7 +131,7 @@ export function menu() {
|
||||||
(ui.tabDun = newButton(undefined, onButtonClick, {icon:'<i class="fas fa-check"></i>'})),
|
(ui.tabDun = newButton(undefined, onButtonClick, {icon:'<i class="fas fa-check"></i>'})),
|
||||||
(ui.tabClr = newButton(undefined, onButtonClick, {icon:'<i class="fas fa-trash-alt"></i>'}))
|
(ui.tabClr = newButton(undefined, onButtonClick, {icon:'<i class="fas fa-trash-alt"></i>'}))
|
||||||
], {class:"ext-buttons f-row"}),
|
], {class:"ext-buttons f-row"}),
|
||||||
_____: newGroup(LANG.cs_menu, $('cam-stock'), { modes:CAM, driven, separator }),
|
_____: newGroup(LANG.cs_menu, $('cam-stock'), { modes:CAM, driven, separator, hideable, group:"cam-stock" }),
|
||||||
camStockX: newInput(LANG.cs_wdth_s, {title:LANG.cs_wdth_l, convert:toFloat, bound:bound(0,9999), units}),
|
camStockX: newInput(LANG.cs_wdth_s, {title:LANG.cs_wdth_l, convert:toFloat, bound:bound(0,9999), units}),
|
||||||
camStockY: newInput(LANG.cs_dpth_s, {title:LANG.cs_dpth_l, convert:toFloat, bound:bound(0,9999), units}),
|
camStockY: newInput(LANG.cs_dpth_s, {title:LANG.cs_dpth_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}),
|
camStockZ: newInput(LANG.cs_hght_s, {title:LANG.cs_hght_l, convert:toFloat, bound:bound(0,9999), units}),
|
||||||
|
|
@ -142,7 +143,7 @@ export function menu() {
|
||||||
// camStockManual: newRow([
|
// camStockManual: newRow([
|
||||||
// (ui.stockPlace = newButton('position', onButtonClick, { })),
|
// (ui.stockPlace = newButton('position', onButtonClick, { })),
|
||||||
// ], {class:"ext-buttons f-row"}),
|
// ], {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, hideable, group:"cam-limits" }),
|
||||||
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, selector, top }),
|
camZTop: newInput(LANG.ou_ztop_s, {title:LANG.ou_ztop_l, convert:toFloat, units, trigger, selector, top }),
|
||||||
|
|
@ -151,7 +152,7 @@ export function menu() {
|
||||||
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}),
|
||||||
camFastFeedZ: newInput(LANG.cc_rzpd_s, {title:LANG.cc_rzpd_l, convert:toFloat, units}),
|
camFastFeedZ: newInput(LANG.cc_rzpd_s, {title:LANG.cc_rzpd_l, convert:toFloat, units}),
|
||||||
_____: newGroup(LANG.ou_menu, $('cam-output'), { modes:CAM, driven, separator, group:"cam-output" }),
|
_____: newGroup(LANG.ou_menu, $('cam-output'), { modes:CAM, driven, separator, hideable, group:"cam-output" }),
|
||||||
camEaseDown: newBoolean(LANG.cr_ease_s, onBooleanClick, {title:LANG.cr_ease_l}),
|
camEaseDown: newBoolean(LANG.cr_ease_s, onBooleanClick, {title:LANG.cr_ease_l}),
|
||||||
camDepthFirst: newBoolean(LANG.ou_depf_s, onBooleanClick, {title:LANG.ou_depf_l}),
|
camDepthFirst: newBoolean(LANG.ou_depf_s, onBooleanClick, {title:LANG.ou_depf_l}),
|
||||||
camInnerFirst: newBoolean(LANG.ou_inrf_s, onBooleanClick, {title:LANG.ou_inrf_l}),
|
camInnerFirst: newBoolean(LANG.ou_inrf_s, onBooleanClick, {title:LANG.ou_inrf_l}),
|
||||||
|
|
@ -162,7 +163,7 @@ export function menu() {
|
||||||
separator: newBlank({ class:"set-sep", driven }),
|
separator: newBlank({ class:"set-sep", driven }),
|
||||||
camEaseAngle: newInput(LANG.ou_eang_s, {title:LANG.ou_eang_l, convert:toFloat, bound:bound(0.1,85), show:() => ui.camEaseDown.checked}),
|
camEaseAngle: newInput(LANG.ou_eang_s, {title:LANG.ou_eang_l, convert:toFloat, bound:bound(0.1,85), show:() => ui.camEaseDown.checked}),
|
||||||
camFullEngage: newInput(LANG.ou_feng_s, {title:LANG.ou_feng_l, convert:toFloat, bound:bound(0.1,1.0)}),
|
camFullEngage: newInput(LANG.ou_feng_s, {title:LANG.ou_feng_l, convert:toFloat, bound:bound(0.1,1.0)}),
|
||||||
_____: newGroup(LANG.or_menu, $('cam-origin'), { modes:CAM, driven, separator }),
|
_____: newGroup(LANG.or_menu, $('cam-origin'), { modes:CAM, driven, separator, hideable, group:"cam-origin" }),
|
||||||
camOriginTop: newBoolean(LANG.or_topp_s, onBooleanClick, {title:LANG.or_topp_l}),
|
camOriginTop: newBoolean(LANG.or_topp_s, onBooleanClick, {title:LANG.or_topp_l}),
|
||||||
camOriginCenter: newBoolean(LANG.or_cntr_s, onBooleanClick, {title:LANG.or_cntr_l}),
|
camOriginCenter: newBoolean(LANG.or_cntr_s, onBooleanClick, {title:LANG.or_cntr_l}),
|
||||||
separator: newBlank({ class:"set-sep", driven }),
|
separator: newBlank({ class:"set-sep", driven }),
|
||||||
|
|
@ -174,7 +175,7 @@ export function menu() {
|
||||||
newButton("select", originSelect),
|
newButton("select", originSelect),
|
||||||
newButton("reset", originReset),
|
newButton("reset", originReset),
|
||||||
], { class: "ext-buttons f-row" }),
|
], { class: "ext-buttons f-row" }),
|
||||||
_____: newGroup(LANG.op_xprt_s, $('cam-expert'), { group:"cam_expert", modes:CAM, marker: false, driven, separator }),
|
_____: newGroup(LANG.op_xprt_s, $('cam-expert'), { group:"cam_expert", modes:CAM, marker: false, driven, separator, hideable }),
|
||||||
camArcEnabled: newBoolean(LANG.cx_arce_s, onBooleanClick, { title:LANG.cx_arce_l }),
|
camArcEnabled: newBoolean(LANG.cx_arce_s, onBooleanClick, { title:LANG.cx_arce_l }),
|
||||||
camArcTolerance: newInput(LANG.cx_arct_s, {title:LANG.cx_arct_l, convert:toFloat, bound:bound(0,100), units, trigger, show:() => ui.camArcEnabled.checked}),
|
camArcTolerance: newInput(LANG.cx_arct_s, {title:LANG.cx_arct_l, convert:toFloat, bound:bound(0,100), units, trigger, show:() => ui.camArcEnabled.checked}),
|
||||||
camArcResolution: newInput(LANG.cx_arcr_s, {title:LANG.cx_arcr_l, convert:toFloat, bound:bound(0,180), trigger, show:() => ui.camArcEnabled.checked}),
|
camArcResolution: newInput(LANG.cx_arcr_s, {title:LANG.cx_arcr_l, convert:toFloat, bound:bound(0,180), trigger, show:() => ui.camArcEnabled.checked}),
|
||||||
|
|
|
||||||
|
|
@ -339,7 +339,7 @@ export function opRender() {
|
||||||
`<div id="${mark + i}" class="${clazz.join(' ')}"${title}>`,
|
`<div id="${mark + i}" class="${clazz.join(' ')}"${title}>`,
|
||||||
`<label class="label">${label}</label>`,
|
`<label class="label">${label}</label>`,
|
||||||
clock ? '' :
|
clock ? '' :
|
||||||
`<label id="${mark + i}-x" class="del"><i class="fa fa-trash"></i></label>`,
|
`<label id="${mark + i}-x" class="del"><i class="fa-solid fa-xmark"></i></label>`,
|
||||||
`</div>`
|
`</div>`
|
||||||
]);
|
]);
|
||||||
bind[mark + i] = rec;
|
bind[mark + i] = rec;
|
||||||
|
|
@ -421,9 +421,10 @@ export function opRender() {
|
||||||
const brect = ev.target.getBoundingClientRect();
|
const brect = ev.target.getBoundingClientRect();
|
||||||
const prect = parent.getBoundingClientRect();
|
const prect = parent.getBoundingClientRect();
|
||||||
const Prect = poprec.div.getBoundingClientRect();
|
const Prect = poprec.div.getBoundingClientRect();
|
||||||
const tdiff = prect.top - brect.top;
|
const topmv = brect.top - prect.top - Math.min(50, Prect.height/5);
|
||||||
const botoff = innerHeight - (brect.top + Prect.height);
|
const topnu = prect.top + topmv;
|
||||||
const offpx = -tdiff + (botoff < 0 ? botoff : -Prect.height/3);
|
const botof = innerHeight - (topnu + Prect.height);
|
||||||
|
const offpx = botof < 0 ? topmv + botof : topmv;
|
||||||
poprec.div.style.transform = `translateY(${offpx}px)`;
|
poprec.div.style.transform = `translateY(${offpx}px)`;
|
||||||
poprec.div.onmouseenter = () => { inside = true };
|
poprec.div.onmouseenter = () => { inside = true };
|
||||||
poprec.div.onmouseleave = onLeave;
|
poprec.div.onmouseleave = onLeave;
|
||||||
|
|
@ -587,7 +588,7 @@ export function zPlaneSelect({ which, onselect }) {
|
||||||
}
|
}
|
||||||
clearPops();
|
clearPops();
|
||||||
zPlaneStart(which, value => {
|
zPlaneStart(which, value => {
|
||||||
onselect(parseFloat(value));
|
onselect(parseFloat(value) / api.view.unit_scale());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,8 @@ export function cam_export(print, online) {
|
||||||
time: 0
|
time: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// console.log({ offset, origin, stock });
|
||||||
|
|
||||||
function section(section) {
|
function section(section) {
|
||||||
append();
|
append();
|
||||||
online({ section });
|
online({ section });
|
||||||
|
|
|
||||||
|
|
@ -134,12 +134,15 @@ class OpArea extends CamOp {
|
||||||
polys = POLY.union(nupolys, 0.00001, true);
|
polys = POLY.union(nupolys, 0.00001, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// filter out invalid polys
|
||||||
|
polys = polys.filter(p => p && p.length > 2);
|
||||||
|
|
||||||
// process each area separately
|
// process each area separately
|
||||||
let proc = 0;
|
let proc = 0;
|
||||||
let pinc = 1 / polys.length;
|
let pinc = 1 / polys.length;
|
||||||
for (let area of polys) {
|
for (let area of polys) {
|
||||||
let bounds = area.getBounds3D();
|
let bounds = area.getBounds3D();
|
||||||
let ts_off = toolDiam / 2 - ts_eps + (op.leave_xy ?? 0);
|
let ts_off = toolDiam / 2 + (op.leave_xy ?? 0) + ts_eps;
|
||||||
let offopt = {
|
let offopt = {
|
||||||
arc: 250,
|
arc: 250,
|
||||||
join: roundSharps ? ClipperLib.JoinType.jtRound : undefined,
|
join: roundSharps ? ClipperLib.JoinType.jtRound : undefined,
|
||||||
|
|
@ -175,7 +178,7 @@ class OpArea extends CamOp {
|
||||||
for (let z of zs) {
|
for (let z of zs) {
|
||||||
let slice = newLayer(z);
|
let slice = newLayer(z);
|
||||||
let layers = slice.output();
|
let layers = slice.output();
|
||||||
let shadow = await shadowAt(z);
|
let shadow = await shadowAt(z + 0.01);
|
||||||
let tool_shadow = [
|
let tool_shadow = [
|
||||||
...POLY.offset(shadow, [ ts_off ], { count: 1, z, ...offopt }),
|
...POLY.offset(shadow, [ ts_off ], { count: 1, z, ...offopt }),
|
||||||
...POLY.offset(shadow, [ -ts_off ], { count: 1, z, ...offopt }),
|
...POLY.offset(shadow, [ -ts_off ], { count: 1, z, ...offopt }),
|
||||||
|
|
@ -188,8 +191,23 @@ class OpArea extends CamOp {
|
||||||
let outs = [];
|
let outs = [];
|
||||||
let clip = [];
|
let clip = [];
|
||||||
let firstOff = -(toolDiam / 2 + (op.leave_xy ?? 0));
|
let firstOff = -(toolDiam / 2 + (op.leave_xy ?? 0));
|
||||||
POLY.subtract([ area ], shadow, clip, undefined, undefined, 0);
|
// remove shadow from area
|
||||||
POLY.offset(clip, [ firstOff, -toolOver ], {
|
if (op.ignore) {
|
||||||
|
clip = [ area ];
|
||||||
|
} else {
|
||||||
|
POLY.subtract([ area ], shadow, clip, undefined, undefined, 0);
|
||||||
|
}
|
||||||
|
//generate offsets to use
|
||||||
|
let offsets = [ firstOff ];
|
||||||
|
//if we need a finish cut, add it
|
||||||
|
let finish_cut = op.finish_cut ?? 0;
|
||||||
|
if (finish_cut != 0) { //todo: this should check for camInnerFirst and warn if it is not true
|
||||||
|
offsets.push(-finish_cut);
|
||||||
|
}
|
||||||
|
//everything else uses the tool stepover
|
||||||
|
offsets.push(-toolOver);
|
||||||
|
//actually offset the walls inwards
|
||||||
|
POLY.offset(clip, offsets, {
|
||||||
count: op.walls ? 1 : (op.steps ?? 999), outs, flat: true, z: z - zMov, ...offopt
|
count: op.walls ? 1 : (op.steps ?? 999), outs, flat: true, z: z - zMov, ...offopt
|
||||||
});
|
});
|
||||||
// if we see no offsets, re-check the mesh bottom Z then exit
|
// if we see no offsets, re-check the mesh bottom Z then exit
|
||||||
|
|
@ -248,14 +266,15 @@ class OpArea extends CamOp {
|
||||||
progress(proc, 'clear');
|
progress(proc, 'clear');
|
||||||
} else
|
} else
|
||||||
if (mode === 'trace') {
|
if (mode === 'trace') {
|
||||||
let { tr_over, tr_type } = op;
|
let { tr_over, tr_offz, tr_type } = op;
|
||||||
let zs = down ? base_util.lerp(zTop, op.thru ? zBottom : Math.max(zBottom, area.minZ()), down) : [ bounds.min.z ];
|
let zs = down ? base_util.lerp(zTop, op.thru ? zBottom : Math.max(zBottom, area.minZ()), down) : [ bounds.min.z ];
|
||||||
let zroc = 0;
|
let zroc = 0;
|
||||||
let zinc = 1 / zs.length;
|
let zinc = 1 / zs.length;
|
||||||
|
if (tr_offz) zs = zs.map(z => z - tr_offz);
|
||||||
for (let z of zs) {
|
for (let z of zs) {
|
||||||
let slice = newLayer(z);
|
let slice = newLayer(z);
|
||||||
let layers = slice.output();
|
let layers = slice.output();
|
||||||
let shadow = await shadowAt(z);
|
let shadow = op.base ? state.shadow.base : await shadowAt(z);
|
||||||
let outs = [];
|
let outs = [];
|
||||||
if (tr_type === 'none') {
|
if (tr_type === 'none') {
|
||||||
// todo: move this out of the zs loop and only setZ when needed
|
// todo: move this out of the zs loop and only setZ when needed
|
||||||
|
|
@ -462,7 +481,8 @@ class OpArea extends CamOp {
|
||||||
while (areas?.length) {
|
while (areas?.length) {
|
||||||
let min = {
|
let min = {
|
||||||
dist: Infinity,
|
dist: Infinity,
|
||||||
area: undefined
|
area: undefined,
|
||||||
|
point: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let area of areas.filter(p => !p.used)) {
|
for (let area of areas.filter(p => !p.used)) {
|
||||||
|
|
@ -477,16 +497,19 @@ class OpArea extends CamOp {
|
||||||
if (find.distance < min.dist) {
|
if (find.distance < min.dist) {
|
||||||
min.area = area;
|
min.area = area;
|
||||||
min.dist = find.distance;
|
min.dist = find.distance;
|
||||||
|
min.point = find.point
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we have a next-closest top poly, pocket that
|
// if we have a next-closest top poly, pocket that
|
||||||
if (min.area) {
|
if (min.area) {
|
||||||
min.area.used = true;
|
min.area.used = true;
|
||||||
|
printPoint = min.point;
|
||||||
pocket({
|
pocket({
|
||||||
cutdir: op.ov_conv,
|
cutdir: op.ov_conv,
|
||||||
depthFirst: process.camDepthFirst && !op.drape,
|
depthFirst: process.camDepthFirst,
|
||||||
easeDown: op.down && process.easeDown ? op.down : 0,
|
easeDown: op.down && process.easeDown ? op.down : 0,
|
||||||
|
outline: op.drape || op.mode === 'trace',
|
||||||
progress: (n,m) => progress(n/m, "area"),
|
progress: (n,m) => progress(n/m, "area"),
|
||||||
slices: min.area.filter(slice => slice.camLines)
|
slices: min.area.filter(slice => slice.camLines)
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ function createFilter(op, origin, axis) {
|
||||||
let index = 0;
|
let index = 0;
|
||||||
const accept = [];
|
const accept = [];
|
||||||
filter = function (slices) {
|
filter = function (slices) {
|
||||||
for (let slice of slices) {
|
for (let slice of slices.filter(s => s.camLines)) {
|
||||||
if (slice_fn && slice_fn(slice, index++)) {
|
if (slice_fn && slice_fn(slice, index++)) {
|
||||||
accept.push(slice);
|
accept.push(slice);
|
||||||
} else if (box) {
|
} else if (box) {
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,7 @@ class OpDrill extends CamOp {
|
||||||
|
|
||||||
async slice(progress) {
|
async slice(progress) {
|
||||||
let { op, state } = this;
|
let { op, state } = this;
|
||||||
let { settings, addSlices, widget, updateToolDiams } = state;
|
let { color, settings, addSlices, widget, updateToolDiams, zBottom } = state;
|
||||||
let { color } = state;
|
|
||||||
let { drills } = op
|
let { drills } = op
|
||||||
|
|
||||||
let drillTool = new Tool(settings, op.tool),
|
let drillTool = new Tool(settings, op.tool),
|
||||||
|
|
@ -39,6 +38,10 @@ class OpDrill extends CamOp {
|
||||||
}
|
}
|
||||||
|
|
||||||
drill.zBottom = drill.z - drill.depth;
|
drill.zBottom = drill.z - drill.depth;
|
||||||
|
|
||||||
|
// honor zBottom when set
|
||||||
|
if (zBottom) drill.zBottom = Math.max(zBottom, drill.zBottom);
|
||||||
|
|
||||||
// for thru holes, follow z thru when set
|
// for thru holes, follow z thru when set
|
||||||
if ((op.thru > 0)) {
|
if ((op.thru > 0)) {
|
||||||
drill.zBottom -= op.thru;
|
drill.zBottom -= op.thru;
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,9 @@ class OpLevel extends CamOp {
|
||||||
let zBot = zTop - down;
|
let zBot = zTop - down;
|
||||||
let zList = stepz && down ? util.lerp(zTop, zBot, stepz) : [ zBot ];
|
let zList = stepz && down ? util.lerp(zTop, zBot, stepz) : [ zBot ];
|
||||||
|
|
||||||
|
// ensure zList is descending
|
||||||
|
zList.sort((a,b) => b - a);
|
||||||
|
|
||||||
if (share.ran) {
|
if (share.ran) {
|
||||||
console.log('skip');
|
console.log('skip');
|
||||||
this.skip = true;
|
this.skip = true;
|
||||||
|
|
@ -49,6 +52,7 @@ class OpLevel extends CamOp {
|
||||||
POLY.fillArea(clear, 1090, stepOver, points);
|
POLY.fillArea(clear, 1090, stepOver, points);
|
||||||
|
|
||||||
let layers = this.layers = [];
|
let layers = this.layers = [];
|
||||||
|
|
||||||
for (let z of zList) {
|
for (let z of zList) {
|
||||||
let lines = [];
|
let lines = [];
|
||||||
layers.push(lines);
|
layers.push(lines);
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,11 @@ class OpOutline extends CamOp {
|
||||||
let areas = shadow.base.clone(true);
|
let areas = shadow.base.clone(true);
|
||||||
ops_list.push(new OpArea(state, {
|
ops_list.push(new OpArea(state, {
|
||||||
areas: { [widget.id]: areas.map(p => p.toArray()) },
|
areas: { [widget.id]: areas.map(p => p.toArray()) },
|
||||||
|
base: true,
|
||||||
direction,
|
direction,
|
||||||
dogbones,
|
dogbones,
|
||||||
down,
|
down,
|
||||||
|
drape: true,
|
||||||
expand: 0,
|
expand: 0,
|
||||||
mode: 'trace',
|
mode: 'trace',
|
||||||
omitinner: omitvoid,
|
omitinner: omitvoid,
|
||||||
|
|
|
||||||
|
|
@ -147,13 +147,13 @@ class OpRegister extends CamOp {
|
||||||
|
|
||||||
prepare(ops, progress) {
|
prepare(ops, progress) {
|
||||||
let { op } = this;
|
let { op } = this;
|
||||||
let { emitDrills, setDrill, setTool, setTravelBoundary } = ops;
|
let { emitDrills, emitTraces, setDrill, setTool, setTravelBoundary } = ops;
|
||||||
|
|
||||||
setTravelBoundary();
|
setTravelBoundary();
|
||||||
if (op.axis === '-' || op.axis === '=') {
|
if (op.axis === '-' || op.axis === '=') {
|
||||||
setTool(op.tool, op.feed, op.rate);
|
setTool(op.tool, op.feed, op.rate);
|
||||||
for (let slice of this.sliceOut) {
|
for (let slice of this.sliceOut) {
|
||||||
ops.emitTrace(slice);
|
emitTraces(slice.camLines);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setTool(op.tool, undefined, op.rate);
|
setTool(op.tool, undefined, op.rate);
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,10 @@ class OpRough extends CamOp {
|
||||||
|
|
||||||
// outside only if we're not clearing all of stock
|
// outside only if we're not clearing all of stock
|
||||||
if (cutOutside && !op.all) {
|
if (cutOutside && !op.all) {
|
||||||
|
if (op.leave) {
|
||||||
|
// recompute area with offset when provided
|
||||||
|
areas = POLY.flatten(POLY.expand(shadowBase, tool.fluteDiameter() / 2 - 0.001 + op.leave));
|
||||||
|
}
|
||||||
ops_list.push(new OpArea(state, {
|
ops_list.push(new OpArea(state, {
|
||||||
rename: op.rename ?? "cutout",
|
rename: op.rename ?? "cutout",
|
||||||
spindle: op.spindle,
|
spindle: op.spindle,
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ class OpTrace extends CamOp {
|
||||||
|
|
||||||
async slice(progress) {
|
async slice(progress) {
|
||||||
let { op, state } = this;
|
let { op, state } = this;
|
||||||
let { areas, direction, down, expand, follow, offover, offset, outline, mode, ov_botz, ov_topz } = op;
|
let { areas, direction, down, expand, follow, offover, offset, offz, outline, ignore, mode, ov_botz, ov_topz } = op;
|
||||||
let { plunge, rate, refine, smooth, spindle, step, steps, thru, tolerance, tool } = op;
|
let { plunge, rate, refine, smooth, spindle, step, steps, thru, tolerance, tool } = op;
|
||||||
let trace = {
|
let trace = {
|
||||||
areas,
|
areas,
|
||||||
|
|
@ -21,6 +21,7 @@ class OpTrace extends CamOp {
|
||||||
down,
|
down,
|
||||||
expand,
|
expand,
|
||||||
follow,
|
follow,
|
||||||
|
ignore,
|
||||||
mode: mode === 'clear' ? 'clear' : 'trace',
|
mode: mode === 'clear' ? 'clear' : 'trace',
|
||||||
outline,
|
outline,
|
||||||
ov_botz,
|
ov_botz,
|
||||||
|
|
@ -39,6 +40,7 @@ class OpTrace extends CamOp {
|
||||||
tool,
|
tool,
|
||||||
thru,
|
thru,
|
||||||
tr_over: offover,
|
tr_over: offover,
|
||||||
|
tr_offz: offz,
|
||||||
tr_type: offset
|
tr_type: offset
|
||||||
};
|
};
|
||||||
this.op_trace = new OpArea(state, trace);
|
this.op_trace = new OpArea(state, trace);
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
||||||
{ alignTop } = settings.controller,
|
{ alignTop } = settings.controller,
|
||||||
{ camArcEnabled, camArcResolution, camArcTolerance } = process,
|
{ camArcEnabled, camArcResolution, camArcTolerance } = process,
|
||||||
{ camDepthFirst, camEaseAngle, camEaseDown } = process,
|
{ camDepthFirst, camEaseAngle, camEaseDown } = process,
|
||||||
{ camFastFeed, camFastFeedZ } = process,
|
{ camFastFeed, camFastFeedZ, camZTop } = process,
|
||||||
{ camStockX, camStockY, camStockZ, camStockIndexed, camStockOffset } = process,
|
{ camStockX, camStockY, camStockZ, camStockIndexed, camStockOffset } = process,
|
||||||
{ camForceZMax, camFullEngage, camInnerFirst, camOriginCenter } = process,
|
{ camForceZMax, camFullEngage, camInnerFirst, camOriginCenter } = process,
|
||||||
{ camOriginOffX, camOriginOffY, camOriginOffZ, camZClearance } = process,
|
{ camOriginOffX, camOriginOffY, camOriginOffZ, camZClearance } = process,
|
||||||
|
|
@ -110,7 +110,7 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
||||||
wmx = wmpos.x,
|
wmx = wmpos.x,
|
||||||
wmy = wmpos.y,
|
wmy = wmpos.y,
|
||||||
wmz = !camStockIndexed ? stock.z - boundsZ : alignTop ? 0 : 0,
|
wmz = !camStockIndexed ? stock.z - boundsZ : alignTop ? 0 : 0,
|
||||||
zSafe = camStockIndexed ? Math.hypot(stock.y, stock.z) / 2 + camZClearance : stockZClear,
|
zSafe = Math.max(camZTop, camStockIndexed ? Math.hypot(stock.y, stock.z) / 2 + camZClearance : stockZClear),
|
||||||
originx = (camOriginCenter ? 0 : -stock.x / 2) + (camOriginOffX || 0),
|
originx = (camOriginCenter ? 0 : -stock.x / 2) + (camOriginOffX || 0),
|
||||||
originy = (camOriginCenter ? 0 : -stock.y / 2) + (camOriginOffY || 0),
|
originy = (camOriginCenter ? 0 : -stock.y / 2) + (camOriginOffY || 0),
|
||||||
origin = newPoint(originx, originy, zSafe),
|
origin = newPoint(originx, originy, zSafe),
|
||||||
|
|
@ -138,6 +138,7 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
||||||
toolType,
|
toolType,
|
||||||
toolDiam,
|
toolDiam,
|
||||||
toolDiamMove,
|
toolDiamMove,
|
||||||
|
toolDiamEpsilon,
|
||||||
travelBounds,
|
travelBounds,
|
||||||
spindle = 0,
|
spindle = 0,
|
||||||
spindleMax = device.spindleMax,
|
spindleMax = device.spindleMax,
|
||||||
|
|
@ -194,6 +195,7 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSpindle(speed) {
|
function setSpindle(speed) {
|
||||||
|
// console.trace({ setSpindle: speed });
|
||||||
spindle = Math.min(speed, spindleMax);
|
spindle = Math.min(speed, spindleMax);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,6 +213,7 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
||||||
toolType = tool.getType();
|
toolType = tool.getType();
|
||||||
toolDiam = tool.fluteDiameter();
|
toolDiam = tool.fluteDiameter();
|
||||||
toolDiamMove = (tool.hasTaper() ? tolerance ?? toolDiam : toolDiam) * 2;
|
toolDiamMove = (tool.hasTaper() ? tolerance ?? toolDiam : toolDiam) * 2;
|
||||||
|
toolDiamEpsilon = toolDiam * 0.01,
|
||||||
lastTool = toolID;
|
lastTool = toolID;
|
||||||
}
|
}
|
||||||
feedRate = Math.min(camFastFeed, feed || feedRate || plunge);
|
feedRate = Math.min(camFastFeed, feed || feedRate || plunge);
|
||||||
|
|
@ -387,7 +390,7 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
||||||
/**
|
/**
|
||||||
* when moving between contour endpoints, check if we can
|
* when moving between contour endpoints, check if we can
|
||||||
* instead route around the bounding area of the contour
|
* instead route around the bounding area of the contour
|
||||||
* whih we call the coastline.
|
* which we call the coastline.
|
||||||
*/
|
*/
|
||||||
function coastlineMove(point) {
|
function coastlineMove(point) {
|
||||||
let from = toWidgetCoords(printPoint);
|
let from = toWidgetCoords(printPoint);
|
||||||
|
|
@ -395,6 +398,7 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
||||||
if (!coastline || from.distTo2D(to) < 0.01) {
|
if (!coastline || from.distTo2D(to) < 0.01) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
let minz = Math.min(from.z, to.z);
|
||||||
let start = { dist: 1, poly: 0, pt: from };
|
let start = { dist: 1, poly: 0, pt: from };
|
||||||
let end = { dist: 1, poly: 1, pt: to };
|
let end = { dist: 1, poly: 1, pt: to };
|
||||||
for (let poly of coastline) {
|
for (let poly of coastline) {
|
||||||
|
|
@ -445,7 +449,9 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let i=sp, d=0; d < dist; i += dir, d++) {
|
for (let i=sp, d=0; d < dist; i += dir, d++) {
|
||||||
layerPush(toWorkCoords(points[i % pl]), 1, 0, tool);
|
let cp = points[i % pl].clone();
|
||||||
|
cp.z = Math.max(minz, cp.z);
|
||||||
|
layerPush(toWorkCoords(cp), 1, 0, tool);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -567,7 +573,7 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
||||||
} else
|
} else
|
||||||
// otherwise move over before descending
|
// otherwise move over before descending
|
||||||
if (deltaZ <= -tolerance) {
|
if (deltaZ <= -tolerance) {
|
||||||
if (debug) console.log('over before descend');
|
if (debug) console.log('over before descend', deltaZ, -tolerance);
|
||||||
layerPush(point.clone().setZ(printPoint.z), 0, 0, tool);
|
layerPush(point.clone().setZ(printPoint.z), 0, 0, tool);
|
||||||
newLayer();
|
newLayer();
|
||||||
}
|
}
|
||||||
|
|
@ -590,9 +596,11 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
||||||
if (lastTravelBounds) check.push(...lastTravelBounds);
|
if (lastTravelBounds) check.push(...lastTravelBounds);
|
||||||
let from = toWidgetCoords(printPoint);
|
let from = toWidgetCoords(printPoint);
|
||||||
let to = toWidgetCoords(point);
|
let to = toWidgetCoords(point);
|
||||||
|
let ep = toolDiamEpsilon;
|
||||||
for (let poly of check) {
|
for (let poly of check) {
|
||||||
let ints = poly.intersections(from, to);
|
let ints = poly.intersections(from, to) ?? [];
|
||||||
if (ints.length) {
|
let far = ints.filter(p => p.distTo2D(to) > ep && p.distTo2D(from) > ep);
|
||||||
|
if (far.length) {
|
||||||
if (debug) console.log({ ints, poly, deltaXY, deltaZ });
|
if (debug) console.log({ ints, poly, deltaXY, deltaZ });
|
||||||
upAndOver = "bounds";
|
upAndOver = "bounds";
|
||||||
break;
|
break;
|
||||||
|
|
@ -636,7 +644,7 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
||||||
|
|
||||||
// plunge safety catch
|
// plunge safety catch
|
||||||
if (deltaZ < 0 && !contouring) {
|
if (deltaZ < 0 && !contouring) {
|
||||||
if (debug) console.log('deltaZ snap', rate, plungeRate);
|
if (debug) console.log('plunge safety', deltaZ, rate, plungeRate);
|
||||||
emit = 1;
|
emit = 1;
|
||||||
rate = plungeRate;
|
rate = plungeRate;
|
||||||
}
|
}
|
||||||
|
|
@ -667,16 +675,21 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
||||||
* @param {boolean} cutdir true=CW false=CCW
|
* @param {boolean} cutdir true=CW false=CCW
|
||||||
* @param {boolean} depthFirst prioritize cut depth in pockets by nesting
|
* @param {boolean} depthFirst prioritize cut depth in pockets by nesting
|
||||||
*/
|
*/
|
||||||
function pocket({ slices, cutdir, depthFirst, progress }) {
|
function pocket({ slices, cutdir, depthFirst, outline, progress }) {
|
||||||
let total = 0;
|
let total = 0;
|
||||||
let depthData = [];
|
let depthData = [];
|
||||||
|
|
||||||
for (let slice of slices) {
|
for (let slice of slices) {
|
||||||
let polys = [], t = [], c = [];
|
let polys = [], t = [], c = [];
|
||||||
// use shadow + tool radius offset when available (roughing)
|
|
||||||
|
// collect polys in to tops (parents) and children
|
||||||
|
// so we can have the windings be opposite
|
||||||
POLY.flatten(slice.camLines).forEach((poly) => {
|
POLY.flatten(slice.camLines).forEach((poly) => {
|
||||||
|
// poly is child if has parent
|
||||||
let child = poly.parent;
|
let child = poly.parent;
|
||||||
|
// for depth, collapse parent to 1 or 0 (has, missing)
|
||||||
if (depthFirst) { poly = poly.clone(); poly.parent = child ? 1 : 0 }
|
if (depthFirst) { poly = poly.clone(); poly.parent = child ? 1 : 0 }
|
||||||
|
// place poly into top or child bucket
|
||||||
if (child) c.push(poly); else t.push(poly);
|
if (child) c.push(poly); else t.push(poly);
|
||||||
polys.push(poly);
|
polys.push(poly);
|
||||||
});
|
});
|
||||||
|
|
@ -687,6 +700,7 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
||||||
POLY.setWinding(c, !cutdir);
|
POLY.setWinding(c, !cutdir);
|
||||||
|
|
||||||
if (depthFirst) {
|
if (depthFirst) {
|
||||||
|
// re-nest layer polys and add to depth stack
|
||||||
polys = POLY.nest(polys,true,true);
|
polys = POLY.nest(polys,true,true);
|
||||||
polys.tool_shadow = POLY.flatten(slice.tool_shadow.clone(true));
|
polys.tool_shadow = POLY.flatten(slice.tool_shadow.clone(true));
|
||||||
depthData.push(polys);
|
depthData.push(polys);
|
||||||
|
|
@ -706,18 +720,18 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
||||||
|
|
||||||
if (depthFirst) {
|
if (depthFirst) {
|
||||||
for (let i=0; i<depthData.length; i++) {
|
for (let i=0; i<depthData.length; i++) {
|
||||||
descend(depthData.slice(i));
|
descend(depthData.slice(i), undefined, outline);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function descend(stack, inside) {
|
function descend(stack, inside, outline) {
|
||||||
if (stack.length === 0) return;
|
if (stack.length === 0) return;
|
||||||
let tops = stack[0];
|
let tops = stack[0];
|
||||||
let flat = tops.filter(poly => !poly.marked);
|
let flat = (outline ? POLY.flatten(tops) : tops).filter(poly => !poly.marked);
|
||||||
if (flat.length === 0) return;
|
if (flat.length === 0) return;
|
||||||
if (inside) {
|
if (inside) {
|
||||||
flat = flat.filter(p => p.isNested(inside));
|
flat = flat.filter(p => p.isInside(inside));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
|
|
@ -736,7 +750,13 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
||||||
polyEmit(poly, CLOSEST_TO_PP, engage);
|
polyEmit(poly, CLOSEST_TO_PP, engage);
|
||||||
engage = false;
|
engage = false;
|
||||||
}
|
}
|
||||||
descend(stack.slice(1), poly);
|
if (outline) {
|
||||||
|
output.forEach(poly => {
|
||||||
|
descend(stack.slice(1), poly, outline);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
descend(stack.slice(1), poly, outline);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -826,20 +846,31 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
||||||
|
|
||||||
// calculate ease down for poly path output
|
// calculate ease down for poly path output
|
||||||
if (startPoint.z > point0.z) {
|
if (startPoint.z > point0.z) {
|
||||||
let easeFeed = plungeRate + ((feedRate - plungeRate) * easeThrottle);
|
let easeMax = feedRate * camFullEngage;
|
||||||
|
let easeLerp = plungeRate + ((feedRate - plungeRate) * easeThrottle);
|
||||||
|
let easeFeed = Math.min(easeLerp, easeMax);
|
||||||
let zat = startPoint.z;
|
let zat = startPoint.z;
|
||||||
let lp;
|
let len = points.length;
|
||||||
for (let i=0; ; i++) {
|
let lp, lz = Infinity;
|
||||||
let ii = i % points.length;
|
// hard cap on number of repeats to catch bad geometry
|
||||||
|
for (let i=0; i<len*50 ; i++) {
|
||||||
|
let ii = i % len;
|
||||||
let pt = points[ii];
|
let pt = points[ii];
|
||||||
if (zat <= pt.z) {
|
if (zat <= pt.z) {
|
||||||
// rotate points to start at end of ease
|
// rotate points to start at end of ease
|
||||||
points = [...points.slice(i), ...points.slice(0,i)];
|
points = [...points.slice(ii), ...points.slice(0,ii)];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (i > 0) {
|
if (i > 0) {
|
||||||
let dd = lp.distTo2D(pt);
|
let dd = lp.distTo2D(pt);
|
||||||
zat = Math.max(pt.z, zat - (dd * easeDzPerMm));
|
zat = Math.max(pt.z, zat - (dd * easeDzPerMm));
|
||||||
|
if (zat > lz) {
|
||||||
|
// rotate points to start at end of ease
|
||||||
|
// also should never get here unless bad geometry
|
||||||
|
points = [...points.slice(ii), ...points.slice(0,ii)];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
lz = zat;
|
||||||
}
|
}
|
||||||
lp = pt.clone().setZ(Math.max(pt.z, zat));
|
lp = pt.clone().setZ(Math.max(pt.z, zat));
|
||||||
camOut(lp, 1, { feed: easeFeed });
|
camOut(lp, 1, { feed: easeFeed });
|
||||||
|
|
@ -950,7 +981,7 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
||||||
// console.log('coming from another widget', { printPoint });
|
// console.log('coming from another widget', { printPoint });
|
||||||
} else if (center) {
|
} else if (center) {
|
||||||
// we're the first widget output. offset is center
|
// we're the first widget output. offset is center
|
||||||
printPoint = origin.clone().move({ x: center.x, y: center.y });
|
printPoint = origin.clone().move({ x: center.x, y: center.y, z: 0 });
|
||||||
// console.log('first widget output', { printPoint });
|
// console.log('first widget output', { printPoint });
|
||||||
} else {
|
} else {
|
||||||
console.log({ missing_center_using_origin: origin });
|
console.log({ missing_center_using_origin: origin });
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,7 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
|
||||||
bottom_stock, bottom_part, bottom_gap, bottom_z,
|
bottom_stock, bottom_part, bottom_gap, bottom_z,
|
||||||
}, 3);
|
}, 3);
|
||||||
|
|
||||||
// console.log({ bounds, stock, track, workarea });
|
// console.log({ bounds, stock, track, workarea, camZBottom });
|
||||||
|
|
||||||
return structuredClone(workarea);
|
return structuredClone(workarea);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -51,18 +51,20 @@ export class Topo {
|
||||||
stepsY = Math.ceil(boundsY / resolution),
|
stepsY = Math.ceil(boundsY / resolution),
|
||||||
widtopo = widget.topo,
|
widtopo = widget.topo,
|
||||||
topoCache = widtopo
|
topoCache = widtopo
|
||||||
&& widtopo.resolution === resolution
|
&& widtopo.tolerance === tolerance
|
||||||
&& widtopo.diameter === toolDiameter
|
&& widtopo.diameter === toolDiameter
|
||||||
? widtopo : undefined,
|
? widtopo : undefined,
|
||||||
topo = widget.topo = topoCache || {
|
topo = widget.topo = topoCache || {
|
||||||
|
axis,
|
||||||
data: new Float32Array(new SharedArrayBuffer(stepsX * stepsY * 4)),
|
data: new Float32Array(new SharedArrayBuffer(stepsX * stepsY * 4)),
|
||||||
stepsX: stepsX,
|
stepsX,
|
||||||
stepsY: stepsY,
|
stepsY,
|
||||||
bounds: bounds,
|
bounds,
|
||||||
diameter: toolDiameter,
|
diameter: toolDiameter,
|
||||||
resolution: resolution,
|
resolution,
|
||||||
|
tolerance,
|
||||||
profile: toolOffset,
|
profile: toolOffset,
|
||||||
widget: widget,
|
widget,
|
||||||
raster: true,
|
raster: true,
|
||||||
slices: null
|
slices: null
|
||||||
},
|
},
|
||||||
|
|
@ -120,14 +122,15 @@ export class Topo {
|
||||||
}
|
}
|
||||||
let toolData = { positions: toolPos, bounds: toolBounds };
|
let toolData = { positions: toolPos, bounds: toolBounds };
|
||||||
|
|
||||||
const vertices = widget.getGeoVertices({ unroll: true, translate: true });
|
let vertices = widget.getGeoVertices({ unroll: true, translate: true });
|
||||||
const wbounds = widget.getBoundingBox();
|
let wbounds = widget.getBoundingBox();
|
||||||
if (!inside) {
|
if (!inside) {
|
||||||
wbounds.expandByVector({ x: toolDiameter/2 + resolution, y: toolDiameter/2 + resolution, z: 0 });
|
wbounds.expandByVector({ x: toolDiameter/2 + resolution, y: toolDiameter/2 + resolution, z: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// swap XY vertices (unswap later after polylines generated)
|
// swap XY vertices (unswap later after polylines generated)
|
||||||
if (contourY) {
|
if (contourY) {
|
||||||
|
vertices = vertices.slice();
|
||||||
for (let i=0; i<vertices.length; i+= 3) {
|
for (let i=0; i<vertices.length; i+= 3) {
|
||||||
let tmp = vertices[i+1];
|
let tmp = vertices[i+1];
|
||||||
vertices[i+1] = vertices[i+0];
|
vertices[i+1] = vertices[i+0];
|
||||||
|
|
@ -796,7 +799,7 @@ export class Trace {
|
||||||
ok++;
|
ok++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ok === clips.length;
|
return ok > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -121,12 +121,13 @@ export function menu() {
|
||||||
separator: newBlank({ class:"set-sep", driven }),
|
separator: newBlank({ class:"set-sep", driven }),
|
||||||
sliceSupportAngle: newInput(LANG.sp_angl_s, {title:LANG.sp_angl_l, convert:toFloat, bound:bound(0.0,90.0)}),
|
sliceSupportAngle: newInput(LANG.sp_angl_s, {title:LANG.sp_angl_l, convert:toFloat, bound:bound(0.0,90.0)}),
|
||||||
sliceSupportDensity: newInput(LANG.sp_dens_s, {title:LANG.sp_dens_l, convert:toFloat, bound:bound(0.0,1.0)}),
|
sliceSupportDensity: newInput(LANG.sp_dens_s, {title:LANG.sp_dens_l, convert:toFloat, bound:bound(0.0,1.0)}),
|
||||||
sliceSupportGap: newInput(LANG.sp_gaps_s, {title:LANG.sp_gaps_l, convert:toInt, bound:bound(0,5)}),
|
|
||||||
sliceSupportOffset: newInput(LANG.sp_offs_s, {title:LANG.sp_offs_l, convert:toFloat, bound:bound(0.0,200.0)}),
|
sliceSupportOffset: newInput(LANG.sp_offs_s, {title:LANG.sp_offs_l, convert:toFloat, bound:bound(0.0,200.0)}),
|
||||||
sliceSupportExtra: newInput(LANG.sp_xpnd_s, {title:LANG.sp_xpnd_l, convert:toFloat, bound:bound(0.0,10.0)}),
|
sliceSupportExtra: newInput(LANG.sp_xpnd_s, {title:LANG.sp_xpnd_l, convert:toFloat, bound:bound(0.0,10.0)}),
|
||||||
sliceSupportSpan: newInput(LANG.sp_span_s, {title:LANG.sp_span_l, convert:toFloat, bound:bound(0.0,200.0), show:() => ui.sliceSupportEnable.checked }),
|
// sliceSupportSpan: newInput(LANG.sp_span_s, {title:LANG.sp_span_l, convert:toFloat, bound:bound(0.0,200.0), show:() => ui.sliceSupportEnable.checked }),
|
||||||
separator: newBlank({ class:"set-sep", driven }),
|
separator: newBlank({ class:"set-sep", driven }),
|
||||||
sliceSupportOutline: newBoolean(LANG.sp_outl_s, onBooleanClick, {title:LANG.sp_outl_l, xshow: () => !isTree() }),
|
sliceSupportOutline: newBoolean(LANG.sp_outl_s, onBooleanClick, {title:LANG.sp_outl_l, xshow: () => !isTree() }),
|
||||||
|
sliceSupportGap: newBoolean(LANG.sp_gaps_s, onBooleanClick, {title:LANG.sp_gaps_l }),
|
||||||
|
sliceSupportTree: newBoolean(LANG.sp_tree_s, onBooleanClick, {title:LANG.sp_tree_l }),
|
||||||
separator: newBlank({ class:"set-sep", driven, show:manualSupport }),
|
separator: newBlank({ class:"set-sep", driven, show:manualSupport }),
|
||||||
sliceSupportManual: newRow([
|
sliceSupportManual: newRow([
|
||||||
(ui.ssmAdd = newButton(undefined, onButtonClick, {icon:'<i class="fas fa-plus"></i>'})),
|
(ui.ssmAdd = newButton(undefined, onButtonClick, {icon:'<i class="fas fa-plus"></i>'})),
|
||||||
|
|
|
||||||
|
|
@ -380,6 +380,8 @@ function supportDone() {
|
||||||
// Use popVisualState to restore original material
|
// Use popVisualState to restore original material
|
||||||
w.popVisualState('paint');
|
w.popVisualState('paint');
|
||||||
});
|
});
|
||||||
|
api.conf.save();
|
||||||
|
api.space.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
// manual supports clear
|
// manual supports clear
|
||||||
|
|
|
||||||
|
|
@ -577,10 +577,12 @@ export function fdm_export(print, online, ondone, ondebug) {
|
||||||
lastFanSpeed = fanSpeed;
|
lastFanSpeed = fanSpeed;
|
||||||
}
|
}
|
||||||
if (bedTemp !== lastBedTemp) {
|
if (bedTemp !== lastBedTemp) {
|
||||||
|
// console.log({ bed_temp_change_to: bedTemp, from: lastBedTemp });
|
||||||
append(`M140 S${bedTemp} T0`);
|
append(`M140 S${bedTemp} T0`);
|
||||||
lastBedTemp = bedTemp;
|
lastBedTemp = bedTemp;
|
||||||
}
|
}
|
||||||
if (nozzleTemp !== lastNozzleTemp) {
|
if (nozzleTemp !== lastNozzleTemp) {
|
||||||
|
// console.log({ temp_change_to: nozzleTemp, from: lastNozzleTemp });
|
||||||
if (t0) append(`M104 S${nozzleTemp} T0`);
|
if (t0) append(`M104 S${nozzleTemp} T0`);
|
||||||
if (t1) append(`M104 S${nozzleTemp} T1`);
|
if (t1) append(`M104 S${nozzleTemp} T1`);
|
||||||
if (!(t0 || t1)) append(`M104 S${nozzleTemp} T${tool}`);
|
if (!(t0 || t1)) append(`M104 S${nozzleTemp} T${tool}`);
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,29 @@ function vopt(opt, ctx) {
|
||||||
return opt;
|
return opt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* return percentage values broken into ranges
|
||||||
|
*
|
||||||
|
* @param {number} plo 0.0-1.0 percentage value
|
||||||
|
* @param {number} phi 0.0-1.0 high percentage value
|
||||||
|
* @param {Array} pcts [{ lo, hi }, ...]
|
||||||
|
*/
|
||||||
|
function divide(plo, phi, pcts) {
|
||||||
|
let sum = 0;
|
||||||
|
let lo = plo;
|
||||||
|
let rval = pcts.map(pct => {
|
||||||
|
sum += pct;
|
||||||
|
let diff = (phi - plo) * pct;
|
||||||
|
let rval = { lo, hi: lo + diff };
|
||||||
|
lo += diff;
|
||||||
|
return rval;
|
||||||
|
});
|
||||||
|
if (Math.abs(1 - sum) > 0.001) {
|
||||||
|
console.log('SUM FAIL', { rval, sum });
|
||||||
|
}
|
||||||
|
return rval;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DRIVER SLICE CONTRACT
|
* DRIVER SLICE CONTRACT
|
||||||
*
|
*
|
||||||
|
|
@ -295,6 +318,10 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isConcurrent) {
|
||||||
|
minions.setPoints(points);
|
||||||
|
}
|
||||||
|
|
||||||
// create Slice objects for specified list of Z heights
|
// create Slice objects for specified list of Z heights
|
||||||
// zGen() produces the list (or empty for slicer auto-detected)
|
// zGen() produces the list (or empty for slicer auto-detected)
|
||||||
slice(points, {
|
slice(points, {
|
||||||
|
|
@ -303,6 +330,7 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
||||||
xray: process.xray,
|
xray: process.xray,
|
||||||
zMin: bounds.min.z,
|
zMin: bounds.min.z,
|
||||||
zMax: bounds.max.z - zCut,
|
zMax: bounds.max.z - zCut,
|
||||||
|
bucketMin: minions.concurrent * 5,
|
||||||
union: controller.healMesh,
|
union: controller.healMesh,
|
||||||
indices: process.indices || process.xray,
|
indices: process.indices || process.xray,
|
||||||
useAssembly,
|
useAssembly,
|
||||||
|
|
@ -328,6 +356,9 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
||||||
})
|
})
|
||||||
.then(decodeSlices)
|
.then(decodeSlices)
|
||||||
.then(processSlices)
|
.then(processSlices)
|
||||||
|
.then(() => {
|
||||||
|
minions.setPoints([]);
|
||||||
|
})
|
||||||
.then(ondone);
|
.then(ondone);
|
||||||
|
|
||||||
// z index generator (bottom up)
|
// z index generator (bottom up)
|
||||||
|
|
@ -441,17 +472,18 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
||||||
}).filter(s => s);
|
}).filter(s => s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// slicing is the first 50% of the update "time"
|
||||||
|
function trackupdate(pct, from, to, msg) {
|
||||||
|
// console.log(from.round(2), to.round(2), msg);
|
||||||
|
onupdate(0.5 + (from + (pct * (to - from))) * 0.5, msg);
|
||||||
|
}
|
||||||
|
|
||||||
// calculate % complete and call onupdate()
|
// calculate % complete and call onupdate()
|
||||||
function doupdate(index, from, to, msg) {
|
function doupdate(index, from, to, msg) {
|
||||||
trackupdate(index / slices.length, from, to, msg);
|
trackupdate(index / slices.length, from, to, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// slicing is the first 50% of the update "time"
|
// for each slice, perform a function and call doupdate()
|
||||||
function trackupdate(pct, from, to, msg) {
|
|
||||||
onupdate(0.5 + (from + (pct * (to - from))) * 0.5, msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
// for each slice, performe a function and call doupdate()
|
|
||||||
function forSlices(from, to, fn, msg) {
|
function forSlices(from, to, fn, msg) {
|
||||||
slices.forEach(slice => {
|
slices.forEach(slice => {
|
||||||
fn(slice);
|
fn(slice);
|
||||||
|
|
@ -462,11 +494,12 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
||||||
/**
|
/**
|
||||||
* Process automatic and manual shadow-based support generation
|
* Process automatic and manual shadow-based support generation
|
||||||
*/
|
*/
|
||||||
async function processSupports() {
|
async function processSupports(plo, phi) {
|
||||||
if (process.sliceSupportType === 'disabled') {
|
if (process.sliceSupportType === 'disabled') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let div = divide(plo, phi, [ 0.5, 0.5 ]);
|
||||||
let stack = slices.slice();
|
let stack = slices.slice();
|
||||||
let indices = stack.map(s => s.z);
|
let indices = stack.map(s => s.z);
|
||||||
let zAngNorm = Math.sin(process.sliceSupportAngle * Math.PI / 180);
|
let zAngNorm = Math.sin(process.sliceSupportAngle * Math.PI / 180);
|
||||||
|
|
@ -476,11 +509,32 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
||||||
// since that is done later and clipped to slice.clips
|
// since that is done later and clipped to slice.clips
|
||||||
stack.sort((a,b) => a.z - b.z);
|
stack.sort((a,b) => a.z - b.z);
|
||||||
|
|
||||||
|
// automatic supports
|
||||||
|
if (!manual) {
|
||||||
|
// find where shadow areas begin (async)
|
||||||
|
await widget.computeShadowStack(indices, progress => {
|
||||||
|
trackupdate(progress, div[0].lo, div[0].hi, "shadow");
|
||||||
|
}, zAngNorm, sliceHeight);
|
||||||
|
|
||||||
|
// assign to slices (sync)
|
||||||
|
for (let slice of stack) {
|
||||||
|
slice.shadow = await widget.shadowAt(slice.z, true);
|
||||||
|
if (!(slice.up && slice.shadow?.length)) continue;
|
||||||
|
// trim shadow to part overhangs
|
||||||
|
let top = slice.up.topPolys();
|
||||||
|
let bot = slice.topPolys();
|
||||||
|
let bridge = [];
|
||||||
|
POLY.subtract(top, bot, bridge, undefined, slice.z, 0, { wasm: true });
|
||||||
|
slice.shadow = POLY.trimTo(slice.shadow, bridge, { minArea: 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// process manual supports if they exist
|
// process manual supports if they exist
|
||||||
let { paint } = widget.anno;
|
let { paint } = widget.anno;
|
||||||
let { belt } = widget;
|
let { belt } = widget;
|
||||||
|
|
||||||
// apply belt transformations, if needed
|
// apply belt transformations, if needed
|
||||||
if (belt && paint) {
|
if (manual && belt && paint?.length) {
|
||||||
let { anchor, angle, dy, slope } = belt;
|
let { anchor, angle, dy, slope } = belt;
|
||||||
// make a copy we can modify
|
// make a copy we can modify
|
||||||
paint = structuredClone(paint);
|
paint = structuredClone(paint);
|
||||||
|
|
@ -496,10 +550,14 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert paint points to circles on matching slices
|
// convert paint points to circles on matching slices
|
||||||
if (manual && paint?.length) {
|
if (manual && paint?.length) {
|
||||||
let hpi = Math.PI/2;
|
let hpi = Math.PI/2;
|
||||||
for (let slice of stack) {
|
for (let slice of stack) {
|
||||||
|
if (!slice.up) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let polys = [];
|
let polys = [];
|
||||||
for (let rec of paint) {
|
for (let rec of paint) {
|
||||||
let { point, radius } = rec;
|
let { point, radius } = rec;
|
||||||
|
|
@ -510,31 +568,34 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
||||||
polys.push(newPolygon().centerCircle(point, radius, 10));
|
polys.push(newPolygon().centerCircle(point, radius, 10));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
slice.shadow = POLY.union(polys, 0, true);
|
// trim polys to part overhangs
|
||||||
}
|
let top = slice.up.topPolys();
|
||||||
}
|
let bot = slice.topPolys();
|
||||||
|
let bridge = [];
|
||||||
// create automatic shadows supports when on manual paint
|
let propose = POLY.setZ(POLY.union(polys, 0, true), slice.z);
|
||||||
if (!manual) {
|
POLY.subtract(top, bot, bridge, undefined, slice.z, 0, { wasm: true });
|
||||||
await widget.computeShadowStack(indices, progress => {
|
propose = POLY.trimTo(propose, bridge, { minArea: 0 });
|
||||||
trackupdate(progress, 0.05, 0.10, "shadow");
|
slice.shadow = propose;
|
||||||
}, zAngNorm);
|
// if (devel) slice.output().setLayer("over", 0x8844aa).addPolys(bridge);
|
||||||
|
|
||||||
for (let slice of stack) {
|
|
||||||
slice.shadow = await widget.shadowAt(slice.z, true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. accumulate / union shadow coverage top down
|
// 1. accumulate / union shadow coverage top down
|
||||||
// 2. trim to area outside slice.clips
|
// 2. trim to area outside slice.clips
|
||||||
let minArea = lineWidth;
|
let minArea = lineWidth * lineWidth;
|
||||||
let shadowSum;
|
let shadowSum;
|
||||||
let length = stack.length;
|
let length = stack.length;
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
|
// convert shadows to trees, when specified
|
||||||
|
if (process.sliceSupportTree) {
|
||||||
|
// console.log('TREE OUTPUT');
|
||||||
|
}
|
||||||
|
|
||||||
// perform accumulation top down
|
// perform accumulation top down
|
||||||
for (let slice of stack.reverse()) {
|
for (let slice of stack.slice().reverse()) {
|
||||||
let shadow = slice.shadow ?? [];
|
let shadow = slice.shadow ?? [];
|
||||||
|
if (devel) slice.output().setLayer("shadow", 0xff0000).addPolys(shadow);
|
||||||
if (process.sliceSupportExtra) {
|
if (process.sliceSupportExtra) {
|
||||||
shadow = POLY.offset(shadow, process.sliceSupportExtra);
|
shadow = POLY.offset(shadow, process.sliceSupportExtra);
|
||||||
}
|
}
|
||||||
|
|
@ -542,14 +603,10 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
||||||
if (shadowSum) {
|
if (shadowSum) {
|
||||||
shadow = POLY.union([...shadow, ...shadowSum], minArea, true);
|
shadow = POLY.union([...shadow, ...shadowSum], minArea, true);
|
||||||
}
|
}
|
||||||
// subtract slice.clips areas (widget boundaries) from shadow projection
|
// subtract slice top areas (widget boundaries) from shadow projection
|
||||||
if (true) {
|
if (true) {
|
||||||
let rem = [];
|
let rem = [];
|
||||||
let clips = [
|
let clips = [ slice.topPolys() ].filter(v => v).flat();
|
||||||
slice.up?.clips,
|
|
||||||
slice.clips,
|
|
||||||
slice.down?.clips
|
|
||||||
].filter(v => v).flat();
|
|
||||||
clips = POLY.union(clips, minArea, true);
|
clips = POLY.union(clips, minArea, true);
|
||||||
POLY.subtract(shadow, clips, rem, null, slice.z, minArea, { wasm: false });
|
POLY.subtract(shadow, clips, rem, null, slice.z, minArea, { wasm: false });
|
||||||
shadow = rem;
|
shadow = rem;
|
||||||
|
|
@ -570,11 +627,47 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
||||||
.centerRectangle(newPoint(0, 0, slice.z), boundsx, boundsy)
|
.centerRectangle(newPoint(0, 0, slice.z), boundsx, boundsy)
|
||||||
.move({ x: 0, y: -boundsy / 2 + skewy, z: 0 });
|
.move({ x: 0, y: -boundsy / 2 + skewy, z: 0 });
|
||||||
shadow = POLY.trimTo(shadow, [ clip ]);
|
shadow = POLY.trimTo(shadow, [ clip ]);
|
||||||
if (devel) slice.output().setLayer("belt clip", 0xffff00).addPolys([ clip ]);
|
// if (devel) slice.output().setLayer("belt clip", 0xffff00).addPolys([ clip ]);
|
||||||
}
|
}
|
||||||
slice.supports = shadow;
|
slice.supports = shadow;
|
||||||
if (devel) slice.output().setLayer("shadow", 0xff0000).addPolys(shadow);
|
// if (devel) slice.output().setLayer("shadow", 0xff0000).addPolys(shadow);
|
||||||
trackupdate((++count/length), 0.10, 0.15, "support");
|
trackupdate((++count/length), div[1].lo, div[1].hi, "support");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO layerDiff shadows to identify tops/bottoms of pillars
|
||||||
|
if (false) {
|
||||||
|
let ps = [];
|
||||||
|
for (let slice of stack.slice().reverse()) {
|
||||||
|
let { supports } = slice;
|
||||||
|
let supportsDown = slice.down ? slice.down.supports : [];
|
||||||
|
let bridges = [], flats = [];
|
||||||
|
slice.supportsDiff = { bridges, flats };
|
||||||
|
ps.push(self.kiri_worker.minions
|
||||||
|
.subtract({
|
||||||
|
a: supports, b: supportsDown,
|
||||||
|
outA: bridges, outB: flats,
|
||||||
|
area: 1, wasm: true, z: slice.z,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await Promise.all(ps);
|
||||||
|
console.log({ slices: slices.map(s => s.supportsDiff) });
|
||||||
|
}
|
||||||
|
|
||||||
|
// trim using support part offset value
|
||||||
|
let gaps = process.sliceSupportGap;
|
||||||
|
for (let slice of stack) {
|
||||||
|
let clips = [
|
||||||
|
slice.clips,
|
||||||
|
gaps ? slice.up?.clips : undefined,
|
||||||
|
gaps ? slice.down?.clips : undefined
|
||||||
|
].filter(v => v).flat();
|
||||||
|
if (clips.length) {
|
||||||
|
let rem = [];
|
||||||
|
clips = POLY.union(clips, minArea, true);
|
||||||
|
POLY.subtract(slice.supports, clips, rem, null, slice.z, minArea, { wasm: false });
|
||||||
|
slice.supports = rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -582,8 +675,8 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
||||||
* Process top and bottom layers or any other
|
* Process top and bottom layers or any other
|
||||||
* layers detected and marked for solid fill
|
* layers detected and marked for solid fill
|
||||||
*/
|
*/
|
||||||
async function processSolidLayers() {
|
async function processSolidLayers(plo, phi) {
|
||||||
forSlices(0.15, 0.2, slice => {
|
forSlices(plo, phi, slice => {
|
||||||
let range = slice.params;
|
let range = slice.params;
|
||||||
let isBottom = slice.index < bottomLayers;
|
let isBottom = slice.index < bottomLayers;
|
||||||
let isTop = topLayers && slice.index > slices.length - topLayers - 1;
|
let isTop = topLayers && slice.index > slices.length - topLayers - 1;
|
||||||
|
|
@ -604,20 +697,29 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
||||||
/**
|
/**
|
||||||
* Process layer diffs and project solid areas
|
* Process layer diffs and project solid areas
|
||||||
*/
|
*/
|
||||||
async function processLayerDiffs() {
|
async function processLayerDiffs(plo, phi) {
|
||||||
|
let div = divide(plo, phi, [ 0.9, 0.05, 0.05 ]);
|
||||||
// boolean diff layers to detect bridges and flats
|
// boolean diff layers to detect bridges and flats
|
||||||
|
let promises = [];
|
||||||
profileStart("delta");
|
profileStart("delta");
|
||||||
forSlices(0.2, 0.33, slice => {
|
forSlices(div[0].lo, div[1].hi, slice => {
|
||||||
let params = slice.params || process;
|
let params = slice.params || process;
|
||||||
let solidMinArea = params.sliceSolidMinArea;
|
let solidMinArea = params.sliceSolidMinArea;
|
||||||
let sliceMinThick = params.sliceSolidMinThick;
|
let sliceMinThick = params.sliceSolidMinThick;
|
||||||
let sliceFillGrow = params.sliceFillGrow;
|
let sliceFillGrow = params.sliceFillGrow;
|
||||||
layerDiff(slice, { area: solidMinArea, grow: sliceFillGrow, thick: sliceMinThick });
|
let p = layerDiff(slice, {
|
||||||
|
area: solidMinArea,
|
||||||
|
grow: sliceFillGrow,
|
||||||
|
thick: sliceMinThick,
|
||||||
|
async: true
|
||||||
|
});
|
||||||
|
promises.push(p);
|
||||||
}, "layer deltas");
|
}, "layer deltas");
|
||||||
|
await Promise.all(promises);
|
||||||
profileEnd();
|
profileEnd();
|
||||||
// project bridges and flats up and down into part
|
// project bridges and flats up and down into part
|
||||||
profileStart("delta-project");
|
profileStart("delta-project");
|
||||||
forSlices(0.33, 0.34, slice => {
|
forSlices(div[1].lo, div[1].hi, slice => {
|
||||||
let params = slice.params || process;
|
let params = slice.params || process;
|
||||||
topLayers = params.sliceTopLayers || 0;
|
topLayers = params.sliceTopLayers || 0;
|
||||||
bottomLayers = params.sliceBottomLayers || 0;
|
bottomLayers = params.sliceBottomLayers || 0;
|
||||||
|
|
@ -628,7 +730,7 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
||||||
profileEnd();
|
profileEnd();
|
||||||
// union solid areas
|
// union solid areas
|
||||||
profileStart("solid-union");
|
profileStart("solid-union");
|
||||||
forSlices(0.34, 0.35, slice => {
|
forSlices(div[2].lo, div[2].hi, slice => {
|
||||||
if (slice.solids) {
|
if (slice.solids) {
|
||||||
slice.solids = POLY.union(slice.solids, 0, true);
|
slice.solids = POLY.union(slice.solids, 0, true);
|
||||||
}
|
}
|
||||||
|
|
@ -639,10 +741,11 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
||||||
/**
|
/**
|
||||||
* Process solid fill patterns
|
* Process solid fill patterns
|
||||||
*/
|
*/
|
||||||
async function processSolidFills() {
|
async function processSolidFills(plo, phi) {
|
||||||
profileStart("solid-fill")
|
profileStart("solid-fill")
|
||||||
let promises = isConcurrent ? [] : undefined;
|
let promises = isConcurrent ? [] : undefined;
|
||||||
forSlices(0.35, promises ? 0.4 : 0.5, slice => {
|
let div = divide(plo, phi, promises ? [ 0.8, 0.2 ] : [ 1 ]);
|
||||||
|
forSlices(div[0].lo, div[0].hi, slice => {
|
||||||
let params = slice.params || process;
|
let params = slice.params || process;
|
||||||
let solidWidth = params.sliceFillWidth || 1;
|
let solidWidth = params.sliceFillWidth || 1;
|
||||||
let fillSpace = fillSpacing * solidWidth;
|
let fillSpace = fillSpacing * solidWidth;
|
||||||
|
|
@ -654,7 +757,7 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
||||||
slices.last().finishSolids = true
|
slices.last().finishSolids = true
|
||||||
if (promises) {
|
if (promises) {
|
||||||
await tracker(promises, (i, t) => {
|
await tracker(promises, (i, t) => {
|
||||||
trackupdate(i / t, 0.4, 0.5);
|
trackupdate(i / t, div[1].lo, div[1].hi);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
profileEnd();
|
profileEnd();
|
||||||
|
|
@ -663,10 +766,11 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
||||||
/**
|
/**
|
||||||
* Process sparse infill patterns
|
* Process sparse infill patterns
|
||||||
*/
|
*/
|
||||||
async function processSparseInfill() {
|
async function processSparseInfill(plo, phi) {
|
||||||
let lastType;
|
let lastType;
|
||||||
let promises = isConcurrent ? [] : undefined;
|
let promises = isConcurrent ? [] : undefined;
|
||||||
forSlices(0.5, promises ? 0.55 : 0.7, slice => {
|
let div = divide(plo, phi, promises ? [ 0.8, 0.2 ] : [ 1 ]);
|
||||||
|
forSlices(div[0].lo, div[0].hi, slice => {
|
||||||
let params = slice.params || process;
|
let params = slice.params || process;
|
||||||
if (!params.sliceFillSparse) {
|
if (!params.sliceFillSparse) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -689,7 +793,7 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
||||||
}, "infill");
|
}, "infill");
|
||||||
if (promises) {
|
if (promises) {
|
||||||
await tracker(promises, (i, t) => {
|
await tracker(promises, (i, t) => {
|
||||||
trackupdate(i / t, 0.55, 0.7);
|
trackupdate(i / t, div[1].lo, div[1].hi);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// filter out tiny fill points less than nozzle diameter
|
// filter out tiny fill points less than nozzle diameter
|
||||||
|
|
@ -715,10 +819,11 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
||||||
/**
|
/**
|
||||||
* Process support structure fills
|
* Process support structure fills
|
||||||
*/
|
*/
|
||||||
async function processSupportFills() {
|
async function processSupportFills(plo, phi) {
|
||||||
profileStart("support-fill");
|
profileStart("support-fill");
|
||||||
let promises = false && isConcurrent ? [] : undefined;
|
let promises = false && isConcurrent ? [] : undefined;
|
||||||
forSlices(0.8, promises ? 0.88 : 0.9, slice => {
|
let div = divide(plo, phi, promises ? [ 0.8, 0.2 ] : [ 1 ]);
|
||||||
|
forSlices(div[0].lo, div[0].hi, slice => {
|
||||||
let params = slice.params || process;
|
let params = slice.params || process;
|
||||||
let density = params.sliceSupportDensity;
|
let density = params.sliceSupportDensity;
|
||||||
layerSupportFill({
|
layerSupportFill({
|
||||||
|
|
@ -733,7 +838,7 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
||||||
}, "support fill");
|
}, "support fill");
|
||||||
if (promises) {
|
if (promises) {
|
||||||
await tracker(promises, (i, t) => {
|
await tracker(promises, (i, t) => {
|
||||||
trackupdate(i / t, 0.88, 0.9);
|
trackupdate(i / t, div[1].lo, div[1].hi);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
profileEnd();
|
profileEnd();
|
||||||
|
|
@ -788,8 +893,8 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
||||||
slices.forEach((s,i) => s.index = i);
|
slices.forEach((s,i) => s.index = i);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderSlices() {
|
async function renderSlices(plo, phi) {
|
||||||
forSlices(0.9, 1.0, slice => {
|
forSlices(plo, phi, slice => {
|
||||||
let params = slice.params || process;
|
let params = slice.params || process;
|
||||||
layerRender(slice, params, {
|
layerRender(slice, params, {
|
||||||
dark: controller.dark,
|
dark: controller.dark,
|
||||||
|
|
@ -850,7 +955,7 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// process solid layers (top/bottom)
|
// process solid layers (top/bottom)
|
||||||
await processSolidLayers();
|
await processSolidLayers(0.10, 0.20);
|
||||||
|
|
||||||
// add lead in anchor when specified in belt mode (but not for synths)
|
// add lead in anchor when specified in belt mode (but not for synths)
|
||||||
if (isBelt) {
|
if (isBelt) {
|
||||||
|
|
@ -883,10 +988,6 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// support generation using either
|
|
||||||
// enclosed shadow or manual painted supports
|
|
||||||
await processSupports();
|
|
||||||
|
|
||||||
// calculations only relevant when solid layers are used
|
// calculations only relevant when solid layers are used
|
||||||
// layer boolean diffs need to be computed to find flat areas to fill
|
// layer boolean diffs need to be computed to find flat areas to fill
|
||||||
// and overhangs that need to be supported. these are stored in flats
|
// and overhangs that need to be supported. these are stored in flats
|
||||||
|
|
@ -894,24 +995,27 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
||||||
// for "real" objects, fill the remaining voids with sparse fill
|
// for "real" objects, fill the remaining voids with sparse fill
|
||||||
// sparse layers only present when non-vase mode and sparse % > 0
|
// sparse layers only present when non-vase mode and sparse % > 0
|
||||||
if (!vaseMode) {
|
if (!vaseMode) {
|
||||||
await processLayerDiffs();
|
await processLayerDiffs(0.2, 0.4);
|
||||||
await processSolidFills();
|
// support generation using either
|
||||||
await processSparseInfill();
|
// enclosed shadow or manual painted supports
|
||||||
|
await processSupports(0.4, 0.5);
|
||||||
|
await processSolidFills(0.5, 0.6);
|
||||||
|
await processSparseInfill(0.6, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
// fill all supports (auto and manual)
|
// fill all supports (auto and manual)
|
||||||
if (supportDensity) {
|
if (supportDensity) {
|
||||||
await processSupportFills();
|
await processSupportFills(0.8, 0.84);
|
||||||
}
|
}
|
||||||
|
|
||||||
// brick/interleave mode processing
|
// brick/interleave mode processing
|
||||||
if (isBrick) {
|
if (isBrick) {
|
||||||
await processBrickMode();
|
await processBrickMode(0.84, 0.85);
|
||||||
}
|
}
|
||||||
|
|
||||||
// render if not explicitly disabled
|
// render if not explicitly disabled
|
||||||
if (render) {
|
if (render) {
|
||||||
await renderSlices();
|
await renderSlices(0.85, 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isBelt) {
|
if (isBelt) {
|
||||||
|
|
@ -1086,6 +1190,7 @@ export function slicePost(settings, onupdate) {
|
||||||
// assign grid_id which can be embedded in gcode and
|
// assign grid_id which can be embedded in gcode and
|
||||||
// used by the controller to cancel objects during print
|
// used by the controller to cancel objects during print
|
||||||
let { bounds } = settings;
|
let { bounds } = settings;
|
||||||
|
if (!bounds) return;
|
||||||
for (let widget of widgets) {
|
for (let widget of widgets) {
|
||||||
let { pos, box } = widget.track;
|
let { pos, box } = widget.track;
|
||||||
// calculate top/left coordinate for widget
|
// calculate top/left coordinate for widget
|
||||||
|
|
@ -1375,19 +1480,36 @@ export function layerDiff(slice, options = {}) {
|
||||||
let newBridges = [];
|
let newBridges = [];
|
||||||
let newFlats = [];
|
let newFlats = [];
|
||||||
|
|
||||||
POLY.subtract(topInner, downInner, newBridges, newFlats, slice.z, area, {
|
if (options.async) {
|
||||||
wasm: true
|
return self.kiri_worker.minions
|
||||||
});
|
.subtract({
|
||||||
|
a: topInner, b: downInner,
|
||||||
|
outA: newBridges, outB: newFlats,
|
||||||
|
area, wasm: true, z: slice.z,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
layerDiffDone({ slice, bridges, flats, newBridges, newFlats, options });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
POLY.subtract(topInner, downInner, newBridges, newFlats, slice.z, area, {
|
||||||
|
wasm: true
|
||||||
|
});
|
||||||
|
layerDiffDone({ slice, bridges, flats, newBridges, newFlats, options });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function layerDiffDone({ slice, bridges, flats, newBridges, newFlats, options }) {
|
||||||
|
const { sla, grow, area, thick } = options;
|
||||||
|
|
||||||
// console.log(slice.z, { newBridges, newFlats });
|
// console.log(slice.z, { newBridges, newFlats });
|
||||||
newBridges = newBridges.filter(p => p.areaDeep() >= area && p.thickness(true) >= thick);
|
newBridges = newBridges.filter(p => p.areaDeep() >= area && p.thickness(true) >= thick);
|
||||||
newFlats = newFlats.filter(p => p.areaDeep() >= area && p.thickness(true) >= thick);
|
newFlats = newFlats.filter(p => p.areaDeep() >= area && p.thickness(true) >= thick);
|
||||||
|
|
||||||
if (grow > 0 && newBridges.length) {
|
if (grow > 0 && newBridges.length) {
|
||||||
newBridges = POLY.offset(newBridges, grow);
|
newBridges = POLY.offset(newBridges, grow, { z: slice.z });
|
||||||
}
|
}
|
||||||
if (grow > 0 && newFlats.length) {
|
if (grow > 0 && newFlats.length) {
|
||||||
newFlats = POLY.offset(newFlats, grow);
|
newFlats = POLY.offset(newFlats, grow, { z: slice.z });
|
||||||
}
|
}
|
||||||
|
|
||||||
bridges.appendAll(newBridges);
|
bridges.appendAll(newBridges);
|
||||||
|
|
|
||||||
|
|
@ -19,13 +19,15 @@ class Engine {
|
||||||
filter: { FDM: "internal" },
|
filter: { FDM: "internal" },
|
||||||
device: conf.defaults.fdm.d, // device profile
|
device: conf.defaults.fdm.d, // device profile
|
||||||
process: conf.defaults.fdm.p, // slicing settings
|
process: conf.defaults.fdm.p, // slicing settings
|
||||||
widget: { [this.widget.id]: {} }
|
widget: { [this.widget.id]: {} },
|
||||||
|
time: Date.now()
|
||||||
};
|
};
|
||||||
this.listener = () => { };
|
this.listener = () => { };
|
||||||
try {
|
try {
|
||||||
client.setWorkPath(workURL);
|
client.setWorkPath(workURL);
|
||||||
client.setPoolPath(poolURL);
|
client.setPoolPath(poolURL);
|
||||||
client.restart();
|
client.restart();
|
||||||
|
client.pool.start();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log({ error });
|
console.log({ error });
|
||||||
}
|
}
|
||||||
|
|
@ -37,6 +39,7 @@ class Engine {
|
||||||
new load.STL().load(url, vertices => {
|
new load.STL().load(url, vertices => {
|
||||||
this.listener({ loaded: url, vertices });
|
this.listener({ loaded: url, vertices });
|
||||||
this.widget.loadVertices(vertices).center();
|
this.widget.loadVertices(vertices).center();
|
||||||
|
this.setTopOffset(0);
|
||||||
accept(this);
|
accept(this);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -49,6 +52,10 @@ class Engine {
|
||||||
api.platform.clear();
|
api.platform.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
workspace() {
|
||||||
|
return api.settings.export({ engine: this.settings });
|
||||||
|
}
|
||||||
|
|
||||||
parse(data) {
|
parse(data) {
|
||||||
return new Promise((accept, reject) => {
|
return new Promise((accept, reject) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -63,11 +70,8 @@ class Engine {
|
||||||
}
|
}
|
||||||
|
|
||||||
setThreading(bool) {
|
setThreading(bool) {
|
||||||
if (bool) {
|
console.log('setThreading() deprecated');
|
||||||
client.pool.start();
|
return this;
|
||||||
} else {
|
|
||||||
client.pool.stop();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setListener(listener) {
|
setListener(listener) {
|
||||||
|
|
@ -86,7 +90,15 @@ class Engine {
|
||||||
* @returns {Engine} this
|
* @returns {Engine} this
|
||||||
*/
|
*/
|
||||||
setMode(mode) {
|
setMode(mode) {
|
||||||
this.settings.mode = mode;
|
let lmode = mode.toLowerCase();
|
||||||
|
Object.assign(this.settings, {
|
||||||
|
mode: mode,
|
||||||
|
controller: {},
|
||||||
|
render: false,
|
||||||
|
filter: { [mode]: "internal" },
|
||||||
|
device: conf.defaults[lmode].d,
|
||||||
|
process: conf.defaults[lmode].p,
|
||||||
|
});
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,17 +135,24 @@ class Engine {
|
||||||
process.camStockX = stock.x;
|
process.camStockX = stock.x;
|
||||||
process.camStockY = stock.y;
|
process.camStockY = stock.y;
|
||||||
process.camStockZ = stock.z;
|
process.camStockZ = stock.z;
|
||||||
if (this.origin) settings.stock.center = origin;
|
settings.stock.center = {
|
||||||
|
x: stock.x / 2,
|
||||||
|
y: stock.y / 2,
|
||||||
|
z: stock.z / 2
|
||||||
|
};
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTopOffset(offset = 0) {
|
setTopOffset(offset = 0) {
|
||||||
this.topOffset = offset;
|
this.topOffset = offset;
|
||||||
|
let wbb = this.widget.getBoundingBox();
|
||||||
|
this.widget.setTopZ(wbb.max.z - offset);
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
setOrigin(x, y, z) {
|
setOrigin(x, y, z) {
|
||||||
this.origin = { x, y, z };
|
this.origin = { x, y, z };
|
||||||
if (this.settings.stock) this.settings.stock.center = { x, y, z };
|
this.settings.origin = this.origin;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,11 +177,11 @@ class Engine {
|
||||||
}
|
}
|
||||||
|
|
||||||
slice() {
|
slice() {
|
||||||
this.widget.setTopZ((this.settings?.stock?.z || 0) - (this.topOffset || 0));
|
|
||||||
return new Promise((accept, reject) => {
|
return new Promise((accept, reject) => {
|
||||||
client.clear();
|
client.clear();
|
||||||
client.sync([this.widget]);
|
client.sync([this.widget]);
|
||||||
client.rotate(this.settings);
|
client.rotate(this.settings);
|
||||||
|
client.slicePre(this.settings, () => {});
|
||||||
client.slice(this.settings, this.widget, msg => {
|
client.slice(this.settings, this.widget, msg => {
|
||||||
this.listener({ slice: msg });
|
this.listener({ slice: msg });
|
||||||
if (msg.error) {
|
if (msg.error) {
|
||||||
|
|
@ -170,6 +189,7 @@ class Engine {
|
||||||
}
|
}
|
||||||
if (msg.done) {
|
if (msg.done) {
|
||||||
accept(this);
|
accept(this);
|
||||||
|
client.slicePost(this.settings, () => {});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,20 @@ const funcs = self.minion = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
subtract(data, seq) {
|
||||||
|
let { arg, opt } = data;
|
||||||
|
let { area, wasm, z } = opt;
|
||||||
|
let a = codec.decode(arg.a);
|
||||||
|
let b = codec.decode(arg.b);
|
||||||
|
let outA = [], outB = [];
|
||||||
|
POLY.subtract(a, b, outA, outB, z, area, { wasm });
|
||||||
|
reply({
|
||||||
|
seq,
|
||||||
|
outA: codec.encode(outA),
|
||||||
|
outB: codec.encode(outB)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
union(data, seq) {
|
union(data, seq) {
|
||||||
if (!(data.polys && data.polys.length)) {
|
if (!(data.polys && data.polys.length)) {
|
||||||
reply({ seq, union: codec.encode([]) });
|
reply({ seq, union: codec.encode([]) });
|
||||||
|
|
@ -140,14 +154,11 @@ const funcs = self.minion = {
|
||||||
|
|
||||||
sliceZ(data, seq) {
|
sliceZ(data, seq) {
|
||||||
debug('minion.sliceZ', { data, seq });
|
debug('minion.sliceZ', { data, seq });
|
||||||
let { z, points, options } = data;
|
let { z, options } = data;
|
||||||
let i = 0, p = 0, realp = new Array(points.length / 3);
|
let { points } = cache;
|
||||||
while (i < points.length) {
|
|
||||||
realp[p++] = newPoint(points[i++], points[i++], points[i++]).round(3);
|
|
||||||
}
|
|
||||||
let state = { zero: [] };
|
let state = { zero: [] };
|
||||||
let output = [];
|
let output = [];
|
||||||
sliceZ(z, realp, {
|
sliceZ(z, points, {
|
||||||
...options,
|
...options,
|
||||||
each(out) { output.push(out) }
|
each(out) { output.push(out) }
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
|
|
@ -159,9 +170,17 @@ const funcs = self.minion = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setPoints(data, seq) {
|
||||||
|
let { points } = data;
|
||||||
|
let i = 0, p = 0, realp = new Array(points.length / 3);
|
||||||
|
while (i < points.length) {
|
||||||
|
realp[p++] = newPoint(points[i++], points[i++], points[i++]).round(3);
|
||||||
|
}
|
||||||
|
cache.points = realp;
|
||||||
|
},
|
||||||
|
|
||||||
putCache(msg) {
|
putCache(msg) {
|
||||||
const { key, data } = msg;
|
const { key, data } = msg;
|
||||||
// log({ minion_putCache: key, data });
|
|
||||||
if (data) {
|
if (data) {
|
||||||
cache[key] = data;
|
cache[key] = data;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ let drivers = {
|
||||||
WJET
|
WJET
|
||||||
},
|
},
|
||||||
ccvalue = self.navigator ? self.navigator.hardwareConcurrency || 0 : 0,
|
ccvalue = self.navigator ? self.navigator.hardwareConcurrency || 0 : 0,
|
||||||
concurrent = Math.min(4, self.Worker && ccvalue > 3 ? ccvalue - 1 : 0),
|
concurrent = Math.round(Math.max(4, self.Worker && ccvalue > 3 ? ccvalue * 0.75 : 0)),
|
||||||
current = {
|
current = {
|
||||||
print: null,
|
print: null,
|
||||||
snap: null,
|
snap: null,
|
||||||
|
|
@ -101,6 +101,9 @@ function minhandler(msg) {
|
||||||
|
|
||||||
// for concurrent operations
|
// for concurrent operations
|
||||||
const minwork = {
|
const minwork = {
|
||||||
|
|
||||||
|
// core functions
|
||||||
|
|
||||||
get concurrent() {
|
get concurrent() {
|
||||||
return concurrent
|
return concurrent
|
||||||
},
|
},
|
||||||
|
|
@ -137,6 +140,76 @@ const minwork = {
|
||||||
minions.length = 0;
|
minions.length = 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
queue(work, ondone, direct) {
|
||||||
|
minionq.push({work, ondone, direct});
|
||||||
|
minwork.kick();
|
||||||
|
},
|
||||||
|
|
||||||
|
queueAsync(work, direct) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
minwork.queue(work, resolve, direct);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
kick() {
|
||||||
|
if (minions.length && minionq.length) {
|
||||||
|
let qrec = minionq.shift();
|
||||||
|
let minion = minions.shift();
|
||||||
|
let seq = miniseq++;
|
||||||
|
qrec.work.seq = seq;
|
||||||
|
minifns[seq] = (data) => {
|
||||||
|
qrec.ondone(data);
|
||||||
|
minions.push(minion);
|
||||||
|
minwork.kick();
|
||||||
|
};
|
||||||
|
minion.postMessage(qrec.work, qrec.direct);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
broadcast(cmd, data, direct) {
|
||||||
|
for (let minion of minions) {
|
||||||
|
minion.postMessage({
|
||||||
|
cmd, ...data
|
||||||
|
}, direct);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setPoints(points) {
|
||||||
|
let i = 0, floatP = new Float32Array(points.length * 3);
|
||||||
|
for (let p of points) {
|
||||||
|
floatP[i++] = p.x;
|
||||||
|
floatP[i++] = p.y;
|
||||||
|
floatP[i++] = p.z;
|
||||||
|
}
|
||||||
|
minwork.broadcast("setPoints", { points: floatP });
|
||||||
|
},
|
||||||
|
|
||||||
|
// added functions (should be namespaced)
|
||||||
|
|
||||||
|
subtract({ a, b, outA, outB, z, area, wasm }) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (concurrent < 2 || a.length + b.length < concurrent * 2 || POLY.points([...a,...b]) < concurrent * 50) {
|
||||||
|
POLY.subtract(a, b, outA, outB, z, area, { wasm });
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
minwork.queue({
|
||||||
|
cmd: "subtract",
|
||||||
|
opt: { area, wasm, z },
|
||||||
|
arg: {
|
||||||
|
a: codec.encode(a),
|
||||||
|
b: codec.encode(b),
|
||||||
|
outA: outA ? 1 : 0,
|
||||||
|
outB: outB ? 1 : 0,
|
||||||
|
}
|
||||||
|
}, result => {
|
||||||
|
if (outA) outA.push(...codec.decode(result.outA));
|
||||||
|
if (outB) outB.push(...codec.decode(result.outB));
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
union(polys, minarea) {
|
union(polys, minarea) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (concurrent < 2 || polys.length < concurrent * 2 || POLY.points(polys) < concurrent * 50) {
|
if (concurrent < 2 || polys.length < concurrent * 2 || POLY.points(polys) < concurrent * 50) {
|
||||||
|
|
@ -222,17 +295,9 @@ const minwork = {
|
||||||
reject("concurrent slice unavaiable");
|
reject("concurrent slice unavaiable");
|
||||||
}
|
}
|
||||||
let { each } = options;
|
let { each } = options;
|
||||||
// todo use shared array buffer?
|
|
||||||
let i = 0, floatP = new Float32Array(points.length * 3);
|
|
||||||
for (let p of points) {
|
|
||||||
floatP[i++] = p.x;
|
|
||||||
floatP[i++] = p.y;
|
|
||||||
floatP[i++] = p.z;
|
|
||||||
}
|
|
||||||
minwork.queue({
|
minwork.queue({
|
||||||
cmd: "sliceZ",
|
cmd: "sliceZ",
|
||||||
z,
|
z,
|
||||||
points: floatP,
|
|
||||||
options: codec.toCodable(options)
|
options: codec.toCodable(options)
|
||||||
}, data => {
|
}, data => {
|
||||||
let recs = codec.decode(data.output);
|
let recs = codec.decode(data.output);
|
||||||
|
|
@ -242,43 +307,9 @@ const minwork = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resolve(recs);
|
resolve(recs);
|
||||||
}, [ floatP.buffer ]);
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
queue(work, ondone, direct) {
|
|
||||||
minionq.push({work, ondone, direct});
|
|
||||||
minwork.kick();
|
|
||||||
},
|
|
||||||
|
|
||||||
queueAsync(work, direct) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
minwork.queue(work, resolve, direct);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
kick() {
|
|
||||||
if (minions.length && minionq.length) {
|
|
||||||
let qrec = minionq.shift();
|
|
||||||
let minion = minions.shift();
|
|
||||||
let seq = miniseq++;
|
|
||||||
qrec.work.seq = seq;
|
|
||||||
minifns[seq] = (data) => {
|
|
||||||
qrec.ondone(data);
|
|
||||||
minions.push(minion);
|
|
||||||
minwork.kick();
|
|
||||||
};
|
|
||||||
minion.postMessage(qrec.work, qrec.direct);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
broadcast(cmd, data, direct) {
|
|
||||||
for (let minion of minions) {
|
|
||||||
minion.postMessage({
|
|
||||||
cmd, ...data
|
|
||||||
}, direct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(`kiri | init work | ${version || "rogue"}`);
|
console.log(`kiri | init work | ${version || "rogue"}`);
|
||||||
|
|
@ -586,9 +617,9 @@ const dispatch = {
|
||||||
const { process } = settings;
|
const { process } = settings;
|
||||||
const origin = settings.origin;
|
const origin = settings.origin;
|
||||||
const offset = {
|
const offset = {
|
||||||
x: origin.x - (process.camOriginOffX ?? 0),
|
x: origin.x,// - (process.camOriginOffX ?? 0),
|
||||||
y: -origin.y - (process.camOriginOffY ?? 0),
|
y: -origin.y,// - (process.camOriginOffY ?? 0),
|
||||||
z: origin.z - (process.camOriginOffZ ?? 0)
|
z: origin.z,// + (process.camOriginOffZ ?? 0)
|
||||||
};
|
};
|
||||||
const device = settings.device;
|
const device = settings.device;
|
||||||
const print = setPrint(newPrint(settings, Object.values(wcache)));
|
const print = setPrint(newPrint(settings, Object.values(wcache)));
|
||||||
|
|
|
||||||
932
src/load/dxf.js
Normal file
932
src/load/dxf.js
Normal file
|
|
@ -0,0 +1,932 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
import { newPolygon } from '../geo/polygon.js';
|
||||||
|
import { newPoint } from '../geo/point.js';
|
||||||
|
import { polygons } from '../geo/polygons.js';
|
||||||
|
|
||||||
|
export function parseAsync(text, opt) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
resolve(parse(text, opt));
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parse(text, opt = { }) {
|
||||||
|
const justPoly = opt.flat || false;
|
||||||
|
const fromSoup = opt.soup !== false || justPoly;
|
||||||
|
const depth = parseFloat(opt.depth || 5);
|
||||||
|
const segmentSize = parseFloat(opt.segmentSize || 1); // default 1mm segments
|
||||||
|
const minSegments = parseInt(opt.minSegments || 4); // minimum segments for very small arcs
|
||||||
|
const objs = [];
|
||||||
|
const polys = [];
|
||||||
|
|
||||||
|
// Parse DXF file - normalize line endings and split
|
||||||
|
const lines = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n').map(l => l.trim());
|
||||||
|
|
||||||
|
// Parse header for units (can be overridden by user)
|
||||||
|
const fileUnits = extractUnits(lines);
|
||||||
|
// Use file units if "auto" or not specified, otherwise use user's choice
|
||||||
|
const inputUnits = (!opt.units || opt.units === 'auto') ? fileUnits : opt.units;
|
||||||
|
const scale = getScaleToMM(inputUnits); // convert to mm (Kiri:Moto's internal unit)
|
||||||
|
|
||||||
|
const entities = extractEntities(lines);
|
||||||
|
|
||||||
|
// Scale all entities to mm BEFORE stitching
|
||||||
|
scaleEntities(entities, scale);
|
||||||
|
|
||||||
|
// Stitch together open paths that share endpoints (tolerance in mm now)
|
||||||
|
const tolerance = Math.max(0.001, segmentSize * 0.001); // 0.1% of segment size, min 0.001mm
|
||||||
|
const stitchedEntities = stitchPaths(entities, tolerance);
|
||||||
|
|
||||||
|
// Convert entities to polygons (no scaling needed, already in mm)
|
||||||
|
for (let entity of stitchedEntities) {
|
||||||
|
if (entity.type === 'STITCHED') {
|
||||||
|
// Convert stitched path parts into a single polyline
|
||||||
|
const points = [];
|
||||||
|
for (const part of entity.parts) {
|
||||||
|
const partPoints = convertEntityToPoints(part, segmentSize, minSegments);
|
||||||
|
if (partPoints.length > 0) {
|
||||||
|
if (points.length === 0) {
|
||||||
|
points.push(...partPoints);
|
||||||
|
} else {
|
||||||
|
// Skip first point if it's the same as our last point (avoid duplicates)
|
||||||
|
points.push(...partPoints.slice(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (points.length < 2) continue;
|
||||||
|
|
||||||
|
let poly = newPolygon().addPoints(
|
||||||
|
points.map(p => newPoint(p.x, p.y, p.z || 0))
|
||||||
|
).clean();
|
||||||
|
|
||||||
|
if (entity.closed && poly.appearsClosed()) {
|
||||||
|
poly.points.pop();
|
||||||
|
} else if (!entity.closed) {
|
||||||
|
poly.setOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
polys.push(poly);
|
||||||
|
} else if (entity.type === 'POLYLINE' || entity.type === 'LWPOLYLINE') {
|
||||||
|
if (entity.points.length < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let poly = newPolygon().addPoints(
|
||||||
|
entity.points.map(p => newPoint(p.x, p.y, p.z || 0))
|
||||||
|
).clean();
|
||||||
|
|
||||||
|
// Check if closed
|
||||||
|
if (entity.closed && poly.appearsClosed()) {
|
||||||
|
poly.points.pop();
|
||||||
|
} else if (!entity.closed) {
|
||||||
|
poly.setOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
polys.push(poly);
|
||||||
|
} else if (entity.type === 'LINE') {
|
||||||
|
// Convert line to polyline
|
||||||
|
let poly = newPolygon().addPoints([
|
||||||
|
newPoint(entity.start.x, entity.start.y, entity.start.z || 0),
|
||||||
|
newPoint(entity.end.x, entity.end.y, entity.end.z || 0)
|
||||||
|
]);
|
||||||
|
poly.setOpen(true);
|
||||||
|
polys.push(poly);
|
||||||
|
} else if (entity.type === 'CIRCLE') {
|
||||||
|
// Convert circle to polygon with points
|
||||||
|
// Calculate segments based on circumference and desired segment size (already in mm)
|
||||||
|
const circumference = 2 * Math.PI * entity.radius;
|
||||||
|
const segments = Math.max(minSegments, Math.ceil(circumference / segmentSize));
|
||||||
|
let points = [];
|
||||||
|
for (let i = 0; i < segments; i++) {
|
||||||
|
const angle = (i / segments) * Math.PI * 2;
|
||||||
|
points.push(newPoint(
|
||||||
|
entity.center.x + Math.cos(angle) * entity.radius,
|
||||||
|
entity.center.y + Math.sin(angle) * entity.radius,
|
||||||
|
entity.center.z || 0
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let poly = newPolygon().addPoints(points).clean();
|
||||||
|
polys.push(poly);
|
||||||
|
} else if (entity.type === 'ARC') {
|
||||||
|
// Convert arc to polyline
|
||||||
|
// DXF arcs always go counterclockwise. Handle angle wrapping.
|
||||||
|
let startAngle = entity.startAngle;
|
||||||
|
let endAngle = entity.endAngle;
|
||||||
|
let angleDiff = endAngle - startAngle;
|
||||||
|
|
||||||
|
// If endAngle < startAngle, arc wraps through 0/360
|
||||||
|
if (angleDiff < 0) {
|
||||||
|
angleDiff += Math.PI * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const arcLength = angleDiff * entity.radius;
|
||||||
|
const segments = Math.max(minSegments, Math.ceil(arcLength / segmentSize));
|
||||||
|
let points = [];
|
||||||
|
|
||||||
|
if (entity.reversed) {
|
||||||
|
// Sample backwards from end to start
|
||||||
|
for (let i = 0; i <= segments; i++) {
|
||||||
|
const angle = endAngle - (i / segments) * angleDiff;
|
||||||
|
points.push(newPoint(
|
||||||
|
entity.center.x + Math.cos(angle) * entity.radius,
|
||||||
|
entity.center.y + Math.sin(angle) * entity.radius,
|
||||||
|
entity.center.z || 0
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Sample forward from start to end
|
||||||
|
for (let i = 0; i <= segments; i++) {
|
||||||
|
const angle = startAngle + (i / segments) * angleDiff;
|
||||||
|
points.push(newPoint(
|
||||||
|
entity.center.x + Math.cos(angle) * entity.radius,
|
||||||
|
entity.center.y + Math.sin(angle) * entity.radius,
|
||||||
|
entity.center.z || 0
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let poly = newPolygon().addPoints(points);
|
||||||
|
poly.setOpen(true);
|
||||||
|
polys.push(poly);
|
||||||
|
} else if (entity.type === 'SPLINE') {
|
||||||
|
// Convert NURBS spline to polyline by sampling (already in mm)
|
||||||
|
if (entity.controlPoints.length < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const points = evaluateSpline(entity, segmentSize, minSegments);
|
||||||
|
if (points.length < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let poly = newPolygon().addPoints(points);
|
||||||
|
if (entity.closed) {
|
||||||
|
// Remove duplicate end point if closed
|
||||||
|
if (poly.appearsClosed()) {
|
||||||
|
poly.points.pop();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
poly.setOpen(true);
|
||||||
|
}
|
||||||
|
polys.push(poly);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nest polygons to identify holes vs outlines
|
||||||
|
const sub = fromSoup ? polygons.nest(polys) : polys;
|
||||||
|
const nest = sub.filter(p => {
|
||||||
|
for (let pc of polys) {
|
||||||
|
if (pc === p) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return !pc.isEquivalent(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (justPoly) {
|
||||||
|
return nest;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extrude polygons to 3D
|
||||||
|
for (let poly of nest) {
|
||||||
|
let obj = poly.extrude(depth);
|
||||||
|
objs.push(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
return objs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractEntities(lines) {
|
||||||
|
const entities = [];
|
||||||
|
let inEntities = false;
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
while (i < lines.length - 1) {
|
||||||
|
const code = lines[i];
|
||||||
|
const value = lines[i + 1];
|
||||||
|
|
||||||
|
// Check if we're in the ENTITIES section
|
||||||
|
if (code === '0' && value === 'SECTION') {
|
||||||
|
if (i + 3 < lines.length && lines[i + 2] === '2' && lines[i + 3] === 'ENTITIES') {
|
||||||
|
inEntities = true;
|
||||||
|
i += 4;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code === '0' && value === 'ENDSEC' && inEntities) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inEntities && code === '0') {
|
||||||
|
if (value === 'POLYLINE') {
|
||||||
|
const entity = parsePolyline(lines, i);
|
||||||
|
if (entity) {
|
||||||
|
entities.push(entity);
|
||||||
|
i = entity.endIndex;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else if (value === 'LWPOLYLINE') {
|
||||||
|
const entity = parseLWPolyline(lines, i);
|
||||||
|
if (entity) {
|
||||||
|
entities.push(entity);
|
||||||
|
i = entity.endIndex;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else if (value === 'LINE') {
|
||||||
|
const entity = parseLine(lines, i);
|
||||||
|
if (entity) {
|
||||||
|
entities.push(entity);
|
||||||
|
i = entity.endIndex;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else if (value === 'CIRCLE') {
|
||||||
|
const entity = parseCircle(lines, i);
|
||||||
|
if (entity) {
|
||||||
|
entities.push(entity);
|
||||||
|
i = entity.endIndex;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else if (value === 'ARC') {
|
||||||
|
const entity = parseArc(lines, i);
|
||||||
|
if (entity) {
|
||||||
|
entities.push(entity);
|
||||||
|
i = entity.endIndex;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else if (value === 'SPLINE') {
|
||||||
|
const entity = parseSpline(lines, i);
|
||||||
|
if (entity) {
|
||||||
|
entities.push(entity);
|
||||||
|
i = entity.endIndex;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePolyline(lines, start) {
|
||||||
|
let i = start + 2;
|
||||||
|
let closed = false;
|
||||||
|
const points = [];
|
||||||
|
|
||||||
|
// Read polyline flags
|
||||||
|
while (i < lines.length - 1) {
|
||||||
|
const code = lines[i];
|
||||||
|
const value = lines[i + 1];
|
||||||
|
|
||||||
|
if (code === '70') {
|
||||||
|
// Polyline flag: 1 = closed
|
||||||
|
closed = (parseInt(value) & 1) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code === '0' && value === 'VERTEX') {
|
||||||
|
const vertex = parseVertex(lines, i);
|
||||||
|
if (vertex) {
|
||||||
|
points.push(vertex.point);
|
||||||
|
i = vertex.endIndex;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code === '0' && value === 'SEQEND') {
|
||||||
|
return { type: 'POLYLINE', points, closed, endIndex: i + 2 };
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseVertex(lines, start) {
|
||||||
|
let i = start + 2;
|
||||||
|
const point = { x: 0, y: 0, z: 0 };
|
||||||
|
|
||||||
|
while (i < lines.length - 1) {
|
||||||
|
const code = lines[i];
|
||||||
|
const value = lines[i + 1];
|
||||||
|
|
||||||
|
if (code === '10') point.x = parseFloat(value);
|
||||||
|
if (code === '20') point.y = parseFloat(value);
|
||||||
|
if (code === '30') point.z = parseFloat(value);
|
||||||
|
|
||||||
|
if (code === '0') {
|
||||||
|
return { point, endIndex: i };
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { point, endIndex: i };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLWPolyline(lines, start) {
|
||||||
|
let i = start + 2;
|
||||||
|
let closed = false;
|
||||||
|
const points = [];
|
||||||
|
let currentPoint = null;
|
||||||
|
|
||||||
|
while (i < lines.length - 1) {
|
||||||
|
const code = lines[i];
|
||||||
|
const value = lines[i + 1];
|
||||||
|
|
||||||
|
if (code === '70') {
|
||||||
|
closed = (parseInt(value) & 1) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code === '10') {
|
||||||
|
if (currentPoint) {
|
||||||
|
points.push(currentPoint);
|
||||||
|
}
|
||||||
|
currentPoint = { x: parseFloat(value), y: 0, z: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code === '20' && currentPoint) {
|
||||||
|
currentPoint.y = parseFloat(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code === '0') {
|
||||||
|
if (currentPoint) {
|
||||||
|
points.push(currentPoint);
|
||||||
|
}
|
||||||
|
return { type: 'LWPOLYLINE', points, closed, endIndex: i };
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPoint) {
|
||||||
|
points.push(currentPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: 'LWPOLYLINE', points, closed, endIndex: i };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLine(lines, start) {
|
||||||
|
let i = start + 2;
|
||||||
|
const start_point = { x: 0, y: 0, z: 0 };
|
||||||
|
const end_point = { x: 0, y: 0, z: 0 };
|
||||||
|
|
||||||
|
while (i < lines.length - 1) {
|
||||||
|
const code = lines[i];
|
||||||
|
const value = lines[i + 1];
|
||||||
|
|
||||||
|
if (code === '10') start_point.x = parseFloat(value);
|
||||||
|
if (code === '20') start_point.y = parseFloat(value);
|
||||||
|
if (code === '30') start_point.z = parseFloat(value);
|
||||||
|
if (code === '11') end_point.x = parseFloat(value);
|
||||||
|
if (code === '21') end_point.y = parseFloat(value);
|
||||||
|
if (code === '31') end_point.z = parseFloat(value);
|
||||||
|
|
||||||
|
if (code === '0') {
|
||||||
|
return { type: 'LINE', start: start_point, end: end_point, endIndex: i };
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: 'LINE', start: start_point, end: end_point, endIndex: i };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCircle(lines, start) {
|
||||||
|
let i = start + 2;
|
||||||
|
const center = { x: 0, y: 0, z: 0 };
|
||||||
|
let radius = 0;
|
||||||
|
|
||||||
|
while (i < lines.length - 1) {
|
||||||
|
const code = lines[i];
|
||||||
|
const value = lines[i + 1];
|
||||||
|
|
||||||
|
if (code === '10') center.x = parseFloat(value);
|
||||||
|
if (code === '20') center.y = parseFloat(value);
|
||||||
|
if (code === '30') center.z = parseFloat(value);
|
||||||
|
if (code === '40') radius = parseFloat(value);
|
||||||
|
|
||||||
|
if (code === '0') {
|
||||||
|
return { type: 'CIRCLE', center, radius, endIndex: i };
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: 'CIRCLE', center, radius, endIndex: i };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArc(lines, start) {
|
||||||
|
let i = start + 2;
|
||||||
|
const center = { x: 0, y: 0, z: 0 };
|
||||||
|
let radius = 0;
|
||||||
|
let startAngle = 0;
|
||||||
|
let endAngle = 0;
|
||||||
|
|
||||||
|
while (i < lines.length - 1) {
|
||||||
|
const code = lines[i];
|
||||||
|
const value = lines[i + 1];
|
||||||
|
|
||||||
|
if (code === '10') center.x = parseFloat(value);
|
||||||
|
if (code === '20') center.y = parseFloat(value);
|
||||||
|
if (code === '30') center.z = parseFloat(value);
|
||||||
|
if (code === '40') radius = parseFloat(value);
|
||||||
|
if (code === '50') startAngle = parseFloat(value) * Math.PI / 180; // Convert to radians
|
||||||
|
if (code === '51') endAngle = parseFloat(value) * Math.PI / 180; // Convert to radians
|
||||||
|
|
||||||
|
if (code === '0') {
|
||||||
|
return { type: 'ARC', center, radius, startAngle, endAngle, endIndex: i };
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: 'ARC', center, radius, startAngle, endAngle, endIndex: i };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSpline(lines, start) {
|
||||||
|
let i = start + 2;
|
||||||
|
let degree = 3; // default cubic
|
||||||
|
let closed = false;
|
||||||
|
const controlPoints = [];
|
||||||
|
const knots = [];
|
||||||
|
let numKnots = 0;
|
||||||
|
let numControlPoints = 0;
|
||||||
|
|
||||||
|
while (i < lines.length - 1) {
|
||||||
|
const code = lines[i];
|
||||||
|
const value = lines[i + 1];
|
||||||
|
|
||||||
|
if (code === '70') {
|
||||||
|
// Spline flag: bit 0 (1) = closed
|
||||||
|
closed = (parseInt(value) & 1) === 1;
|
||||||
|
}
|
||||||
|
if (code === '71') degree = parseInt(value);
|
||||||
|
if (code === '72') numKnots = parseInt(value);
|
||||||
|
if (code === '73') numControlPoints = parseInt(value);
|
||||||
|
if (code === '40') {
|
||||||
|
// Knot value
|
||||||
|
knots.push(parseFloat(value));
|
||||||
|
}
|
||||||
|
if (code === '10') {
|
||||||
|
// Control point X - start new point
|
||||||
|
controlPoints.push({ x: parseFloat(value), y: 0, z: 0 });
|
||||||
|
}
|
||||||
|
if (code === '20' && controlPoints.length > 0) {
|
||||||
|
// Control point Y
|
||||||
|
controlPoints[controlPoints.length - 1].y = parseFloat(value);
|
||||||
|
}
|
||||||
|
if (code === '30' && controlPoints.length > 0) {
|
||||||
|
// Control point Z
|
||||||
|
controlPoints[controlPoints.length - 1].z = parseFloat(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code === '0') {
|
||||||
|
return { type: 'SPLINE', degree, closed, knots, controlPoints, endIndex: i };
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: 'SPLINE', degree, closed, knots, controlPoints, endIndex: i };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate NURBS B-spline curve to generate sample points (entities already scaled to mm)
|
||||||
|
function evaluateSpline(entity, segmentSize, minSegments) {
|
||||||
|
const { degree, controlPoints, knots, closed } = entity;
|
||||||
|
|
||||||
|
if (controlPoints.length < degree + 1 || knots.length === 0) {
|
||||||
|
// Degenerate spline, just return control points
|
||||||
|
return controlPoints.map(p => newPoint(p.x, p.y, p.z));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Estimate curve length by summing control point distances (rough approximation)
|
||||||
|
let estimatedLength = 0;
|
||||||
|
for (let i = 1; i < controlPoints.length; i++) {
|
||||||
|
const dx = controlPoints[i].x - controlPoints[i-1].x;
|
||||||
|
const dy = controlPoints[i].y - controlPoints[i-1].y;
|
||||||
|
estimatedLength += Math.sqrt(dx * dx + dy * dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate number of samples
|
||||||
|
const numSamples = Math.max(minSegments, Math.ceil(estimatedLength / segmentSize));
|
||||||
|
const points = [];
|
||||||
|
|
||||||
|
// Find parameter range (first and last non-repeated knot values)
|
||||||
|
const knotStart = knots[degree];
|
||||||
|
const knotEnd = knots[knots.length - degree - 1];
|
||||||
|
|
||||||
|
if (knotStart >= knotEnd) {
|
||||||
|
// Invalid knot vector, return control points
|
||||||
|
return controlPoints.map(p => newPoint(p.x, p.y, p.z));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sample the curve
|
||||||
|
for (let i = 0; i <= numSamples; i++) {
|
||||||
|
const t = knotStart + (i / numSamples) * (knotEnd - knotStart);
|
||||||
|
const point = evaluateNURBS(t, degree, controlPoints, knots);
|
||||||
|
points.push(newPoint(point.x, point.y, point.z));
|
||||||
|
}
|
||||||
|
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate a single point on a NURBS curve using De Boor's algorithm
|
||||||
|
function evaluateNURBS(t, degree, controlPoints, knots) {
|
||||||
|
const n = controlPoints.length - 1;
|
||||||
|
|
||||||
|
// Clamp t to valid range
|
||||||
|
t = Math.max(knots[degree], Math.min(knots[n + 1], t));
|
||||||
|
|
||||||
|
// Find knot span (which segment t falls into)
|
||||||
|
let span = degree;
|
||||||
|
while (span <= n && knots[span + 1] <= t) {
|
||||||
|
span++;
|
||||||
|
}
|
||||||
|
if (span > n) span = n;
|
||||||
|
|
||||||
|
// Compute basis functions using Cox-de Boor recursion
|
||||||
|
const N = [];
|
||||||
|
for (let i = 0; i <= n; i++) {
|
||||||
|
N[i] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize degree 0 basis functions
|
||||||
|
for (let i = 0; i <= n; i++) {
|
||||||
|
if (t >= knots[i] && t < knots[i + 1]) {
|
||||||
|
N[i][0] = 1.0;
|
||||||
|
} else {
|
||||||
|
N[i][0] = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Special case for last knot
|
||||||
|
if (t === knots[n + 1]) {
|
||||||
|
N[n][0] = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute higher degree basis functions
|
||||||
|
for (let k = 1; k <= degree; k++) {
|
||||||
|
for (let i = 0; i <= n; i++) {
|
||||||
|
let c1 = 0, c2 = 0;
|
||||||
|
|
||||||
|
if (N[i][k - 1] !== 0) {
|
||||||
|
if (knots[i + k] !== knots[i]) {
|
||||||
|
c1 = ((t - knots[i]) / (knots[i + k] - knots[i])) * N[i][k - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i + 1 <= n && N[i + 1][k - 1] !== 0) {
|
||||||
|
if (knots[i + k + 1] !== knots[i + 1]) {
|
||||||
|
c2 = ((knots[i + k + 1] - t) / (knots[i + k + 1] - knots[i + 1])) * N[i + 1][k - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
N[i][k] = c1 + c2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute curve point as weighted sum of control points
|
||||||
|
let x = 0, y = 0, z = 0;
|
||||||
|
for (let i = 0; i <= n; i++) {
|
||||||
|
const weight = N[i][degree] || 0;
|
||||||
|
x += controlPoints[i].x * weight;
|
||||||
|
y += controlPoints[i].y * weight;
|
||||||
|
z += controlPoints[i].z * weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { x, y, z };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale all entity coordinates to millimeters
|
||||||
|
function scaleEntities(entities, scale) {
|
||||||
|
if (scale === 1) return; // no scaling needed
|
||||||
|
|
||||||
|
for (let entity of entities) {
|
||||||
|
if (entity.type === 'LINE') {
|
||||||
|
entity.start.x *= scale;
|
||||||
|
entity.start.y *= scale;
|
||||||
|
entity.start.z = (entity.start.z || 0) * scale;
|
||||||
|
entity.end.x *= scale;
|
||||||
|
entity.end.y *= scale;
|
||||||
|
entity.end.z = (entity.end.z || 0) * scale;
|
||||||
|
} else if (entity.type === 'CIRCLE') {
|
||||||
|
entity.center.x *= scale;
|
||||||
|
entity.center.y *= scale;
|
||||||
|
entity.center.z = (entity.center.z || 0) * scale;
|
||||||
|
entity.radius *= scale;
|
||||||
|
} else if (entity.type === 'ARC') {
|
||||||
|
entity.center.x *= scale;
|
||||||
|
entity.center.y *= scale;
|
||||||
|
entity.center.z = (entity.center.z || 0) * scale;
|
||||||
|
entity.radius *= scale;
|
||||||
|
} else if (entity.type === 'POLYLINE' || entity.type === 'LWPOLYLINE') {
|
||||||
|
for (let point of entity.points) {
|
||||||
|
point.x *= scale;
|
||||||
|
point.y *= scale;
|
||||||
|
point.z = (point.z || 0) * scale;
|
||||||
|
}
|
||||||
|
} else if (entity.type === 'SPLINE') {
|
||||||
|
for (let point of entity.controlPoints) {
|
||||||
|
point.x *= scale;
|
||||||
|
point.y *= scale;
|
||||||
|
point.z = (point.z || 0) * scale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract units from DXF header
|
||||||
|
function extractUnits(lines) {
|
||||||
|
let i = 0;
|
||||||
|
let inHeader = false;
|
||||||
|
|
||||||
|
while (i < lines.length - 1) {
|
||||||
|
const code = lines[i];
|
||||||
|
const value = lines[i + 1];
|
||||||
|
|
||||||
|
if (code === '0' && value === 'SECTION') {
|
||||||
|
if (i + 3 < lines.length && lines[i + 2] === '2' && lines[i + 3] === 'HEADER') {
|
||||||
|
inHeader = true;
|
||||||
|
i += 4;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code === '0' && value === 'ENDSEC' && inHeader) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inHeader && code === '9' && value === '$INSUNITS') {
|
||||||
|
// Next line should be 70, followed by the unit code
|
||||||
|
if (i + 3 < lines.length && lines[i + 2] === '70') {
|
||||||
|
const unitCode = parseInt(lines[i + 3]);
|
||||||
|
// DXF INSUNITS codes: 0=unitless, 1=inches, 2=feet, 4=mm, 5=cm, 6=meters
|
||||||
|
switch (unitCode) {
|
||||||
|
case 1: return 'inch';
|
||||||
|
case 2: return 'foot';
|
||||||
|
case 4: return 'mm';
|
||||||
|
case 5: return 'cm';
|
||||||
|
case 6: return 'meter';
|
||||||
|
default: return 'mm'; // default to mm for unitless
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'mm'; // default to millimeters
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get scale factor to convert from input units to millimeters
|
||||||
|
function getScaleToMM(inputUnits) {
|
||||||
|
// Scale factors to convert to mm (Kiri:Moto's internal unit)
|
||||||
|
const toMM = {
|
||||||
|
'mm': 1,
|
||||||
|
'cm': 10,
|
||||||
|
'meter': 1000,
|
||||||
|
'inch': 25.4,
|
||||||
|
'foot': 304.8
|
||||||
|
};
|
||||||
|
|
||||||
|
return toMM[inputUnits] || 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stitch together open paths that share endpoints
|
||||||
|
function stitchPaths(entities, tolerance = 0.01) {
|
||||||
|
const stitched = [];
|
||||||
|
const used = new Set();
|
||||||
|
|
||||||
|
// Helper to check if two points are within tolerance
|
||||||
|
const pointsMatch = (p1, p2) => {
|
||||||
|
const dx = p1.x - p2.x;
|
||||||
|
const dy = p1.y - p2.y;
|
||||||
|
const dz = (p1.z || 0) - (p2.z || 0);
|
||||||
|
return Math.sqrt(dx * dx + dy * dy + dz * dz) < tolerance;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to get endpoints of an entity
|
||||||
|
const getEndpoints = (entity) => {
|
||||||
|
if (entity.type === 'LINE') {
|
||||||
|
return { start: entity.start, end: entity.end };
|
||||||
|
} else if (entity.type === 'ARC') {
|
||||||
|
// Calculate actual arc endpoints
|
||||||
|
const startX = entity.center.x + Math.cos(entity.startAngle) * entity.radius;
|
||||||
|
const startY = entity.center.y + Math.sin(entity.startAngle) * entity.radius;
|
||||||
|
const endX = entity.center.x + Math.cos(entity.endAngle) * entity.radius;
|
||||||
|
const endY = entity.center.y + Math.sin(entity.endAngle) * entity.radius;
|
||||||
|
const start = { x: startX, y: startY, z: entity.center.z || 0 };
|
||||||
|
const end = { x: endX, y: endY, z: entity.center.z || 0 };
|
||||||
|
// If arc is reversed, swap the endpoints
|
||||||
|
if (entity.reversed) {
|
||||||
|
return { start: end, end: start };
|
||||||
|
}
|
||||||
|
return { start, end };
|
||||||
|
} else if (entity.type === 'POLYLINE' || entity.type === 'LWPOLYLINE') {
|
||||||
|
if (entity.closed || entity.points.length < 2) return null;
|
||||||
|
return {
|
||||||
|
start: entity.points[0],
|
||||||
|
end: entity.points[entity.points.length - 1]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to convert entity to points
|
||||||
|
const entityToPoints = (entity) => {
|
||||||
|
if (entity.type === 'LINE') {
|
||||||
|
return [entity.start, entity.end];
|
||||||
|
} else if (entity.type === 'POLYLINE' || entity.type === 'LWPOLYLINE') {
|
||||||
|
return [...entity.points];
|
||||||
|
}
|
||||||
|
// For ARC and other types, return null (will be converted later in main loop)
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// First, identify stitchable entities (LINE, ARC, open POLYLINE)
|
||||||
|
const stitchable = [];
|
||||||
|
for (let i = 0; i < entities.length; i++) {
|
||||||
|
const entity = entities[i];
|
||||||
|
if (entity.type === 'LINE' || entity.type === 'ARC' ||
|
||||||
|
((entity.type === 'POLYLINE' || entity.type === 'LWPOLYLINE') && !entity.closed)) {
|
||||||
|
stitchable.push({ entity, index: i });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to stitch paths together
|
||||||
|
for (let i = 0; i < stitchable.length; i++) {
|
||||||
|
if (used.has(i)) continue;
|
||||||
|
|
||||||
|
const { entity, index } = stitchable[i];
|
||||||
|
const endpoints = getEndpoints(entity);
|
||||||
|
if (!endpoints) {
|
||||||
|
stitched.push(entity);
|
||||||
|
used.add(i);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a new path
|
||||||
|
const path = [entity];
|
||||||
|
used.add(i);
|
||||||
|
let currentEnd = endpoints.end;
|
||||||
|
let currentStart = endpoints.start;
|
||||||
|
let foundMatch = true;
|
||||||
|
|
||||||
|
// Keep extending the path
|
||||||
|
while (foundMatch) {
|
||||||
|
foundMatch = false;
|
||||||
|
|
||||||
|
for (let j = 0; j < stitchable.length; j++) {
|
||||||
|
if (used.has(j)) continue;
|
||||||
|
|
||||||
|
const nextEndpoints = getEndpoints(stitchable[j].entity);
|
||||||
|
if (!nextEndpoints) continue;
|
||||||
|
|
||||||
|
// Check if this entity connects to current end
|
||||||
|
if (pointsMatch(currentEnd, nextEndpoints.start)) {
|
||||||
|
path.push(stitchable[j].entity);
|
||||||
|
currentEnd = nextEndpoints.end;
|
||||||
|
used.add(j);
|
||||||
|
foundMatch = true;
|
||||||
|
break;
|
||||||
|
} else if (pointsMatch(currentEnd, nextEndpoints.end)) {
|
||||||
|
// Need to reverse this entity
|
||||||
|
const reversed = reverseEntity(stitchable[j].entity);
|
||||||
|
path.push(reversed);
|
||||||
|
// After reversing, the start becomes the new end
|
||||||
|
const reversedEndpoints = getEndpoints(reversed);
|
||||||
|
currentEnd = reversedEndpoints.end;
|
||||||
|
used.add(j);
|
||||||
|
foundMatch = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Check if this entity connects to current start (prepend)
|
||||||
|
else if (pointsMatch(currentStart, nextEndpoints.end)) {
|
||||||
|
path.unshift(stitchable[j].entity);
|
||||||
|
currentStart = nextEndpoints.start;
|
||||||
|
used.add(j);
|
||||||
|
foundMatch = true;
|
||||||
|
break;
|
||||||
|
} else if (pointsMatch(currentStart, nextEndpoints.start)) {
|
||||||
|
// Need to reverse and prepend
|
||||||
|
const reversed = reverseEntity(stitchable[j].entity);
|
||||||
|
path.unshift(reversed);
|
||||||
|
// After reversing, the end becomes the new start
|
||||||
|
const reversedEndpoints = getEndpoints(reversed);
|
||||||
|
currentStart = reversedEndpoints.start;
|
||||||
|
used.add(j);
|
||||||
|
foundMatch = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert path to a single stitched entity
|
||||||
|
if (path.length === 1) {
|
||||||
|
stitched.push(path[0]);
|
||||||
|
} else {
|
||||||
|
// Combine into stitched polyline - mark for later conversion
|
||||||
|
const stitchedEntity = {
|
||||||
|
type: 'STITCHED',
|
||||||
|
parts: path,
|
||||||
|
closed: pointsMatch(currentStart, currentEnd)
|
||||||
|
};
|
||||||
|
stitched.push(stitchedEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add non-stitchable entities (CIRCLE, SPLINE, closed POLYLINE)
|
||||||
|
for (let i = 0; i < entities.length; i++) {
|
||||||
|
const entity = entities[i];
|
||||||
|
if (entity.type === 'CIRCLE' || entity.type === 'SPLINE' ||
|
||||||
|
((entity.type === 'POLYLINE' || entity.type === 'LWPOLYLINE') && entity.closed)) {
|
||||||
|
stitched.push(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stitched;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse an entity's direction
|
||||||
|
function reverseEntity(entity) {
|
||||||
|
if (entity.type === 'LINE') {
|
||||||
|
return {
|
||||||
|
type: 'LINE',
|
||||||
|
start: entity.end,
|
||||||
|
end: entity.start
|
||||||
|
};
|
||||||
|
} else if (entity.type === 'ARC') {
|
||||||
|
// Mark the arc as reversed so it gets sampled in reverse
|
||||||
|
return {
|
||||||
|
type: 'ARC',
|
||||||
|
center: entity.center,
|
||||||
|
radius: entity.radius,
|
||||||
|
startAngle: entity.startAngle,
|
||||||
|
endAngle: entity.endAngle,
|
||||||
|
reversed: true
|
||||||
|
};
|
||||||
|
} else if (entity.type === 'POLYLINE' || entity.type === 'LWPOLYLINE') {
|
||||||
|
return {
|
||||||
|
type: entity.type,
|
||||||
|
points: [...entity.points].reverse(),
|
||||||
|
closed: entity.closed
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert entity to array of points (entities already scaled to mm)
|
||||||
|
function convertEntityToPoints(entity, segmentSize, minSegments) {
|
||||||
|
if (entity.type === 'LINE') {
|
||||||
|
return [entity.start, entity.end];
|
||||||
|
} else if (entity.type === 'POLYLINE' || entity.type === 'LWPOLYLINE') {
|
||||||
|
return [...entity.points];
|
||||||
|
} else if (entity.type === 'ARC') {
|
||||||
|
// DXF arcs always go counterclockwise. Handle angle wrapping.
|
||||||
|
let startAngle = entity.startAngle;
|
||||||
|
let endAngle = entity.endAngle;
|
||||||
|
let angleDiff = endAngle - startAngle;
|
||||||
|
|
||||||
|
// If endAngle < startAngle, arc wraps through 0/360
|
||||||
|
if (angleDiff < 0) {
|
||||||
|
angleDiff += Math.PI * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const arcLength = angleDiff * entity.radius;
|
||||||
|
const segments = Math.max(minSegments, Math.ceil(arcLength / segmentSize));
|
||||||
|
const points = [];
|
||||||
|
|
||||||
|
if (entity.reversed) {
|
||||||
|
// Sample backwards from end to start
|
||||||
|
for (let i = 0; i <= segments; i++) {
|
||||||
|
const angle = endAngle - (i / segments) * angleDiff;
|
||||||
|
points.push({
|
||||||
|
x: entity.center.x + Math.cos(angle) * entity.radius,
|
||||||
|
y: entity.center.y + Math.sin(angle) * entity.radius,
|
||||||
|
z: entity.center.z || 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Sample forward from start to end
|
||||||
|
for (let i = 0; i <= segments; i++) {
|
||||||
|
const angle = startAngle + (i / segments) * angleDiff;
|
||||||
|
points.push({
|
||||||
|
x: entity.center.x + Math.cos(angle) * entity.radius,
|
||||||
|
y: entity.center.y + Math.sin(angle) * entity.radius,
|
||||||
|
z: entity.center.z || 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ import { STL } from './stl.js';
|
||||||
import * as OBJ from './obj.js';
|
import * as OBJ from './obj.js';
|
||||||
import * as TMF from './3mf.js';
|
import * as TMF from './3mf.js';
|
||||||
import * as SVG from './svg.js';
|
import * as SVG from './svg.js';
|
||||||
|
import * as DXF from './dxf.js';
|
||||||
import * as GBR from './gbr.js';
|
import * as GBR from './gbr.js';
|
||||||
import { load as pngLoad } from './png.js';
|
import { load as pngLoad } from './png.js';
|
||||||
|
|
||||||
|
|
@ -36,6 +37,11 @@ const types = {
|
||||||
resolve(opt.flat ? out : out.map(m => { return { mesh: m.toFloat32(), file } }));
|
resolve(opt.flat ? out : out.map(m => { return { mesh: m.toFloat32(), file } }));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
dxf(data, file, resolve, reject, opt = {}) {
|
||||||
|
let out = DXF.parse(data, opt);
|
||||||
|
resolve(opt.flat ? out : out.map(m => { return { mesh: m.toFloat32(), file } }));
|
||||||
|
},
|
||||||
|
|
||||||
png(data, file, resolve, reject, opt = {}) {
|
png(data, file, resolve, reject, opt = {}) {
|
||||||
pngLoad.PNG.parse(data, {
|
pngLoad.PNG.parse(data, {
|
||||||
...opt,
|
...opt,
|
||||||
|
|
@ -100,6 +106,6 @@ function load_file(file, opt) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.assign(load_file, { SVG, OBJ, STL, TMF, GBR, PNG: pngLoad.PNG });
|
Object.assign(load_file, { SVG, DXF, OBJ, STL, TMF, GBR, PNG: pngLoad.PNG });
|
||||||
|
|
||||||
export { types, as_buffer, load_data, load_file, load_file as load };
|
export { types, as_buffer, load_data, load_file, load_file as load };
|
||||||
|
|
|
||||||
116
src/load/stl.js
116
src/load/stl.js
|
|
@ -13,14 +13,26 @@ const { Vector3, computeFaceNormal } = THREE;
|
||||||
* @returns {Uint8Array} binary STL data
|
* @returns {Uint8Array} binary STL data
|
||||||
*/
|
*/
|
||||||
export function encode(recs, header = '') {
|
export function encode(recs, header = '') {
|
||||||
// Calculate total vertices
|
// Calculate total triangles with strict validation.
|
||||||
let vtot = 0;
|
let triCount = 0;
|
||||||
for (let rec of recs) {
|
for (let rec of recs || []) {
|
||||||
vtot += (rec.varr.length / 3);
|
const varr = rec?.varr;
|
||||||
|
const len = Number(varr?.length || 0);
|
||||||
|
if (!Number.isFinite(len) || len <= 0) continue;
|
||||||
|
if (len % 9 !== 0) {
|
||||||
|
// Ignore malformed record instead of corrupting output sizing.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
triCount += Math.floor(len / 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
const byteLen = 84 + triCount * 50;
|
||||||
|
if (!Number.isFinite(byteLen) || byteLen <= 84 || byteLen > 0x7fffffff) {
|
||||||
|
throw new RangeError(`invalid STL byte length: ${byteLen}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create STL buffer: 80 byte header + 4 byte count + (50 bytes per triangle)
|
// Create STL buffer: 80 byte header + 4 byte count + (50 bytes per triangle)
|
||||||
let stl = new Uint8Array(80 + 4 + vtot/3 * 50);
|
let stl = new Uint8Array(byteLen);
|
||||||
let dat = new DataView(stl.buffer);
|
let dat = new DataView(stl.buffer);
|
||||||
let pos = 84;
|
let pos = 84;
|
||||||
|
|
||||||
|
|
@ -32,11 +44,12 @@ export function encode(recs, header = '') {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write triangle count at byte 80
|
// Write triangle count at byte 80
|
||||||
dat.setInt32(80, vtot/3, true);
|
dat.setUint32(80, triCount, true);
|
||||||
|
|
||||||
// Write triangles
|
// Write triangles
|
||||||
for (let rec of recs) {
|
for (let rec of recs) {
|
||||||
let { varr } = rec;
|
let { varr } = rec;
|
||||||
|
if (!varr || (varr.length % 9) !== 0) continue;
|
||||||
for (let i = 0, l = varr.length; i < l;) {
|
for (let i = 0, l = varr.length; i < l;) {
|
||||||
// Read three vertices
|
// Read three vertices
|
||||||
let p0 = new Vector3(varr[i++], varr[i++], varr[i++]);
|
let p0 = new Vector3(varr[i++], varr[i++], varr[i++]);
|
||||||
|
|
@ -72,6 +85,97 @@ export function encode(recs, header = '') {
|
||||||
return stl;
|
return stl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode STL as chunked Blob to avoid large contiguous TypedArray allocation.
|
||||||
|
* Useful when JS heap is fragmented or under pressure.
|
||||||
|
* @param {Array} recs
|
||||||
|
* @param {String} header
|
||||||
|
* @param {Number} trisPerChunk
|
||||||
|
* @returns {Blob}
|
||||||
|
*/
|
||||||
|
export function encodeBlob(recs, header = '', trisPerChunk = 4096) {
|
||||||
|
let triCount = 0;
|
||||||
|
for (let rec of recs || []) {
|
||||||
|
const len = Number(rec?.varr?.length || 0);
|
||||||
|
if (!Number.isFinite(len) || len <= 0 || (len % 9) !== 0) continue;
|
||||||
|
triCount += Math.floor(len / 9);
|
||||||
|
}
|
||||||
|
const parts = [];
|
||||||
|
const head = new Uint8Array(84);
|
||||||
|
const headView = new DataView(head.buffer);
|
||||||
|
if (header) {
|
||||||
|
header.substring(0, 80).split('').forEach((c, i) => {
|
||||||
|
headView.setUint8(i, c.charCodeAt(0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
headView.setUint32(80, triCount, true);
|
||||||
|
parts.push(head);
|
||||||
|
|
||||||
|
const maxTris = Math.max(1, Math.floor(Number(trisPerChunk) || 4096));
|
||||||
|
const p0 = new Vector3();
|
||||||
|
const p1 = new Vector3();
|
||||||
|
const p2 = new Vector3();
|
||||||
|
for (let rec of recs || []) {
|
||||||
|
const varr = rec?.varr;
|
||||||
|
if (!varr || (varr.length % 9) !== 0) continue;
|
||||||
|
let i = 0;
|
||||||
|
while (i < varr.length) {
|
||||||
|
const tris = Math.min(maxTris, Math.floor((varr.length - i) / 9));
|
||||||
|
const buf = new Uint8Array(tris * 50);
|
||||||
|
const dat = new DataView(buf.buffer);
|
||||||
|
let pos = 0;
|
||||||
|
for (let t = 0; t < tris; t++) {
|
||||||
|
p0.set(varr[i++], varr[i++], varr[i++]);
|
||||||
|
p1.set(varr[i++], varr[i++], varr[i++]);
|
||||||
|
p2.set(varr[i++], varr[i++], varr[i++]);
|
||||||
|
const norm = computeFaceNormal(p0, p1, p2);
|
||||||
|
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, true);
|
||||||
|
dat.setFloat32(pos + 16, p0.y, true);
|
||||||
|
dat.setFloat32(pos + 20, p0.z, true);
|
||||||
|
dat.setFloat32(pos + 24, p1.x, true);
|
||||||
|
dat.setFloat32(pos + 28, p1.y, true);
|
||||||
|
dat.setFloat32(pos + 32, p1.z, true);
|
||||||
|
dat.setFloat32(pos + 36, p2.x, true);
|
||||||
|
dat.setFloat32(pos + 40, p2.y, true);
|
||||||
|
dat.setFloat32(pos + 44, p2.z, true);
|
||||||
|
dat.setUint16(pos + 48, 0, true);
|
||||||
|
pos += 50;
|
||||||
|
}
|
||||||
|
parts.push(buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Blob(parts, { type: 'application/sla' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeASCII(recs, name = 'solid') {
|
||||||
|
const out = [`solid ${String(name || 'solid').replace(/\s+/g, '_')}`];
|
||||||
|
const p0 = new Vector3();
|
||||||
|
const p1 = new Vector3();
|
||||||
|
const p2 = new Vector3();
|
||||||
|
for (const rec of recs || []) {
|
||||||
|
const varr = rec?.varr;
|
||||||
|
if (!varr || (varr.length % 9) !== 0) continue;
|
||||||
|
for (let i = 0; i < varr.length;) {
|
||||||
|
p0.set(varr[i++], varr[i++], varr[i++]);
|
||||||
|
p1.set(varr[i++], varr[i++], varr[i++]);
|
||||||
|
p2.set(varr[i++], varr[i++], varr[i++]);
|
||||||
|
const n = computeFaceNormal(p0, p1, p2);
|
||||||
|
out.push(`facet normal ${n.x} ${n.y} ${n.z}`);
|
||||||
|
out.push(' outer loop');
|
||||||
|
out.push(` vertex ${p0.x} ${p0.y} ${p0.z}`);
|
||||||
|
out.push(` vertex ${p1.x} ${p1.y} ${p1.z}`);
|
||||||
|
out.push(` vertex ${p2.x} ${p2.y} ${p2.z}`);
|
||||||
|
out.push(' endloop');
|
||||||
|
out.push('endfacet');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.push(`endsolid ${String(name || 'solid').replace(/\s+/g, '_')}`);
|
||||||
|
return out.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
export class STL {
|
export class STL {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.vertices = null;
|
this.vertices = null;
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ export function parse(text, opt = { }) {
|
||||||
if (points.length < 3) {
|
if (points.length < 3) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let poly = newPolygon().addPoints(points.map(p => newPoint(p.x, -p.y, 0)));
|
let poly = newPolygon().addPoints(points.map(p => newPoint(p.x, -p.y, 0))).clean();
|
||||||
if (poly.appearsClosed()) poly.points.pop();
|
if (poly.appearsClosed()) poly.points.pop();
|
||||||
if (type === 'polyline') poly.setOpen(true);
|
if (type === 'polyline') poly.setOpen(true);
|
||||||
poly._svg = { width, miter };
|
poly._svg = { width, miter };
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,9 @@
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import { STL } from './stl.js';
|
import { load_file } from './file.js';
|
||||||
import { OBJ } from './obj.js';
|
|
||||||
import { TMF } from './3mf.js';
|
|
||||||
import { SVG } from './svg.js';
|
|
||||||
|
|
||||||
|
const { STL, OBJ, TMF, SVG, DXF } = load_file;
|
||||||
const CDH = 'Content-Disposition';
|
const CDH = 'Content-Disposition';
|
||||||
|
|
||||||
export function load_url(url, options = {}) {
|
export function load_url(url, options = {}) {
|
||||||
|
|
@ -14,7 +12,7 @@ export function load_url(url, options = {}) {
|
||||||
let xhr = new XMLHttpRequest();
|
let xhr = new XMLHttpRequest();
|
||||||
let file = options.file || options.filename || (((url.split('?')[0]).split('#')[0]).split('/')).pop();
|
let file = options.file || options.filename || (((url.split('?')[0]).split('#')[0]).split('/')).pop();
|
||||||
let ext = file.split('.').pop().toLowerCase();
|
let ext = file.split('.').pop().toLowerCase();
|
||||||
let deftype = ext === "obj" || ext === 'svg' ? "text" : "arraybuffer";
|
let deftype = ext === "obj" || ext === 'svg' || ext === 'dxf' ? "text" : "arraybuffer";
|
||||||
let datatype = options.datatype || deftype;
|
let datatype = options.datatype || deftype;
|
||||||
let formdata = options.formdata;
|
let formdata = options.formdata;
|
||||||
|
|
||||||
|
|
@ -57,6 +55,9 @@ export function load_url(url, options = {}) {
|
||||||
case "svg":
|
case "svg":
|
||||||
resolve(SVG.parse(data).map(m => { return {mesh: m.toFloat32(), file} }));
|
resolve(SVG.parse(data).map(m => { return {mesh: m.toFloat32(), file} }));
|
||||||
break;
|
break;
|
||||||
|
case "dxf":
|
||||||
|
resolve(DXF.parse(data).map(m => { return {mesh: m.toFloat32(), file} }));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
reject(`unknown file type: "${ext}" from ${url}`);
|
reject(`unknown file type: "${ext}" from ${url}`);
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { api } from '../kiri/app/api.js';
|
||||||
import { init_lang } from '../kiri/app/init/lang.js';
|
import { init_lang } from '../kiri/app/init/lang.js';
|
||||||
import { init_input } from '../kiri/app/init/input.js';
|
import { init_input } from '../kiri/app/init/input.js';
|
||||||
import { init_sync } from '../kiri/app/init/sync.js';
|
import { init_sync } from '../kiri/app/init/sync.js';
|
||||||
|
import { surfaces } from '../kiri/app/init/build.js';
|
||||||
|
|
||||||
let traceload = location.search.indexOf('traceload') > 0;
|
let traceload = location.search.indexOf('traceload') > 0;
|
||||||
let load = [];
|
let load = [];
|
||||||
|
|
@ -36,6 +37,7 @@ async function checkReady() {
|
||||||
{
|
{
|
||||||
api.client.start();
|
api.client.start();
|
||||||
await init_lang();
|
await init_lang();
|
||||||
|
surfaces.build();
|
||||||
await init_input();
|
await init_input();
|
||||||
await init_sync();
|
await init_sync();
|
||||||
}
|
}
|
||||||
|
|
@ -44,11 +46,11 @@ async function checkReady() {
|
||||||
}
|
}
|
||||||
load = undefined;
|
load = undefined;
|
||||||
api.event.emit('load-done', stats);
|
api.event.emit('load-done', stats);
|
||||||
|
api.event.emit('resize');
|
||||||
if (api.electron) {
|
if (api.electron) {
|
||||||
$('install').classList.add('hide');
|
$('install').classList.add('hide');
|
||||||
$('app-quit').classList.remove('hide');
|
$('app-quit').classList.remove('hide');
|
||||||
$('app-name-text').innerText = "More Info";
|
[...document.getElementsByClassName('el-app-hide')].forEach(el => el.classList.add('hide'));
|
||||||
$('top-sep').style.display = 'flex';
|
|
||||||
} else if (bootctrl) {
|
} else if (bootctrl) {
|
||||||
$('install').classList.add('hide');
|
$('install').classList.add('hide');
|
||||||
$('uninstall').classList.remove('hide');
|
$('uninstall').classList.remove('hide');
|
||||||
|
|
@ -57,6 +59,7 @@ async function checkReady() {
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
[...document.getElementsByClassName('app-hide')].forEach(el => el.classList.add('hide'));
|
||||||
$('install').onclick = () => {
|
$('install').onclick = () => {
|
||||||
location.replace('/boot');
|
location.replace('/boot');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
357
src/main/mesh.js
357
src/main/mesh.js
|
|
@ -17,10 +17,12 @@ import { edges as meshEdges } from '../mesh/edges.js';
|
||||||
import { open as dataOpen } from '../data/index.js';
|
import { open as dataOpen } from '../data/index.js';
|
||||||
import { load as fileLoad } from '../load/file.js';
|
import { load as fileLoad } from '../load/file.js';
|
||||||
import { THREE } from '../ext/three.js';
|
import { THREE } from '../ext/three.js';
|
||||||
|
import { createDocumentManager } from '../mesh/document.js';
|
||||||
|
|
||||||
const version = '1.5.7';
|
const version = '1.5.7';
|
||||||
const call = broker.send;
|
const call = broker.send;
|
||||||
const dbindex = [ "admin", "space" ];
|
const dbindex = [ "admin", "documents", "versions" ];
|
||||||
|
const DOC_META_KEY = '__doc';
|
||||||
|
|
||||||
const { Quaternion } = THREE;
|
const { Quaternion } = THREE;
|
||||||
|
|
||||||
|
|
@ -28,10 +30,67 @@ function log() {
|
||||||
return api.log.emit(...arguments);
|
return api.log.emit(...arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function boot_status(message = 'loading...') {
|
||||||
|
const curtain = $('curtain');
|
||||||
|
if (!curtain) return;
|
||||||
|
curtain.textContent = String(message || 'loading...');
|
||||||
|
}
|
||||||
|
|
||||||
|
function boot_start(message = 'loading...') {
|
||||||
|
$('app')?.classList?.add('booting');
|
||||||
|
boot_status(message);
|
||||||
|
$d('curtain', 'flex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function boot_done() {
|
||||||
|
$('app')?.classList?.remove('booting');
|
||||||
|
$d('curtain', 'none');
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_doc_meta(meta = metaCache) {
|
||||||
|
if (!meta || typeof meta !== 'object') return {};
|
||||||
|
return meta[DOC_META_KEY] || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function capture_camera_state() {
|
||||||
|
return {
|
||||||
|
place: space.view.save(),
|
||||||
|
focus: space.view.getFocus()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function apply_camera_state(camera) {
|
||||||
|
if (!camera) return;
|
||||||
|
if (camera.place) {
|
||||||
|
space.view.load(camera.place);
|
||||||
|
}
|
||||||
|
if (camera.focus) {
|
||||||
|
space.view.setFocus(camera.focus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function save_camera_to_document() {
|
||||||
|
const dmeta = get_doc_meta(metaCache);
|
||||||
|
metaCache[DOC_META_KEY] = {
|
||||||
|
...dmeta,
|
||||||
|
camera: capture_camera_state()
|
||||||
|
};
|
||||||
|
store_meta();
|
||||||
|
}
|
||||||
|
|
||||||
|
let cameraSaveTimer = null;
|
||||||
|
function schedule_camera_save(delay = 120) {
|
||||||
|
clearTimeout(cameraSaveTimer);
|
||||||
|
cameraSaveTimer = setTimeout(() => {
|
||||||
|
cameraSaveTimer = null;
|
||||||
|
save_camera_to_document();
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
// set below. called once the DOM readyState = complete
|
// set below. called once the DOM readyState = complete
|
||||||
// this is the main() entrypoint called after all dependents load
|
// this is the main() entrypoint called after all dependents load
|
||||||
function init() {
|
function init() {
|
||||||
let stores = dataOpen('mesh', { stores: dbindex, version: 4 }).init(),
|
let stores = dataOpen('mesh', { stores: dbindex, version: 5 }).init(),
|
||||||
dark = false,
|
dark = false,
|
||||||
ortho = false,
|
ortho = false,
|
||||||
zoomrev = true,
|
zoomrev = true,
|
||||||
|
|
@ -39,11 +98,21 @@ function init() {
|
||||||
platform = space.platform,
|
platform = space.platform,
|
||||||
db = api.db = {
|
db = api.db = {
|
||||||
admin: stores.promise('admin'),
|
admin: stores.promise('admin'),
|
||||||
space: stores.promise('space')
|
documents: stores.promise('documents'),
|
||||||
|
versions: stores.promise('versions')
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const docman = api.document = createDocumentManager({
|
||||||
|
admin: db.admin,
|
||||||
|
documents: db.documents,
|
||||||
|
versions: db.versions,
|
||||||
|
maxRevisions: 200
|
||||||
|
});
|
||||||
|
db.space = docman.spaceStore;
|
||||||
|
|
||||||
// initialize the API (to avoid circular dependencies)
|
// initialize the API (to avoid circular dependencies)
|
||||||
api.init();
|
api.init();
|
||||||
|
boot_start('initializing mesh:tool');
|
||||||
|
|
||||||
// mark init time and use count
|
// mark init time and use count
|
||||||
db.admin.put("init", Date.now());
|
db.admin.put("init", Date.now());
|
||||||
|
|
@ -70,26 +139,23 @@ function init() {
|
||||||
colorX: 0xff7777, colorY: 0x7777ff },
|
colorX: 0xff7777, colorY: 0x7777ff },
|
||||||
});
|
});
|
||||||
platform.onMove(() => {
|
platform.onMove(() => {
|
||||||
// save last location and focus
|
// save camera per-document
|
||||||
db.admin.put('camera', {
|
save_camera_to_document();
|
||||||
place: space.view.save(),
|
|
||||||
focus: space.view.getFocus()
|
|
||||||
});
|
|
||||||
}, 100);
|
}, 100);
|
||||||
space.view.setZoom(zoomrev, zoomspd);
|
space.view.setZoom(zoomrev, zoomspd);
|
||||||
|
|
||||||
|
// trigger ui building
|
||||||
|
call.ui_build();
|
||||||
|
|
||||||
|
// trigger space event binding
|
||||||
|
call.space_init({ space: space, platform });
|
||||||
|
|
||||||
// reload stored space when worker is ready
|
// reload stored space when worker is ready
|
||||||
motoClient.on('ready', restore_space);
|
motoClient.on('ready', restore_space);
|
||||||
|
|
||||||
// start worker
|
// start worker
|
||||||
motoClient.start('../lib/mesh/work.js?' + version);
|
motoClient.start('../lib/mesh/work.js?' + version);
|
||||||
|
|
||||||
// trigger space event binding
|
|
||||||
call.space_init({ space: space, platform });
|
|
||||||
|
|
||||||
// trigger ui building
|
|
||||||
call.ui_build();
|
|
||||||
|
|
||||||
// hide url params
|
// hide url params
|
||||||
let wlp = window.location.pathname;
|
let wlp = window.location.pathname;
|
||||||
let mio = wlp.indexOf('/mesh/');
|
let mio = wlp.indexOf('/mesh/');
|
||||||
|
|
@ -102,32 +168,27 @@ function init() {
|
||||||
self.electron = navigator.userAgent.includes('Electron');
|
self.electron = navigator.userAgent.includes('Electron');
|
||||||
}
|
}
|
||||||
|
|
||||||
// restore space layout and view from previous session
|
function clear_workspace() {
|
||||||
async function restore_space() {
|
api.selection.clear();
|
||||||
const db_admin = api.db.admin;
|
for (let sk of api.sketch.list().slice()) {
|
||||||
|
sk.remove();
|
||||||
|
}
|
||||||
|
for (let grp of api.group.list().slice()) {
|
||||||
|
grp.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restore_workspace_from_state(cached = {}, mcache = {}) {
|
||||||
const db_space = api.db.space;
|
const db_space = api.db.space;
|
||||||
// let mcache = {};
|
|
||||||
await db_admin.get("camera")
|
|
||||||
.then(saved => {
|
|
||||||
if (saved) {
|
|
||||||
space.view.load(saved.place);
|
|
||||||
space.view.setFocus(saved.focus);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const mcache = await db_admin.get("meta") || {};
|
|
||||||
let count = 0;
|
let count = 0;
|
||||||
await db_space.iterate({ map: true }).then(cached => {
|
await Promise.resolve(cached).then(cached => {
|
||||||
const keys = [];
|
const keys = [];
|
||||||
const claimed = [];
|
const claimed = [];
|
||||||
for (let [id, data] of Object.entries(cached)) {
|
for (let [id, data] of Object.entries(cached)) {
|
||||||
// console.log({ id, data });
|
|
||||||
keys.push(id);
|
keys.push(id);
|
||||||
if (count++ === 0) {
|
if (count++ === 0) {
|
||||||
log(`restoring workspace`);
|
log(`restoring workspace`);
|
||||||
}
|
}
|
||||||
// restore object based on type
|
|
||||||
// group arrays load models they contain
|
|
||||||
// sketches are loaded by type since they're not grouped
|
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
claimed.push(id);
|
claimed.push(id);
|
||||||
let models = data
|
let models = data
|
||||||
|
|
@ -135,11 +196,11 @@ async function restore_space() {
|
||||||
claimed.push(id);
|
claimed.push(id);
|
||||||
return { id, md: cached[id] }
|
return { id, md: cached[id] }
|
||||||
})
|
})
|
||||||
.filter(r => r.md) // filter cache misses
|
.filter(r => r.md)
|
||||||
.map(r => new meshModel(r.md, r.id).applyMeta(mcache[r.id]))
|
.map(r => new meshModel(r.md, r.id).applyMeta(mcache[r.id]));
|
||||||
if (models.length) {
|
if (models.length) {
|
||||||
log(`restored ${models.length} model(s)`);
|
log(`restored ${models.length} model(s)`);
|
||||||
api.group.new(models, id).applyMeta(mcache[id])
|
api.group.new(models, id).applyMeta(mcache[id]);
|
||||||
} else {
|
} else {
|
||||||
log(`removed empty group ${id}`);
|
log(`removed empty group ${id}`);
|
||||||
db_space.remove(id);
|
db_space.remove(id);
|
||||||
|
|
@ -155,22 +216,17 @@ async function restore_space() {
|
||||||
if (keys.length) {
|
if (keys.length) {
|
||||||
log(`removing ${keys.length} unclaimed meshes`);
|
log(`removing ${keys.length} unclaimed meshes`);
|
||||||
}
|
}
|
||||||
// clear out meshes left in the space db along with their meta-data
|
|
||||||
for (let id of keys) {
|
for (let id of keys) {
|
||||||
db_space.remove(id);
|
db_space.remove(id);
|
||||||
delete mcache[id];
|
delete mcache[id];
|
||||||
}
|
}
|
||||||
// restore global cache only after objects are restored
|
|
||||||
// otherwise their setup will corrupt the cache for other restores
|
|
||||||
metaCache = mcache;
|
metaCache = mcache;
|
||||||
store_meta();
|
api.document.setMeta(metaCache);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
// restore preferences after models are restored
|
|
||||||
return api.prefs.load().then(() => {
|
return api.prefs.load().then(() => {
|
||||||
let { map } = api.prefs;
|
let { map } = api.prefs;
|
||||||
let { space, mode } = map;
|
let { space, mode } = map;
|
||||||
api.grid(space.grid);
|
api.grid(space.grid);
|
||||||
// restore selected state
|
|
||||||
let selist = space.select || [];
|
let selist = space.select || [];
|
||||||
let smodel = api.model.list().filter(m => selist.contains(m.id));
|
let smodel = api.model.list().filter(m => selist.contains(m.id));
|
||||||
let sgroup = api.group.list().filter(m => selist.contains(m.id));
|
let sgroup = api.group.list().filter(m => selist.contains(m.id));
|
||||||
|
|
@ -179,14 +235,38 @@ async function restore_space() {
|
||||||
let tgroup = api.group.list().filter(m => tolist.contains(m.id));
|
let tgroup = api.group.list().filter(m => tolist.contains(m.id));
|
||||||
let sklist = api.sketch.list().filter(s => selist.contains(s.id));
|
let sklist = api.sketch.list().filter(s => selist.contains(s.id));
|
||||||
api.selection.set([...smodel, ...sgroup, ...sklist], [...tmodel, ...tgroup]);
|
api.selection.set([...smodel, ...sgroup, ...sklist], [...tmodel, ...tgroup]);
|
||||||
// restore edit mode
|
|
||||||
api.mode[mode]();
|
api.mode[mode]();
|
||||||
// restore dark mode
|
set_darkmode();
|
||||||
set_darkmode(map.space.dark);
|
|
||||||
});
|
});
|
||||||
}).finally(() => {
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// restore space layout and view from previous session
|
||||||
|
async function restore_space() {
|
||||||
|
const db_admin = api.db.admin;
|
||||||
|
const db_space = api.db.space;
|
||||||
|
const docman = api.document;
|
||||||
|
boot_status('loading document');
|
||||||
|
const currentDoc = await docman.restoreOrCreate();
|
||||||
|
const mcache = docman.getMeta() || {};
|
||||||
|
const oldCamera = await db_admin.get("camera");
|
||||||
|
const docCamera = get_doc_meta(mcache).camera || oldCamera || null;
|
||||||
|
boot_status('restoring workspace');
|
||||||
|
const cached = await db_space.iterate({ map: true }) || {};
|
||||||
|
docman.pause();
|
||||||
|
try {
|
||||||
|
await restore_workspace_from_state(cached, mcache);
|
||||||
|
} finally {
|
||||||
|
docman.resume();
|
||||||
|
}
|
||||||
|
boot_status('restoring view');
|
||||||
|
apply_camera_state(docCamera);
|
||||||
|
space.update();
|
||||||
|
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||||
|
boot_status('finalizing');
|
||||||
|
Promise.resolve().finally(() => {
|
||||||
// hide loading curtain
|
// hide loading curtain
|
||||||
$d('curtain','none');
|
boot_done();
|
||||||
// restore handles visibility
|
// restore handles visibility
|
||||||
handles.setEnabled(api.prefs.map.space.bounds ?? false);
|
handles.setEnabled(api.prefs.map.space.bounds ?? false);
|
||||||
// restore script if was showing
|
// restore script if was showing
|
||||||
|
|
@ -196,15 +276,103 @@ async function restore_space() {
|
||||||
if (api.prefs.map.info.welcome !== false) {
|
if (api.prefs.map.info.welcome !== false) {
|
||||||
api.welcome(version);
|
api.welcome(version);
|
||||||
}
|
}
|
||||||
|
api.file.set_doc_name(currentDoc?.name || 'Untitled');
|
||||||
broker.publish("app_ready");
|
broker.publish("app_ready");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function document_new(opt = {}) {
|
||||||
|
const docman = api.document;
|
||||||
|
await docman.flush();
|
||||||
|
await docman.commit('document.autosave', 'document.autosave');
|
||||||
|
docman.pause();
|
||||||
|
try {
|
||||||
|
clear_workspace();
|
||||||
|
space.view.home();
|
||||||
|
metaCache = {
|
||||||
|
[DOC_META_KEY]: {
|
||||||
|
camera: capture_camera_state()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await docman.create(opt.name || 'Untitled');
|
||||||
|
docman.setMeta(metaCache);
|
||||||
|
} finally {
|
||||||
|
docman.resume();
|
||||||
|
}
|
||||||
|
await docman.commit('document.new', 'document.new');
|
||||||
|
api.file.set_doc_name(docman.current?.name || 'Untitled');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function document_open(opt = {}) {
|
||||||
|
const docman = api.document;
|
||||||
|
const id = String(opt?.id || '');
|
||||||
|
if (!id) return;
|
||||||
|
await docman.flush();
|
||||||
|
await docman.open(id, { autosave: opt.autosave !== false });
|
||||||
|
const cached = docman.getSpace() || {};
|
||||||
|
const mcache = docman.getMeta() || {};
|
||||||
|
docman.pause();
|
||||||
|
try {
|
||||||
|
clear_workspace();
|
||||||
|
await restore_workspace_from_state(cached, mcache);
|
||||||
|
} finally {
|
||||||
|
docman.resume();
|
||||||
|
}
|
||||||
|
apply_camera_state(get_doc_meta(mcache).camera);
|
||||||
|
api.file.set_doc_name(docman.current?.name || 'Untitled');
|
||||||
|
}
|
||||||
|
|
||||||
// add space event bindings
|
// add space event bindings
|
||||||
function space_init(data) {
|
function space_init(data) {
|
||||||
let platcolor = 0x00ff00;
|
let platcolor = 0x00ff00;
|
||||||
let { space, platform } = data;
|
let { space, platform } = data;
|
||||||
let { selection } = api;
|
let { selection } = api;
|
||||||
|
|
||||||
|
function selection_or_visible_entities() {
|
||||||
|
const selected = api.selection.list(true);
|
||||||
|
if (selected?.length) return selected;
|
||||||
|
return [
|
||||||
|
...api.group.list().filter(g => g.visible()),
|
||||||
|
...api.sketch.list().filter(s => s.visible())
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function fit_visible() {
|
||||||
|
const entities = selection_or_visible_entities();
|
||||||
|
const objects = entities.map(e => e?.object).filter(o => o);
|
||||||
|
return space.view.fit(undefined, {
|
||||||
|
padding: 1,
|
||||||
|
visibleOnly: true,
|
||||||
|
objects: objects.length ? objects : undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function focus_visible() {
|
||||||
|
const entities = selection_or_visible_entities();
|
||||||
|
if (entities.length) {
|
||||||
|
return api.focus(entities);
|
||||||
|
}
|
||||||
|
return api.focus([
|
||||||
|
...api.group.list(),
|
||||||
|
...api.sketch.list()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function norm_code(evt) {
|
||||||
|
if (evt?.code) return evt.code;
|
||||||
|
const key = evt?.key;
|
||||||
|
if (!key) return '';
|
||||||
|
if (key === ' ') return 'Space';
|
||||||
|
if (key === 'Spacebar') return 'Space';
|
||||||
|
if (key === 'Escape') return 'Escape';
|
||||||
|
if (key.length === 1) {
|
||||||
|
const up = key.toUpperCase();
|
||||||
|
if (up >= 'A' && up <= 'Z') return `Key${up}`;
|
||||||
|
if (up >= '0' && up <= '9') return `Digit${up}`;
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
// add file drop handler
|
// add file drop handler
|
||||||
space.event.addHandlers(self, [
|
space.event.addHandlers(self, [
|
||||||
'drop', (evt) => {
|
'drop', (evt) => {
|
||||||
|
|
@ -222,6 +390,10 @@ function space_init(data) {
|
||||||
'dragleave', evt => {
|
'dragleave', evt => {
|
||||||
platform.set({ opacity: 0, color: platcolor });
|
platform.set({ opacity: 0, color: platcolor });
|
||||||
},
|
},
|
||||||
|
// camera interactions (orbit/pan/dolly) are not guaranteed to trigger platform.onMove
|
||||||
|
'wheel', () => schedule_camera_save(),
|
||||||
|
'mouseup', () => schedule_camera_save(),
|
||||||
|
'touchend', () => schedule_camera_save(),
|
||||||
'keypress', evt => {
|
'keypress', evt => {
|
||||||
if (api.modal.showing) {
|
if (api.modal.showing) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -229,7 +401,8 @@ function space_init(data) {
|
||||||
if (evt.key === '?') {
|
if (evt.key === '?') {
|
||||||
return api.welcome(version);
|
return api.welcome(version);
|
||||||
}
|
}
|
||||||
let { shiftKey, metaKey, ctrlKey, code, target } = evt;
|
let { shiftKey, metaKey, ctrlKey, target } = evt;
|
||||||
|
let code = norm_code(evt);
|
||||||
if (target.nodeName === 'TEXTAREA') {
|
if (target.nodeName === 'TEXTAREA') {
|
||||||
api.script.changed();
|
api.script.changed();
|
||||||
return;
|
return;
|
||||||
|
|
@ -250,7 +423,11 @@ function space_init(data) {
|
||||||
case 'KeyB':
|
case 'KeyB':
|
||||||
return selection.boundsBox({toggle:true});
|
return selection.boundsBox({toggle:true});
|
||||||
case 'KeyC':
|
case 'KeyC':
|
||||||
return selection.centerXY().focus();
|
if (shiftKey) {
|
||||||
|
return selection.floor();
|
||||||
|
} else {
|
||||||
|
return selection.centerXY().focus();
|
||||||
|
}
|
||||||
case 'KeyD':
|
case 'KeyD':
|
||||||
return shiftKey && api.tool.duplicate();
|
return shiftKey && api.tool.duplicate();
|
||||||
case 'KeyE':
|
case 'KeyE':
|
||||||
|
|
@ -259,14 +436,14 @@ function space_init(data) {
|
||||||
return api.sketch.extrude();
|
return api.sketch.extrude();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case 'KeyF':
|
|
||||||
return shiftKey ? selection.focus() : selection.floor().focus();
|
|
||||||
case 'KeyG':
|
case 'KeyG':
|
||||||
return shiftKey ?
|
return shiftKey ?
|
||||||
(api.mode.is([ api.modes.sketch ]) ? api.sketch.arrange.group() : api.tool.regroup()) :
|
(api.mode.is([ api.modes.sketch ]) ? api.sketch.arrange.group() : api.tool.regroup()) :
|
||||||
api.grid();
|
api.grid();
|
||||||
case 'KeyH':
|
case 'KeyH':
|
||||||
return shiftKey ? selection.hide() : space.view.home();
|
if (shiftKey) return selection.hide();
|
||||||
|
schedule_camera_save(180);
|
||||||
|
return space.view.home();
|
||||||
case 'KeyI':
|
case 'KeyI':
|
||||||
return api.file.import();
|
return api.file.import();
|
||||||
case 'KeyL':
|
case 'KeyL':
|
||||||
|
|
@ -283,7 +460,9 @@ function space_init(data) {
|
||||||
if (!api.mode.is([ api.modes.object ])) return;
|
if (!api.mode.is([ api.modes.object ])) return;
|
||||||
return shiftKey ? selection.visible({toggle:true}) : meshSplit.start();
|
return shiftKey ? selection.visible({toggle:true}) : meshSplit.start();
|
||||||
case 'KeyT':
|
case 'KeyT':
|
||||||
return shiftKey ? api.tool.triangulate() : space.view.top();
|
if (shiftKey) return api.tool.triangulate();
|
||||||
|
schedule_camera_save(180);
|
||||||
|
return space.view.top();
|
||||||
case 'KeyU':
|
case 'KeyU':
|
||||||
return shiftKey && api.tool.union();
|
return shiftKey && api.tool.union();
|
||||||
case 'KeyV':
|
case 'KeyV':
|
||||||
|
|
@ -295,7 +474,9 @@ function space_init(data) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'keydown', evt => {
|
'keydown', evt => {
|
||||||
let { shiftKey, metaKey, ctrlKey, code, target } = evt;
|
let { shiftKey, metaKey, ctrlKey, target } = evt;
|
||||||
|
let code = norm_code(evt);
|
||||||
|
const key = evt?.key;
|
||||||
if (target.nodeName === 'TEXTAREA') {
|
if (target.nodeName === 'TEXTAREA') {
|
||||||
if (code === 'Tab') {
|
if (code === 'Tab') {
|
||||||
estop(evt);
|
estop(evt);
|
||||||
|
|
@ -320,14 +501,36 @@ function space_init(data) {
|
||||||
delete keyOnce[code];
|
delete keyOnce[code];
|
||||||
return once(evt);
|
return once(evt);
|
||||||
}
|
}
|
||||||
let rv = (Math.PI / 12);
|
|
||||||
if (api.modal.showing) {
|
if (api.modal.showing) {
|
||||||
if (code === 'Escape') {
|
if (code === 'Escape') {
|
||||||
api.modal.cancel();
|
api.modal.cancel();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let rot, floor = api.prefs.map.space.floor !== false;
|
const isFit = code === 'KeyF' ||
|
||||||
|
key === 'f' ||
|
||||||
|
key === 'F';
|
||||||
|
if (isFit && !(metaKey || ctrlKey)) {
|
||||||
|
estop(evt);
|
||||||
|
const rv = shiftKey ? focus_visible() : fit_visible();
|
||||||
|
schedule_camera_save(220);
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
const isSpace = code === 'Space' ||
|
||||||
|
code === 'Spacebar' ||
|
||||||
|
key === ' ' ||
|
||||||
|
key === 'Spacebar';
|
||||||
|
if (isSpace) {
|
||||||
|
if (selection.clear()) {
|
||||||
|
meshEdges.clear();
|
||||||
|
meshSplit.end();
|
||||||
|
}
|
||||||
|
estop(evt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let rv = (Math.PI / 12);
|
||||||
|
let rot;
|
||||||
|
let floor = api.prefs.map.space.floor !== false;
|
||||||
switch (code) {
|
switch (code) {
|
||||||
case 'KeyA':
|
case 'KeyA':
|
||||||
estop(evt);
|
estop(evt);
|
||||||
|
|
@ -353,7 +556,9 @@ function space_init(data) {
|
||||||
if (metaKey || ctrlKey) {
|
if (metaKey || ctrlKey) {
|
||||||
return shiftKey ? api.history.redo() : api.history.undo();
|
return shiftKey ? api.history.redo() : api.history.undo();
|
||||||
} else {
|
} else {
|
||||||
return space.view.reset();
|
space.view.reset();
|
||||||
|
schedule_camera_save(220);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
case 'Escape':
|
case 'Escape':
|
||||||
if (selection.clear()) {
|
if (selection.clear()) {
|
||||||
|
|
@ -664,7 +869,7 @@ function key_once_cancel(code) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function store_meta() {
|
function store_meta() {
|
||||||
api.db.admin.put("meta", metaCache);
|
api.document?.setMeta?.(metaCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
function update_meta(id, data) {
|
function update_meta(id, data) {
|
||||||
|
|
@ -694,28 +899,19 @@ function object_destroy(id) {
|
||||||
function set_darkmode(dark) {
|
function set_darkmode(dark) {
|
||||||
let { prefs, model } = api;
|
let { prefs, model } = api;
|
||||||
let { sky, platform } = space;
|
let { sky, platform } = space;
|
||||||
prefs.map.space.dark = dark;
|
dark = true;
|
||||||
if (dark) {
|
prefs.map.space.dark = true;
|
||||||
materials.wireframe.color.set(0xaaaaaa);
|
materials.wireframe.color.set(0xaaaaaa);
|
||||||
materials.wireline.color.set(0xaaaaaa);
|
materials.wireline.color.set(0xaaaaaa);
|
||||||
$('app').classList.add('dark');
|
|
||||||
} else {
|
|
||||||
materials.wireframe.color.set(0,0,0);
|
|
||||||
materials.wireline.color.set(0,0,0);
|
|
||||||
$('app').classList.remove('dark');
|
|
||||||
}
|
|
||||||
sky.set({
|
sky.set({
|
||||||
color: dark ? 0 : 0xffffff,
|
color: 0,
|
||||||
ambient: { intensity: dark ? 0.55 : 1.1 }
|
ambient: { intensity: 0.55 }
|
||||||
});
|
});
|
||||||
platform.set({
|
platform.set({
|
||||||
light: dark ? 0.08 : 0.08,
|
light: 0.08,
|
||||||
grid: dark ? {
|
grid: {
|
||||||
colorMajor: 0x666666,
|
colorMajor: 0x666666,
|
||||||
colorMinor: 0x333333,
|
colorMinor: 0x333333,
|
||||||
} : {
|
|
||||||
colorMajor: 0xcccccc,
|
|
||||||
colorMinor: 0xeeeeee,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
api.updateFog();
|
api.updateFog();
|
||||||
|
|
@ -741,11 +937,8 @@ function set_normals_length(length) {
|
||||||
function set_normals_color(color) {
|
function set_normals_color(color) {
|
||||||
let { prefs, model } = api;
|
let { prefs, model } = api;
|
||||||
let { map } = prefs;
|
let { map } = prefs;
|
||||||
if (map.space.dark) {
|
map.normals.color_dark = color || 0;
|
||||||
map.normals.color_dark = color || 0;
|
map.normals.color_lite = color || 0;
|
||||||
} else {
|
|
||||||
map.normals.color_lite = color || 0;
|
|
||||||
}
|
|
||||||
prefs.save();
|
prefs.save();
|
||||||
// Update existing normals
|
// Update existing normals
|
||||||
for (let m of model.list()) {
|
for (let m of model.list()) {
|
||||||
|
|
@ -809,7 +1002,9 @@ broker.listeners({
|
||||||
set_surface_radius,
|
set_surface_radius,
|
||||||
set_wireframe_opacity,
|
set_wireframe_opacity,
|
||||||
set_wireframe_fog,
|
set_wireframe_fog,
|
||||||
set_snap_value
|
set_snap_value,
|
||||||
|
document_new,
|
||||||
|
document_open
|
||||||
});
|
});
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
@ -837,5 +1032,7 @@ export {
|
||||||
set_surface_radius,
|
set_surface_radius,
|
||||||
set_wireframe_opacity,
|
set_wireframe_opacity,
|
||||||
set_wireframe_fog,
|
set_wireframe_fog,
|
||||||
set_snap_value
|
set_snap_value,
|
||||||
|
document_new,
|
||||||
|
document_open
|
||||||
};
|
};
|
||||||
|
|
|
||||||
329
src/main/void.js
Normal file
329
src/main/void.js
Normal file
|
|
@ -0,0 +1,329 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
import '../add/array.js';
|
||||||
|
import '../add/class.js';
|
||||||
|
import '../add/three.js';
|
||||||
|
|
||||||
|
import { $ } from '../moto/webui.js';
|
||||||
|
import { api } from '../void/api.js';
|
||||||
|
import { space } from '../moto/space.js';
|
||||||
|
import { open as dataOpen } from '../data/index.js';
|
||||||
|
import { toolbar } from '../void/toolbar.js';
|
||||||
|
import { tree } from '../void/tree.js';
|
||||||
|
import { overlay } from '../void/overlay.js';
|
||||||
|
import { datum } from '../void/datum.js';
|
||||||
|
import { interact } from '../void/interact.js';
|
||||||
|
import { properties } from '../void/properties.js';
|
||||||
|
import { ViewCube } from '../void/viewcube.js';
|
||||||
|
import { initSketchConstraintsSolver } from '../void/sketch/constraints.js';
|
||||||
|
|
||||||
|
const version = '0.1.0';
|
||||||
|
const dbindex = ["admin", "documents", "versions"];
|
||||||
|
const VOID_HOME_LEFT = Math.PI / 4;
|
||||||
|
// Match view direction along the test line vector (1,1,1) toward origin.
|
||||||
|
const VOID_HOME_UP = Math.acos(1 / Math.sqrt(3));
|
||||||
|
const LEFT_PANEL_WIDTH_KEY = 'left_panel_width';
|
||||||
|
const LEFT_PANEL_MIN_PX = 190;
|
||||||
|
const VIEWPORT_MIN_PX = 320;
|
||||||
|
|
||||||
|
function setupLeftPanelResize(db) {
|
||||||
|
const content = $('content');
|
||||||
|
const left = $('left-panel');
|
||||||
|
const container = $('container');
|
||||||
|
if (!content || !left || !container) return;
|
||||||
|
|
||||||
|
let handle = $('left-panel-resizer');
|
||||||
|
if (!handle) {
|
||||||
|
handle = document.createElement('div');
|
||||||
|
handle.id = 'left-panel-resizer';
|
||||||
|
handle.title = 'Resize tree panel';
|
||||||
|
content.insertBefore(handle, container);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clampWidth = width => {
|
||||||
|
const maxByLayout = Math.max(LEFT_PANEL_MIN_PX, (content.clientWidth || 1200) - VIEWPORT_MIN_PX);
|
||||||
|
const max = Math.min(640, maxByLayout);
|
||||||
|
return Math.max(LEFT_PANEL_MIN_PX, Math.min(max, Number(width) || LEFT_PANEL_MIN_PX));
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyWidth = width => {
|
||||||
|
const w = clampWidth(width);
|
||||||
|
left.style.flex = `0 0 ${w}px`;
|
||||||
|
left.style.width = `${w}px`;
|
||||||
|
return w;
|
||||||
|
};
|
||||||
|
|
||||||
|
const persistWidth = width => {
|
||||||
|
const w = applyWidth(width);
|
||||||
|
try { localStorage.setItem(LEFT_PANEL_WIDTH_KEY, String(w)); } catch {}
|
||||||
|
db?.admin?.put?.(LEFT_PANEL_WIDTH_KEY, w);
|
||||||
|
};
|
||||||
|
|
||||||
|
const restoreLocal = () => {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(LEFT_PANEL_WIDTH_KEY);
|
||||||
|
if (raw !== null) {
|
||||||
|
const parsed = Number(raw);
|
||||||
|
if (Number.isFinite(parsed)) applyWidth(parsed);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
|
||||||
|
restoreLocal();
|
||||||
|
db?.admin?.get?.(LEFT_PANEL_WIDTH_KEY).then(width => {
|
||||||
|
if (Number.isFinite(width)) applyWidth(width);
|
||||||
|
});
|
||||||
|
|
||||||
|
let dragging = false;
|
||||||
|
let startX = 0;
|
||||||
|
let startW = 0;
|
||||||
|
|
||||||
|
const onMove = event => {
|
||||||
|
if (!dragging) return;
|
||||||
|
const dx = (event?.clientX || 0) - startX;
|
||||||
|
const w = applyWidth(startW + dx);
|
||||||
|
space.update();
|
||||||
|
try { localStorage.setItem(LEFT_PANEL_WIDTH_KEY, String(w)); } catch {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUp = event => {
|
||||||
|
if (!dragging) return;
|
||||||
|
dragging = false;
|
||||||
|
document.body.classList.remove('left-panel-resizing');
|
||||||
|
window.removeEventListener('mousemove', onMove);
|
||||||
|
window.removeEventListener('mouseup', onUp);
|
||||||
|
const dx = (event?.clientX || 0) - startX;
|
||||||
|
persistWidth(startW + dx);
|
||||||
|
space.update();
|
||||||
|
};
|
||||||
|
|
||||||
|
handle.onmousedown = event => {
|
||||||
|
if (event.button !== 0) return;
|
||||||
|
dragging = true;
|
||||||
|
startX = event.clientX || 0;
|
||||||
|
startW = left.getBoundingClientRect().width;
|
||||||
|
document.body.classList.add('left-panel-resizing');
|
||||||
|
window.addEventListener('mousemove', onMove);
|
||||||
|
window.addEventListener('mouseup', onUp);
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onResize = () => {
|
||||||
|
applyWidth(left.getBoundingClientRect().width);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', onResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main initialization function
|
||||||
|
async function init() {
|
||||||
|
console.log({ void_form_init: version });
|
||||||
|
|
||||||
|
// Initialize IndexedDB
|
||||||
|
let stores = dataOpen('void', { stores: dbindex, version: 2 }).init();
|
||||||
|
let db = api.db = {
|
||||||
|
admin: stores.promise('admin'),
|
||||||
|
documents: stores.promise('documents'),
|
||||||
|
versions: stores.promise('versions')
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mark init time and use count
|
||||||
|
db.admin.put("init", Date.now());
|
||||||
|
db.admin.get("uses").then(v => db.admin.put("uses", (v || 0) + 1));
|
||||||
|
|
||||||
|
// Initialize API
|
||||||
|
api.init();
|
||||||
|
await initSketchConstraintsSolver();
|
||||||
|
await api.solids.init();
|
||||||
|
|
||||||
|
// Setup 3D workspace
|
||||||
|
space.setAntiAlias(true);
|
||||||
|
// Void owns its own keymap (Onshape-style); disable space.js defaults.
|
||||||
|
space.useDefaultKeys(false);
|
||||||
|
// Default void to orthographic (CAD-like), while saved camera projection
|
||||||
|
// restoration below can still override per-document/session.
|
||||||
|
space.init($('container'), delta => {}, true);
|
||||||
|
api.sketchRuntime.init(space.world);
|
||||||
|
api.solids.attach(space.world);
|
||||||
|
|
||||||
|
// Initialize 2D overlay system
|
||||||
|
overlay.init();
|
||||||
|
|
||||||
|
// Initialize datum planes
|
||||||
|
const datumGroup = datum.init({ size: 200, visible: true });
|
||||||
|
space.world.add(datumGroup);
|
||||||
|
|
||||||
|
// Add datum labels to overlay
|
||||||
|
datum.updateLabels(overlay);
|
||||||
|
|
||||||
|
// Hook overlay to update datum labels on camera movement
|
||||||
|
overlay.onUpdate = () => {
|
||||||
|
datum.updateLabels(overlay);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize interaction system (hover, select, drag)
|
||||||
|
interact.init();
|
||||||
|
api.document.bindRuntimeObservers();
|
||||||
|
|
||||||
|
// Initialize ViewCube navigation widget
|
||||||
|
const viewcube = new ViewCube({
|
||||||
|
size: 80, // Size in pixels
|
||||||
|
padding: 20, // Padding from corner
|
||||||
|
cubeSize: 1.5 // 3D cube size
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register viewcube to render after main scene
|
||||||
|
space.afterRender((renderer) => {
|
||||||
|
viewcube.render(renderer);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure sky and platform
|
||||||
|
space.sky.set({
|
||||||
|
grid: false,
|
||||||
|
color: 0x101010
|
||||||
|
});
|
||||||
|
|
||||||
|
space.view.setCtrl('void');
|
||||||
|
// Rebind overlay camera/control hooks after Orbit -> Trackball swap.
|
||||||
|
overlay.onProjectionChanged();
|
||||||
|
space.view.setFitVisibleOnly(true);
|
||||||
|
space.view.setHome(VOID_HOME_LEFT, VOID_HOME_UP);
|
||||||
|
|
||||||
|
space.platform.set({
|
||||||
|
visible: false,
|
||||||
|
size: { width: 1000, depth: 1000, height: 0 },
|
||||||
|
zoom: { reverse: true, speed: 1 },
|
||||||
|
grid: {
|
||||||
|
disabled: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enable camera-aligned tracking plane for drag operations
|
||||||
|
space.tracking.setMode('camera-aligned');
|
||||||
|
space.tracking.setDistance(10000); // Far behind camera to catch all rays
|
||||||
|
|
||||||
|
// Save camera position on movement
|
||||||
|
space.platform.onMove(() => {
|
||||||
|
db.admin.put('camera', {
|
||||||
|
place: space.view.save(),
|
||||||
|
focus: space.view.getFocus(),
|
||||||
|
projection: space.view.getProjection()
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Restore saved camera position
|
||||||
|
db.admin.get('camera').then(cam => {
|
||||||
|
if (cam && cam.place) {
|
||||||
|
if (cam.projection && cam.projection !== space.view.getProjection()) {
|
||||||
|
space.view.setProjection(cam.projection);
|
||||||
|
space.view.setCtrl('void');
|
||||||
|
overlay.onProjectionChanged();
|
||||||
|
toolbar.updateProjectionLabel();
|
||||||
|
}
|
||||||
|
space.view.load(cam.place);
|
||||||
|
if (cam.focus) {
|
||||||
|
space.view.setFocus(cam.focus);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use void-specific default home view when no saved camera exists.
|
||||||
|
space.view.home();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build UI components
|
||||||
|
toolbar.build();
|
||||||
|
toolbar.updateProjectionLabel();
|
||||||
|
properties.init();
|
||||||
|
tree.build();
|
||||||
|
setupLeftPanelResize(db);
|
||||||
|
|
||||||
|
// Document history hotkeys: Cmd/Ctrl+Z, Cmd/Ctrl+Shift+Z, Cmd/Ctrl+Y
|
||||||
|
window.addEventListener('keydown', async event => {
|
||||||
|
const isMeta = event.metaKey || event.ctrlKey;
|
||||||
|
if (!isMeta) return;
|
||||||
|
|
||||||
|
const activeTag = document.activeElement?.tagName;
|
||||||
|
const editing = activeTag === 'INPUT' || activeTag === 'TEXTAREA' || document.activeElement?.isContentEditable;
|
||||||
|
if (editing) return;
|
||||||
|
|
||||||
|
const key = event.key.toLowerCase();
|
||||||
|
let handled = false;
|
||||||
|
|
||||||
|
if (key === 'z' && event.shiftKey) {
|
||||||
|
handled = await api.document.redo();
|
||||||
|
} else if (key === 'z') {
|
||||||
|
handled = await api.document.undo();
|
||||||
|
} else if (key === 'y') {
|
||||||
|
handled = await api.document.redo();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handled) {
|
||||||
|
event.preventDefault();
|
||||||
|
toolbar.updateDocumentTitle();
|
||||||
|
tree.render();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure scene redraw when app state changes from non-canvas UI interactions
|
||||||
|
// (tree toggles, toolbar actions, property edits, etc.).
|
||||||
|
window.addEventListener('void-state-change', () => {
|
||||||
|
space.update();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep rendering responsive for keyboard-driven interactions even when
|
||||||
|
// the pointer is not over the canvas and idle-throttling is active.
|
||||||
|
window.addEventListener('keydown', event => {
|
||||||
|
const activeTag = document.activeElement?.tagName;
|
||||||
|
const editing = activeTag === 'INPUT' || activeTag === 'TEXTAREA' || document.activeElement?.isContentEditable;
|
||||||
|
if (!editing) {
|
||||||
|
space.update();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restore last active document, or seed a new blank one.
|
||||||
|
await api.document.restoreOrCreate();
|
||||||
|
api.geometryStore?.seedFromDocument?.(api.document.current);
|
||||||
|
api.sketchRuntime.sync();
|
||||||
|
await api.solids.rebuild('startup');
|
||||||
|
api.geometryStore?.seedFromDocument?.(api.document.current);
|
||||||
|
toolbar.updateDocumentTitle();
|
||||||
|
tree.render();
|
||||||
|
|
||||||
|
const { THREE } = window;
|
||||||
|
|
||||||
|
// Show overlay
|
||||||
|
overlay.show();
|
||||||
|
|
||||||
|
// Add origin
|
||||||
|
overlay.add('origin-point', 'point', {
|
||||||
|
pos3d: new THREE.Vector3(0, 0, 0),
|
||||||
|
radius: 4.8,
|
||||||
|
color: 'rgba(140, 140, 140, 0.45)',
|
||||||
|
stroke: '#5a9fd4',
|
||||||
|
strokeWidth: 2
|
||||||
|
});
|
||||||
|
api.origin.syncOverlayPoint();
|
||||||
|
|
||||||
|
// update canvas based on left panel size
|
||||||
|
space.event.onResize();
|
||||||
|
|
||||||
|
// Hide loading curtain
|
||||||
|
const curtain = $('curtain');
|
||||||
|
if (curtain) {
|
||||||
|
curtain.style.opacity = '0';
|
||||||
|
curtain.style.transition = 'opacity 0.3s';
|
||||||
|
setTimeout(() => {
|
||||||
|
curtain.style.display = 'none';
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log({ void_form_ready: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for DOM ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
115
src/mesh/api.js
115
src/mesh/api.js
|
|
@ -972,6 +972,93 @@ let add = {
|
||||||
};
|
};
|
||||||
|
|
||||||
let file = {
|
let file = {
|
||||||
|
set_doc_name(name = 'Untitled') {
|
||||||
|
const label = String(name || 'Untitled').trim() || 'Untitled';
|
||||||
|
document.title = `${label} | Mesh:Tool`;
|
||||||
|
const el = $('top-doc-name');
|
||||||
|
if (el) {
|
||||||
|
el.textContent = label;
|
||||||
|
el.title = `click to rename (${label})`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async new() {
|
||||||
|
await api.document?.flush?.();
|
||||||
|
await call.document_new({ name: 'Untitled' });
|
||||||
|
api.file.set_doc_name('Untitled');
|
||||||
|
},
|
||||||
|
|
||||||
|
async open() {
|
||||||
|
const docs = await (api.document?.list?.() || Promise.resolve([]));
|
||||||
|
const rows = docs.map(doc => h.div({ class: "doc-open-row", onclick: async function() {
|
||||||
|
api.modal.hide();
|
||||||
|
await call.document_open({ id: doc.id });
|
||||||
|
api.file.set_doc_name(doc.name || 'Untitled');
|
||||||
|
} }, [
|
||||||
|
h.div({ class: "doc-open-name", _: `${doc.name || 'Untitled'}` }),
|
||||||
|
h.button({ _: "rename", onclick(evt) {
|
||||||
|
evt?.stopPropagation?.();
|
||||||
|
api.modal.hide();
|
||||||
|
setTimeout(() => api.file.rename(doc), 0);
|
||||||
|
} }),
|
||||||
|
h.button({ class: "doc-open-del", _: "×", title: "delete", onclick: async (evt) => {
|
||||||
|
evt?.stopPropagation?.();
|
||||||
|
await api.file.delete(doc);
|
||||||
|
} })
|
||||||
|
]));
|
||||||
|
api.modal.dialog({
|
||||||
|
title: "open document",
|
||||||
|
body: [ h.div({ class: "doc-open-list" }, [
|
||||||
|
...rows,
|
||||||
|
rows.length ? undefined : h.div({ class: "doc-open-empty", _: "no documents" }),
|
||||||
|
h.hr(),
|
||||||
|
h.button({ class: "doc-open-new", _: "new", onclick() {
|
||||||
|
api.modal.hide();
|
||||||
|
api.file.new();
|
||||||
|
} })
|
||||||
|
].filter(v => v)) ]
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async rename(doc = api.document?.current) {
|
||||||
|
const current = doc || api.document?.current;
|
||||||
|
if (!current?.id) return;
|
||||||
|
if (api.modal?.showing) {
|
||||||
|
api.modal.hide();
|
||||||
|
await Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
let onclick = onkeydown = (ev) => {
|
||||||
|
if (!tempedit || (ev.code && ev.code !== 'Enter')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
api.document.rename(current.id, tempedit.value).then(rec => {
|
||||||
|
if (rec?.id && api.document?.current?.id === rec.id) {
|
||||||
|
api.file.set_doc_name(rec.name || 'Untitled');
|
||||||
|
}
|
||||||
|
}).finally(() => api.modal.hide());
|
||||||
|
};
|
||||||
|
|
||||||
|
let { tempedit } = api.modal.show(`rename document`, h.div({ class: "rename"}, [
|
||||||
|
h.input({ id: "tempedit", value: current.name || 'Untitled', onkeydown }),
|
||||||
|
h.button({ _: 'ok', onclick })
|
||||||
|
]));
|
||||||
|
tempedit.setSelectionRange(0,1000);
|
||||||
|
tempedit.focus();
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete(doc = api.document?.current) {
|
||||||
|
const current = doc || api.document?.current;
|
||||||
|
if (!current?.id) return;
|
||||||
|
const result = await api.document?.delete?.(current.id);
|
||||||
|
if (result?.switched && result?.current?.id) {
|
||||||
|
await call.document_open({ id: result.current.id, autosave: false });
|
||||||
|
api.file.set_doc_name(result.current.name || 'Untitled');
|
||||||
|
}
|
||||||
|
api.modal.hide();
|
||||||
|
api.file.open();
|
||||||
|
},
|
||||||
|
|
||||||
import() {
|
import() {
|
||||||
// binding created in mesh.build
|
// binding created in mesh.build
|
||||||
$('import').click();
|
$('import').click();
|
||||||
|
|
@ -1348,7 +1435,7 @@ const mode = {
|
||||||
$(`mode-${key}`).classList.remove('selected');
|
$(`mode-${key}`).classList.remove('selected');
|
||||||
}
|
}
|
||||||
$(`mode-${mode}`).classList.add('selected');
|
$(`mode-${mode}`).classList.add('selected');
|
||||||
$('mode-label').innerText = mode;
|
$('top-mode-label').innerText = mode;
|
||||||
api.mode.check();
|
api.mode.check();
|
||||||
meshEdges?.end();
|
meshEdges?.end();
|
||||||
if (mode === 'sketch') {
|
if (mode === 'sketch') {
|
||||||
|
|
@ -1572,10 +1659,30 @@ const api = {
|
||||||
|
|
||||||
history: {
|
history: {
|
||||||
undo() {
|
undo() {
|
||||||
history.undo();
|
if (api.document?.undo) {
|
||||||
|
api.document.undo().then(changed => {
|
||||||
|
if (changed) {
|
||||||
|
call.document_open({ id: api.document.current?.id, autosave: false });
|
||||||
|
} else {
|
||||||
|
history.undo();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
history.undo();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
redo() {
|
redo() {
|
||||||
history.redo();
|
if (api.document?.redo) {
|
||||||
|
api.document.redo().then(changed => {
|
||||||
|
if (changed) {
|
||||||
|
call.document_open({ id: api.document.current?.id, autosave: false });
|
||||||
|
} else {
|
||||||
|
history.redo();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
history.redo();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -1701,6 +1808,8 @@ const api = {
|
||||||
|
|
||||||
sketch,
|
sketch,
|
||||||
|
|
||||||
|
space: motoSpace,
|
||||||
|
|
||||||
tool,
|
tool,
|
||||||
|
|
||||||
isDebug: self.debug === true
|
isDebug: self.debug === true
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,10 @@ let deg = Math.PI / 180;
|
||||||
let und = undefined;
|
let und = undefined;
|
||||||
|
|
||||||
broker.listeners({
|
broker.listeners({
|
||||||
ui_build
|
ui_build,
|
||||||
|
app_ready() {
|
||||||
|
api.file?.set_doc_name?.(api.document?.current?.name || 'Untitled');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let spin_timer;
|
let spin_timer;
|
||||||
|
|
@ -240,14 +243,10 @@ api.welcome = function(version = "unknown") {
|
||||||
api.settings = function() {
|
api.settings = function() {
|
||||||
const { prefs } = api;
|
const { prefs } = api;
|
||||||
const { surface, normals, space, sketch, wireframe } = prefs.map;
|
const { surface, normals, space, sketch, wireframe } = prefs.map;
|
||||||
const { dark } = space;
|
const dark = true;
|
||||||
|
|
||||||
const set1 = div([
|
const set1 = div([
|
||||||
label('dark mode'),
|
label({ class: "header", _: 'auto'}),
|
||||||
input({ type: "checkbox",
|
|
||||||
onchange: ev => call.set_darkmode(ev.target.checked),
|
|
||||||
[ dark ? 'checked' : 'unchecked' ] : 1
|
|
||||||
}),
|
|
||||||
label('auto floor'),
|
label('auto floor'),
|
||||||
input({ type: "checkbox",
|
input({ type: "checkbox",
|
||||||
onchange: ev => prefs.save( space.floor = !space.floor ),
|
onchange: ev => prefs.save( space.floor = !space.floor ),
|
||||||
|
|
@ -391,20 +390,27 @@ function ui_build() {
|
||||||
|
|
||||||
// top left drop menus
|
// top left drop menus
|
||||||
bind($('top-left'), [
|
bind($('top-left'), [
|
||||||
|
div({ _: 'Mesh:Tool', class: "title" }),
|
||||||
|
div({ class: "menubar-separator" }),
|
||||||
div({ class: "menu" }, [
|
div({ class: "menu" }, [
|
||||||
div('File'),
|
div('File'),
|
||||||
div({ class: "menu-items" }, [
|
div({ class: "menu-items" }, [
|
||||||
input({
|
input({
|
||||||
id: "import", type: "file", class: ["hide"], multiple: true, accept:".stl,.obj",
|
id: "import", type: "file", class: ["hide"], multiple: true, accept:".stl,.obj,.svg,.png",
|
||||||
onchange(evt) { broker.send.load_files(evt.target.files) }
|
onchange(evt) { broker.send.load_files(evt.target.files) }
|
||||||
}),
|
}),
|
||||||
|
menu_item('New', file.new),
|
||||||
|
menu_item('Open', file.open),
|
||||||
|
hr(),
|
||||||
menu_item('Import', file.import, 'I'),
|
menu_item('Import', file.import, 'I'),
|
||||||
menu_item('Export', file.export, 'X'),
|
menu_item('Export', file.export, 'X'),
|
||||||
hr(),
|
hr(),
|
||||||
menu_item('Slicer', api.kirimoto),
|
menu_item('Slicer', api.kirimoto),
|
||||||
menu_item('Script', api.script.toggle),
|
menu_item('Script', api.script.toggle),
|
||||||
hr(),
|
hr(),
|
||||||
menu_item('Close', window.close),
|
menu_item('Preferences', api.settings, 'Q'),
|
||||||
|
hr(),
|
||||||
|
menu_item('Close', () => window.close() || api.kirimoto()),
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
div({ class: "menu sketch-on" }, [
|
div({ class: "menu sketch-on" }, [
|
||||||
|
|
@ -467,7 +473,6 @@ function ui_build() {
|
||||||
menu_item('Face', mode.face, '5', 'mode-face'),
|
menu_item('Face', mode.face, '5', 'mode-face'),
|
||||||
menu_item('Edge', mode.edge, '6', 'mode-edge'),
|
menu_item('Edge', mode.edge, '6', 'mode-edge'),
|
||||||
]),
|
]),
|
||||||
div({ id: "mode-label" })
|
|
||||||
]),
|
]),
|
||||||
div({ class: "menu sketch-on" }, [
|
div({ class: "menu sketch-on" }, [
|
||||||
div('Items'),
|
div('Items'),
|
||||||
|
|
@ -510,7 +515,7 @@ function ui_build() {
|
||||||
div({ class: "menu sketch-off" }, [
|
div({ class: "menu sketch-off" }, [
|
||||||
div('Faces'),
|
div('Faces'),
|
||||||
div({ class: "menu-items" }, [
|
div({ class: "menu-items" }, [
|
||||||
menu_item('Flip Normals', tool.invert, ['bi-shift','I']),
|
menu_item('Flip Normals', tool.invert, ['bi-shift','N']),
|
||||||
menu_item('Triangulate', tool.triangulate, ['bi-shift','T']),
|
menu_item('Triangulate', tool.triangulate, ['bi-shift','T']),
|
||||||
menu_item('To Sketch', tool.toSketch),
|
menu_item('To Sketch', tool.toSketch),
|
||||||
hr(),
|
hr(),
|
||||||
|
|
@ -535,6 +540,7 @@ function ui_build() {
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
div({ class: "menu" }, [
|
div({ class: "menu" }, [
|
||||||
|
// div({ class: "fas fa-question" }),
|
||||||
div('Help'),
|
div('Help'),
|
||||||
div({ class: "menu-items" }, [
|
div({ class: "menu-items" }, [
|
||||||
menu_item('About', () => { api.welcome(version) }),
|
menu_item('About', () => { api.welcome(version) }),
|
||||||
|
|
@ -546,15 +552,15 @@ function ui_build() {
|
||||||
menu_item('Versions', api.version),
|
menu_item('Versions', api.version),
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
|
div({ class: "menubar-separator" }),
|
||||||
|
div({ id: "top-mode-label" }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// add help buttons
|
// add help buttons
|
||||||
bind($('top-right'), [
|
bind($('top-right'), [
|
||||||
div({ id: "top-settings", onclick: api.settings }, [
|
div({ id: "top-doc-name", onclick: () => api.file.rename(), _: 'Untitled' }),
|
||||||
div({ class: "fas fa-gear" }),
|
|
||||||
div('Settings')
|
|
||||||
]),
|
|
||||||
]);
|
]);
|
||||||
|
api.file?.set_doc_name?.(api.document?.current?.name || 'Untitled');
|
||||||
|
|
||||||
// modal dialog and page blocker
|
// modal dialog and page blocker
|
||||||
bind($('modal_page'), [
|
bind($('modal_page'), [
|
||||||
|
|
@ -600,32 +606,55 @@ function ui_build() {
|
||||||
return div({ onclick: fn, class: "tool" }, [ bicon(icon), div([ label(help) ]) ]);
|
return div({ onclick: fn, class: "tool" }, [ bicon(icon), div([ label(help) ]) ]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toolbar_separator() {
|
||||||
|
return div({ class: "toolbar-separator" });
|
||||||
|
}
|
||||||
|
|
||||||
// bind sketch chiclets
|
// bind sketch chiclets
|
||||||
bind(sketchtools, div([
|
bind(sketchtools, div([
|
||||||
tool_item('bi-plus', 'New Sketch', add.sketch),
|
tool_item('bi-plus', 'New Sketch', add.sketch),
|
||||||
|
toolbar_separator(),
|
||||||
tool_item('bi-circle', 'Add Circle', api.add.circle),
|
tool_item('bi-circle', 'Add Circle', api.add.circle),
|
||||||
|
toolbar_separator(),
|
||||||
tool_item('bi-square', 'Add Rectangle', api.add.rectangle),
|
tool_item('bi-square', 'Add Rectangle', api.add.rectangle),
|
||||||
|
toolbar_separator(),
|
||||||
tool_item('bi-symmetry-vertical', 'Flip Horizontal', api.sketch.arrange.fliph),
|
tool_item('bi-symmetry-vertical', 'Flip Horizontal', api.sketch.arrange.fliph),
|
||||||
|
toolbar_separator(),
|
||||||
tool_item('bi-symmetry-horizontal', 'Flip Vertical', api.sketch.arrange.flipv),
|
tool_item('bi-symmetry-horizontal', 'Flip Vertical', api.sketch.arrange.flipv),
|
||||||
|
toolbar_separator(),
|
||||||
tool_item('bi-arrow-clockwise', 'Rotate', api.sketch.arrange.rotate),
|
tool_item('bi-arrow-clockwise', 'Rotate', api.sketch.arrange.rotate),
|
||||||
|
toolbar_separator(),
|
||||||
tool_item('bi-union', 'Union', sketch.boolean.union),
|
tool_item('bi-union', 'Union', sketch.boolean.union),
|
||||||
|
toolbar_separator(),
|
||||||
tool_item('bi-intersect', 'Intersect', sketch.boolean.intersect),
|
tool_item('bi-intersect', 'Intersect', sketch.boolean.intersect),
|
||||||
|
toolbar_separator(),
|
||||||
tool_item('bi-exclude', 'Difference', sketch.boolean.difference),
|
tool_item('bi-exclude', 'Difference', sketch.boolean.difference),
|
||||||
|
toolbar_separator(),
|
||||||
tool_item('bi-pip', 'Nest', sketch.boolean.nest),
|
tool_item('bi-pip', 'Nest', sketch.boolean.nest),
|
||||||
|
toolbar_separator(),
|
||||||
tool_item('bi-layers', 'Flatten', sketch.boolean.flatten),
|
tool_item('bi-layers', 'Flatten', sketch.boolean.flatten),
|
||||||
|
toolbar_separator(),
|
||||||
tool_item('bi-cookie', 'Even Odd', sketch.boolean.evenodd),
|
tool_item('bi-cookie', 'Even Odd', sketch.boolean.evenodd),
|
||||||
|
toolbar_separator(),
|
||||||
tool_item('bi-arrow-bar-up', 'Extrude', () => sketch.extrude()),
|
tool_item('bi-arrow-bar-up', 'Extrude', () => sketch.extrude()),
|
||||||
]));
|
]));
|
||||||
|
|
||||||
// bind object chiclets
|
// bind object chiclets
|
||||||
bind(objecttools, div([
|
bind(objecttools, div([
|
||||||
tool_item('bi-pencil', 'New Sketch', add.sketch),
|
tool_item('bi-pencil', 'New Sketch', add.sketch),
|
||||||
|
toolbar_separator(),
|
||||||
tool_item('bi-box', 'New Cube', add.cube),
|
tool_item('bi-box', 'New Cube', add.cube),
|
||||||
|
toolbar_separator(),
|
||||||
tool_item('bi-database', 'New Cylinder', add.cylinder),
|
tool_item('bi-database', 'New Cylinder', add.cylinder),
|
||||||
|
toolbar_separator(),
|
||||||
tool_item('bi-gear', 'New Gear', add.gear),
|
tool_item('bi-gear', 'New Gear', add.gear),
|
||||||
|
toolbar_separator(),
|
||||||
tool_item('bi-union', 'Union', tool.union),
|
tool_item('bi-union', 'Union', tool.union),
|
||||||
|
toolbar_separator(),
|
||||||
tool_item('bi-subtract', 'Subtract', tool.subtract),
|
tool_item('bi-subtract', 'Subtract', tool.subtract),
|
||||||
|
toolbar_separator(),
|
||||||
tool_item('bi-intersect', 'Intersect', tool.intersect),
|
tool_item('bi-intersect', 'Intersect', tool.intersect),
|
||||||
|
toolbar_separator(),
|
||||||
tool_item('bi-exclude', 'Difference', tool.difference),
|
tool_item('bi-exclude', 'Difference', tool.difference),
|
||||||
]));
|
]));
|
||||||
|
|
||||||
|
|
|
||||||
316
src/mesh/document.js
Normal file
316
src/mesh/document.js
Normal file
|
|
@ -0,0 +1,316 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
function uid() {
|
||||||
|
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||||||
|
return crypto.randomUUID().replace(/-/g, '').slice(0, 12);
|
||||||
|
}
|
||||||
|
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clone(data) {
|
||||||
|
if (data === undefined || data === null) return data;
|
||||||
|
if (typeof structuredClone === 'function') {
|
||||||
|
return structuredClone(data);
|
||||||
|
}
|
||||||
|
return JSON.parse(JSON.stringify(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
function title(name = '') {
|
||||||
|
const clean = String(name || '').trim();
|
||||||
|
return clean || 'Untitled';
|
||||||
|
}
|
||||||
|
|
||||||
|
function revKey(docId, revId) {
|
||||||
|
return `${docId}:${revId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDocumentManager({ admin, documents, versions, maxRevisions = 200 }) {
|
||||||
|
const state = {
|
||||||
|
admin,
|
||||||
|
documents,
|
||||||
|
versions,
|
||||||
|
maxRevisions,
|
||||||
|
doc: null,
|
||||||
|
space: {},
|
||||||
|
meta: {},
|
||||||
|
pauseDepth: 0,
|
||||||
|
commitTimer: null,
|
||||||
|
commitDelay: 350
|
||||||
|
};
|
||||||
|
|
||||||
|
function paused() {
|
||||||
|
return state.pauseDepth > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveDoc() {
|
||||||
|
if (!state.doc) return;
|
||||||
|
await state.documents.put(state.doc.id, clone(state.doc));
|
||||||
|
await state.admin.put('current_document_id', state.doc.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSnapshot(doc, revId = null) {
|
||||||
|
if (!doc) {
|
||||||
|
state.space = {};
|
||||||
|
state.meta = {};
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const rid = revId || doc.cursor_rev || doc.head_rev || null;
|
||||||
|
if (!rid) {
|
||||||
|
state.space = {};
|
||||||
|
state.meta = {};
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const rec = await state.versions.get(revKey(doc.id, rid));
|
||||||
|
const snap = rec?.snapshot || {};
|
||||||
|
state.space = clone(snap.space || {});
|
||||||
|
state.meta = clone(snap.meta || {});
|
||||||
|
return rec || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function maybePruneRevisions() {
|
||||||
|
const order = Array.isArray(state.doc?.rev_order) ? state.doc.rev_order : [];
|
||||||
|
const over = order.length - state.maxRevisions;
|
||||||
|
if (over <= 0) return;
|
||||||
|
const purge = order.splice(0, over);
|
||||||
|
for (const rid of purge) {
|
||||||
|
await state.versions.remove(revKey(state.doc.id, rid));
|
||||||
|
}
|
||||||
|
if (state.doc.cursor_rev && !order.includes(state.doc.cursor_rev)) {
|
||||||
|
state.doc.cursor_rev = order[0] || null;
|
||||||
|
}
|
||||||
|
if (state.doc.head_rev && !order.includes(state.doc.head_rev)) {
|
||||||
|
state.doc.head_rev = order[order.length - 1] || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function commit(op_type = 'autosave', label = 'autosave') {
|
||||||
|
if (!state.doc || paused()) return null;
|
||||||
|
const order = Array.isArray(state.doc.rev_order) ? state.doc.rev_order.slice() : [];
|
||||||
|
const cursor = state.doc.cursor_rev || null;
|
||||||
|
const cursorIndex = cursor ? order.indexOf(cursor) : -1;
|
||||||
|
if (cursorIndex >= 0 && cursorIndex < order.length - 1) {
|
||||||
|
const remove = order.slice(cursorIndex + 1);
|
||||||
|
for (const rid of remove) {
|
||||||
|
await state.versions.remove(revKey(state.doc.id, rid));
|
||||||
|
}
|
||||||
|
order.length = cursorIndex + 1;
|
||||||
|
}
|
||||||
|
const parent = order.length ? order[order.length - 1] : null;
|
||||||
|
const rid = uid();
|
||||||
|
await state.versions.put(revKey(state.doc.id, rid), {
|
||||||
|
doc_id: state.doc.id,
|
||||||
|
rev_id: rid,
|
||||||
|
parent_rev: parent,
|
||||||
|
created_at: Date.now(),
|
||||||
|
op_type,
|
||||||
|
label,
|
||||||
|
snapshot: {
|
||||||
|
space: clone(state.space),
|
||||||
|
meta: clone(state.meta)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
order.push(rid);
|
||||||
|
state.doc.rev_order = order;
|
||||||
|
state.doc.head_rev = rid;
|
||||||
|
state.doc.cursor_rev = rid;
|
||||||
|
state.doc.updated_at = Date.now();
|
||||||
|
await maybePruneRevisions();
|
||||||
|
await saveDoc();
|
||||||
|
return rid;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleCommit(op_type = 'autosave', label = 'autosave') {
|
||||||
|
if (!state.doc || paused()) return;
|
||||||
|
clearTimeout(state.commitTimer);
|
||||||
|
state.commitTimer = setTimeout(() => {
|
||||||
|
state.commitTimer = null;
|
||||||
|
commit(op_type, label).catch(error => console.trace(error));
|
||||||
|
}, state.commitDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createDoc(name = 'Untitled') {
|
||||||
|
const id = uid();
|
||||||
|
const now = Date.now();
|
||||||
|
state.doc = {
|
||||||
|
id,
|
||||||
|
name: title(name),
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
head_rev: null,
|
||||||
|
cursor_rev: null,
|
||||||
|
rev_order: []
|
||||||
|
};
|
||||||
|
state.space = {};
|
||||||
|
state.meta = {};
|
||||||
|
await commit('document.create', 'document.create');
|
||||||
|
return clone(state.doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restoreOrCreate() {
|
||||||
|
let docId = await state.admin.get('current_document_id');
|
||||||
|
let doc = docId ? await state.documents.get(docId) : null;
|
||||||
|
if (!doc) {
|
||||||
|
const listed = await state.documents.iterate({ map: true }) || {};
|
||||||
|
const docs = Object.values(listed);
|
||||||
|
if (docs.length) {
|
||||||
|
docs.sort((a, b) => Number(b.updated_at || 0) - Number(a.updated_at || 0));
|
||||||
|
doc = docs[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!doc) {
|
||||||
|
return createDoc('Untitled');
|
||||||
|
}
|
||||||
|
state.doc = clone(doc);
|
||||||
|
await loadSnapshot(state.doc);
|
||||||
|
await saveDoc();
|
||||||
|
return clone(state.doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function open(docId, { autosave = true } = {}) {
|
||||||
|
if (autosave) {
|
||||||
|
await flush();
|
||||||
|
await commit('document.autosave', 'document.autosave');
|
||||||
|
}
|
||||||
|
const next = await state.documents.get(docId);
|
||||||
|
if (!next) throw new Error(`document ${docId} missing`);
|
||||||
|
state.doc = clone(next);
|
||||||
|
await loadSnapshot(state.doc);
|
||||||
|
await saveDoc();
|
||||||
|
return clone(state.doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rename(docId, name) {
|
||||||
|
const doc = await state.documents.get(docId);
|
||||||
|
if (!doc) return null;
|
||||||
|
doc.name = title(name);
|
||||||
|
doc.updated_at = Date.now();
|
||||||
|
await state.documents.put(doc.id, doc);
|
||||||
|
if (state.doc?.id === doc.id) {
|
||||||
|
state.doc = clone(doc);
|
||||||
|
await state.admin.put('current_document_id', doc.id);
|
||||||
|
}
|
||||||
|
return clone(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function list() {
|
||||||
|
const map = await state.documents.iterate({ map: true }) || {};
|
||||||
|
return Object.values(map)
|
||||||
|
.sort((a, b) => Number(b.updated_at || 0) - Number(a.updated_at || 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove(docId) {
|
||||||
|
const id = String(docId || '');
|
||||||
|
if (!id) return { deleted: false, switched: false, current: clone(state.doc) };
|
||||||
|
await flush();
|
||||||
|
const doc = await state.documents.get(id);
|
||||||
|
if (!doc) return { deleted: false, switched: false, current: clone(state.doc) };
|
||||||
|
const revs = Array.isArray(doc.rev_order) ? doc.rev_order : [];
|
||||||
|
for (const rid of revs) {
|
||||||
|
await state.versions.remove(revKey(id, rid));
|
||||||
|
}
|
||||||
|
await state.documents.remove(id);
|
||||||
|
if (state.doc?.id !== id) {
|
||||||
|
return { deleted: true, switched: false, current: clone(state.doc) };
|
||||||
|
}
|
||||||
|
const map = await state.documents.iterate({ map: true }) || {};
|
||||||
|
const docs = Object.values(map).sort((a, b) => Number(b.updated_at || 0) - Number(a.updated_at || 0));
|
||||||
|
if (!docs.length) {
|
||||||
|
await createDoc('Untitled');
|
||||||
|
return { deleted: true, switched: true, current: clone(state.doc) };
|
||||||
|
}
|
||||||
|
state.doc = clone(docs[0]);
|
||||||
|
await loadSnapshot(state.doc);
|
||||||
|
await saveDoc();
|
||||||
|
return { deleted: true, switched: true, current: clone(state.doc) };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function undo() {
|
||||||
|
await flush();
|
||||||
|
if (!state.doc) return false;
|
||||||
|
const order = Array.isArray(state.doc.rev_order) ? state.doc.rev_order : [];
|
||||||
|
if (order.length < 2) return false;
|
||||||
|
const idx = order.indexOf(state.doc.cursor_rev);
|
||||||
|
if (idx <= 0) return false;
|
||||||
|
state.doc.cursor_rev = order[idx - 1];
|
||||||
|
state.doc.updated_at = Date.now();
|
||||||
|
await loadSnapshot(state.doc, state.doc.cursor_rev);
|
||||||
|
await saveDoc();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function redo() {
|
||||||
|
await flush();
|
||||||
|
if (!state.doc) return false;
|
||||||
|
const order = Array.isArray(state.doc.rev_order) ? state.doc.rev_order : [];
|
||||||
|
const idx = order.indexOf(state.doc.cursor_rev);
|
||||||
|
if (idx < 0 || idx >= order.length - 1) return false;
|
||||||
|
state.doc.cursor_rev = order[idx + 1];
|
||||||
|
state.doc.updated_at = Date.now();
|
||||||
|
await loadSnapshot(state.doc, state.doc.cursor_rev);
|
||||||
|
await saveDoc();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function flush() {
|
||||||
|
if (state.commitTimer) {
|
||||||
|
clearTimeout(state.commitTimer);
|
||||||
|
state.commitTimer = null;
|
||||||
|
await commit('autosave.flush', 'autosave.flush');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const spaceStore = {
|
||||||
|
async put(key, value) {
|
||||||
|
state.space[String(key)] = clone(value);
|
||||||
|
scheduleCommit('space.put', `space.put:${key}`);
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
async remove(key) {
|
||||||
|
delete state.space[String(key)];
|
||||||
|
scheduleCommit('space.remove', `space.remove:${key}`);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
async get(key) {
|
||||||
|
return clone(state.space[String(key)]);
|
||||||
|
},
|
||||||
|
async iterate(opt = {}) {
|
||||||
|
if (opt?.map) {
|
||||||
|
return clone(state.space);
|
||||||
|
}
|
||||||
|
return Object.entries(state.space || {});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
spaceStore,
|
||||||
|
pause() {
|
||||||
|
state.pauseDepth++;
|
||||||
|
},
|
||||||
|
resume() {
|
||||||
|
state.pauseDepth = Math.max(0, state.pauseDepth - 1);
|
||||||
|
},
|
||||||
|
setMeta(meta = {}) {
|
||||||
|
state.meta = clone(meta || {});
|
||||||
|
scheduleCommit('meta.set', 'meta.set');
|
||||||
|
},
|
||||||
|
getMeta() {
|
||||||
|
return clone(state.meta || {});
|
||||||
|
},
|
||||||
|
getSpace() {
|
||||||
|
return clone(state.space || {});
|
||||||
|
},
|
||||||
|
get current() {
|
||||||
|
return clone(state.doc);
|
||||||
|
},
|
||||||
|
restoreOrCreate,
|
||||||
|
create: createDoc,
|
||||||
|
open,
|
||||||
|
rename,
|
||||||
|
delete: remove,
|
||||||
|
list,
|
||||||
|
commit,
|
||||||
|
flush,
|
||||||
|
undo,
|
||||||
|
redo
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
import { THREE } from '../ext/three.js';
|
import { THREE } from '../ext/three.js';
|
||||||
const { Matrix4, Matrix3, Vector3, Box3 } = THREE;
|
const { Matrix4, Matrix3, Vector3, Box3 } = THREE;
|
||||||
import { license as motoLicense } from '../moto/license.js';
|
|
||||||
|
|
||||||
// geometry helper functions
|
// geometry helper functions
|
||||||
const geom = {
|
const geom = {
|
||||||
|
|
|
||||||
|
|
@ -929,7 +929,7 @@ class MeshTool {
|
||||||
}
|
}
|
||||||
|
|
||||||
pitch = p.round(3);
|
pitch = p.round(3);
|
||||||
geom.log(`gear pitch radius: ${pitch}`);
|
log(`gear pitch radius: ${pitch}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { gear, pitch };
|
return { gear, pitch };
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,8 @@ function log(msg) {
|
||||||
return worker.publish("mesh.log", msg);
|
return worker.publish("mesh.log", msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.log = log;
|
||||||
|
|
||||||
function cacheUpdate(id, data) {
|
function cacheUpdate(id, data) {
|
||||||
return Object.assign(cache[id], data);
|
return Object.assign(cache[id], data);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@
|
||||||
const terms = {
|
const terms = {
|
||||||
COPYRIGHT: "Copyright (C) Stewart Allen <sa@grid.space> - All Rights Reserved",
|
COPYRIGHT: "Copyright (C) Stewart Allen <sa@grid.space> - All Rights Reserved",
|
||||||
LICENSE: "See the license.md file included with the source distribution",
|
LICENSE: "See the license.md file included with the source distribution",
|
||||||
VERSION: "4.6.0"
|
VERSION: "4.7.0"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const beta = 4600;
|
export const beta = 0;
|
||||||
export const license = terms;
|
export const license = terms;
|
||||||
export const version = terms.VERSION;
|
export const version = terms.VERSION;
|
||||||
|
|
|
||||||
121
src/moto/opfs.js
Normal file
121
src/moto/opfs.js
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
const root = await navigator.storage?.getDirectory();
|
||||||
|
|
||||||
|
function resolvePath(path) {
|
||||||
|
if (!path) {
|
||||||
|
return [];
|
||||||
|
} else if (typeof path === 'string') {
|
||||||
|
path = path.trim();
|
||||||
|
while (path.charAt(0) === '/') {
|
||||||
|
path = path.substring(1);
|
||||||
|
}
|
||||||
|
return path.length ? path.split('/') : [];
|
||||||
|
} else if (Array.isArray(path)) {
|
||||||
|
return path;
|
||||||
|
} else {
|
||||||
|
throw "invalid path value";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dirHandle(path, options = { create: true }) {
|
||||||
|
path = resolvePath(path);
|
||||||
|
if (path.length === 0) {
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
let dir = root;
|
||||||
|
for (let tok of path) {
|
||||||
|
try {
|
||||||
|
dir = await dir.getDirectoryHandle(tok, options);
|
||||||
|
} catch (error) {
|
||||||
|
if (options.report) {
|
||||||
|
console.log({ path, error });
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fileHandle(path, options = { create: false }) {
|
||||||
|
path = resolvePath(path);
|
||||||
|
if (path.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
let target = path.pop();
|
||||||
|
let dir = await dirHandle(path);
|
||||||
|
return dir.getFileHandle(target, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clear() {
|
||||||
|
for await (let name of root.keys()) {
|
||||||
|
await root.removeEntry(name, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function remove(path) {
|
||||||
|
path = resolvePath(path);
|
||||||
|
if (path.length === 0) {
|
||||||
|
return clear();
|
||||||
|
}
|
||||||
|
let target = path.pop();
|
||||||
|
let dir = await dirHandle(path);
|
||||||
|
console.log({ dir, target });
|
||||||
|
await dir.removeEntry(target, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function entries(path) {
|
||||||
|
return (await dirHandle(path)).entries();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function values(path) {
|
||||||
|
return (await dirHandle(path)).values();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function keys(path) {
|
||||||
|
return (await dirHandle(path)).keys();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function tree(path) {
|
||||||
|
path = resolvePath(path);
|
||||||
|
let dir = await dirHandle(path);
|
||||||
|
return treeFrom(dir, path, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function treeFrom(dir, path, list) {
|
||||||
|
let entries = await dir.entries()
|
||||||
|
for await (let [ name, handle ] of entries) {
|
||||||
|
path.push(name);
|
||||||
|
if (handle.kind === 'directory') {
|
||||||
|
await treeFrom(handle, path, list);
|
||||||
|
} else {
|
||||||
|
list.push(path.join('/'));
|
||||||
|
}
|
||||||
|
path.pop();
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getText(path) {
|
||||||
|
let handle = await fileHandle(path);
|
||||||
|
let file = await handle.getFile();
|
||||||
|
return file.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function putText(path, text) {
|
||||||
|
let handle = await fileHandle(path, { create: true });
|
||||||
|
let stream = await handle.createWritable();
|
||||||
|
await stream.write(text);
|
||||||
|
return stream.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OPFS = {
|
||||||
|
dirHandle,
|
||||||
|
fileHandle,
|
||||||
|
entries,
|
||||||
|
keys,
|
||||||
|
values,
|
||||||
|
tree,
|
||||||
|
clear,
|
||||||
|
remove,
|
||||||
|
getText,
|
||||||
|
putText
|
||||||
|
};
|
||||||
|
|
@ -85,6 +85,13 @@ class Orbit extends EventDispatcher {
|
||||||
PAN: MOUSE.MIDDLE
|
PAN: MOUSE.MIDDLE
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Onshape
|
||||||
|
this.mouseVoid = {
|
||||||
|
ORBIT: MOUSE.RIGHT,
|
||||||
|
// ZOOM: MOUSE.LEFT,
|
||||||
|
PAN: MOUSE.MIDDLE
|
||||||
|
};
|
||||||
|
|
||||||
this.mouseButtons = this.mouseDefault;
|
this.mouseButtons = this.mouseDefault;
|
||||||
|
|
||||||
this.setMouse = function(bindings) {
|
this.setMouse = function(bindings) {
|
||||||
|
|
@ -116,6 +123,7 @@ class Orbit extends EventDispatcher {
|
||||||
pan = new Vector3(),
|
pan = new Vector3(),
|
||||||
lastPosition = new Vector3(),
|
lastPosition = new Vector3(),
|
||||||
lastQuaternion = new Quaternion(),
|
lastQuaternion = new Quaternion(),
|
||||||
|
lastZoom = object.zoom !== undefined ? object.zoom : 1,
|
||||||
// so camera.up is the orbit axis
|
// so camera.up is the orbit axis
|
||||||
quat = new Quaternion().setFromUnitVectors(object.up, new Vector3(0, 1, 0)),
|
quat = new Quaternion().setFromUnitVectors(object.up, new Vector3(0, 1, 0)),
|
||||||
quatInverse = quat.clone().invert(),
|
quatInverse = quat.clone().invert(),
|
||||||
|
|
@ -227,15 +235,33 @@ class Orbit extends EventDispatcher {
|
||||||
this.setPosition = function(set) {
|
this.setPosition = function(set) {
|
||||||
thetaSet = firstValue([set.left, set.theta, thetaSet]);
|
thetaSet = firstValue([set.left, set.theta, thetaSet]);
|
||||||
phiSet = firstValue([set.up, set.phi, phiSet]);
|
phiSet = firstValue([set.up, set.phi, phiSet]);
|
||||||
if (set.panX !== undefined) this.target.x = set.panX;
|
let target = this.target;
|
||||||
if (set.panY !== undefined) this.target.y = set.panY;
|
let position = this.object.position;
|
||||||
if (set.panZ !== undefined) this.target.z = set.panZ;
|
if (set.posX !== undefined) position.x = set.posX;
|
||||||
|
if (set.posY !== undefined) position.y = set.posY;
|
||||||
|
if (set.posZ !== undefined) position.z = set.posZ;
|
||||||
|
if (set.panX !== undefined) target.x = set.panX;
|
||||||
|
if (set.panY !== undefined) target.y = set.panY;
|
||||||
|
if (set.panZ !== undefined) target.z = set.panZ;
|
||||||
if (set.scale !== undefined) scale = set.scale;
|
if (set.scale !== undefined) scale = set.scale;
|
||||||
|
else scale = 1;
|
||||||
|
this.update();
|
||||||
};
|
};
|
||||||
|
|
||||||
this.getPosition = function(scaled) {
|
this.getPosition = function({ scaled } = { scaled: false }) {
|
||||||
let t = this.target,
|
let t = this.target,
|
||||||
pos = { left:theta, up:phi, panX:t.x, panY:t.y, panZ:t.z, scale:scaled ? scaleSave : 1 };
|
p = this.object.position,
|
||||||
|
pos = {
|
||||||
|
left: theta,
|
||||||
|
up: phi,
|
||||||
|
panX: t.x,
|
||||||
|
panY: t.y,
|
||||||
|
panZ: t.z,
|
||||||
|
posX: p.x,
|
||||||
|
posY: p.y,
|
||||||
|
posZ: p.z,
|
||||||
|
scale: scaled ? scaleSave : undefined
|
||||||
|
};
|
||||||
return pos;
|
return pos;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -325,11 +351,13 @@ class Orbit extends EventDispatcher {
|
||||||
// min(camera displacement, camera rotation in radians)^2 > EPS
|
// min(camera displacement, camera rotation in radians)^2 > EPS
|
||||||
// using small-angle approximation cos(x/2) = 1 - x^2 / 8
|
// using small-angle approximation cos(x/2) = 1 - x^2 / 8
|
||||||
if (lastPosition.distanceToSquared(this.object.position) > EPS
|
if (lastPosition.distanceToSquared(this.object.position) > EPS
|
||||||
|| 8 * (1 - lastQuaternion.dot(this.object.quaternion)) > EPS) {
|
|| 8 * (1 - lastQuaternion.dot(this.object.quaternion)) > EPS
|
||||||
|
|| Math.abs(lastZoom - this.object.zoom) > EPS) {
|
||||||
|
|
||||||
this.dispatchEvent(changeEvent);
|
this.dispatchEvent(changeEvent);
|
||||||
lastPosition.copy(this.object.position);
|
lastPosition.copy(this.object.position);
|
||||||
lastQuaternion.copy(this.object.quaternion);
|
lastQuaternion.copy(this.object.quaternion);
|
||||||
|
lastZoom = this.object.zoom;
|
||||||
if (notify) notify(position, true);
|
if (notify) notify(position, true);
|
||||||
} else {
|
} else {
|
||||||
if (notify) notify(position, false);
|
if (notify) notify(position, false);
|
||||||
|
|
@ -362,6 +390,15 @@ class Orbit extends EventDispatcher {
|
||||||
if (scope.enabled === false) return;
|
if (scope.enabled === false) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
// keep wheel as dolly, but treat middle-button drag like right-button drag
|
||||||
|
// in default orbit bindings.
|
||||||
|
const touchSynthesized = Boolean(event?.sourceCapabilities?.firesTouchEvents);
|
||||||
|
if (!touchSynthesized
|
||||||
|
&& event.button === MOUSE.MIDDLE
|
||||||
|
&& scope.mouseButtons.ZOOM === MOUSE.MIDDLE
|
||||||
|
&& scope.mouseButtons.PAN === MOUSE.RIGHT) {
|
||||||
|
state = STATE.PAN;
|
||||||
|
} else {
|
||||||
switch (event.button) {
|
switch (event.button) {
|
||||||
case scope.mouseButtons.ORBIT:
|
case scope.mouseButtons.ORBIT:
|
||||||
state = event.metaKey ? STATE.PAN : STATE.ROTATE;
|
state = event.metaKey ? STATE.PAN : STATE.ROTATE;
|
||||||
|
|
@ -373,6 +410,7 @@ class Orbit extends EventDispatcher {
|
||||||
state = STATE.PAN;
|
state = STATE.PAN;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case STATE.ROTATE:
|
case STATE.ROTATE:
|
||||||
|
|
@ -467,14 +505,14 @@ class Orbit extends EventDispatcher {
|
||||||
if (event.wheelDelta !== undefined) {
|
if (event.wheelDelta !== undefined) {
|
||||||
// Chrome/Safari wheelDelta: scroll up = +120, scroll down = -120
|
// Chrome/Safari wheelDelta: scroll up = +120, scroll down = -120
|
||||||
// Negate to match deltaY convention
|
// Negate to match deltaY convention
|
||||||
delta = -event.wheelDelta;
|
delta = event.wheelDelta;
|
||||||
} else if (event.detail !== undefined) {
|
} else if (event.detail !== undefined) {
|
||||||
// Old Firefox DOMMouseScroll detail: scroll up = -3, scroll down = +3
|
// Old Firefox DOMMouseScroll detail: scroll up = -3, scroll down = +3
|
||||||
delta = event.detail * 40; // Normalize to pixel values
|
delta = -event.detail * 40; // Normalize to pixel values
|
||||||
} else if (event.deltaY !== undefined) {
|
} else if (event.deltaY !== undefined) {
|
||||||
// Modern browsers deltaY: scroll up = negative, scroll down = positive
|
// Modern browsers deltaY: scroll up = negative, scroll down = positive
|
||||||
// Already matches our convention
|
// Already matches our convention
|
||||||
delta = event.deltaY;
|
delta = -event.deltaY;
|
||||||
// Firefox's deltaMode indicates the unit of deltaY
|
// Firefox's deltaMode indicates the unit of deltaY
|
||||||
// DOM_DELTA_PIXEL (0x00) - pixels
|
// DOM_DELTA_PIXEL (0x00) - pixels
|
||||||
// DOM_DELTA_LINE (0x01) - lines (default for Firefox, ~3 units per notch)
|
// DOM_DELTA_LINE (0x01) - lines (default for Firefox, ~3 units per notch)
|
||||||
|
|
@ -681,7 +719,23 @@ class Orbit extends EventDispatcher {
|
||||||
|
|
||||||
this.onMouseUp = onMouseUp;
|
this.onMouseUp = onMouseUp;
|
||||||
|
|
||||||
domEl.addEventListener('contextmenu', function (event) { event.preventDefault() }, false);
|
this.dispose = function() {
|
||||||
|
domEl.removeEventListener('contextmenu', onContextMenu, false);
|
||||||
|
domEl.removeEventListener('mousedown', onMouseDown, false);
|
||||||
|
domEl.removeEventListener('wheel', onMouseWheel, false);
|
||||||
|
domEl.removeEventListener('mousewheel', onMouseWheel, false);
|
||||||
|
domEl.removeEventListener('DOMMouseScroll', onMouseWheel, false);
|
||||||
|
domEl.removeEventListener('touchstart', touchstart, false);
|
||||||
|
domEl.removeEventListener('touchend', touchend, false);
|
||||||
|
domEl.removeEventListener('touchmove', touchmove, false);
|
||||||
|
|
||||||
|
document.removeEventListener('mousemove', onMouseMove, false);
|
||||||
|
document.removeEventListener('mouseup', onMouseUp, false);
|
||||||
|
window.removeEventListener('keydown', onKeyDown, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
function onContextMenu(event) { event.preventDefault() }
|
||||||
|
domEl.addEventListener('contextmenu', onContextMenu, false);
|
||||||
domEl.addEventListener('mousedown', onMouseDown, false);
|
domEl.addEventListener('mousedown', onMouseDown, false);
|
||||||
domEl.addEventListener('wheel', onMouseWheel, false); // Modern standard (Chrome, Safari, Firefox)
|
domEl.addEventListener('wheel', onMouseWheel, false); // Modern standard (Chrome, Safari, Firefox)
|
||||||
domEl.addEventListener('mousewheel', onMouseWheel, false); // Legacy Chrome/Safari
|
domEl.addEventListener('mousewheel', onMouseWheel, false); // Legacy Chrome/Safari
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
377
src/moto/trackball.js
Normal file
377
src/moto/trackball.js
Normal file
|
|
@ -0,0 +1,377 @@
|
||||||
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
import { THREE } from '../ext/three.js';
|
||||||
|
import { TrackballControls } from '../ext/three.js';
|
||||||
|
|
||||||
|
const { MOUSE, Vector3 } = THREE;
|
||||||
|
const BUTTON = { LEFT: 0, MIDDLE: 1, RIGHT: 2 };
|
||||||
|
const ACTION = { ROTATE: 0, DOLLY: 1, PAN: 2 };
|
||||||
|
const EPS = 1e-6;
|
||||||
|
const VOID_ROTATE_SPEED = 36.0;
|
||||||
|
const VOID_PAN_SPEED_PERSPECTIVE = 0.8;
|
||||||
|
const VOID_PAN_SPEED_ORTHO = 2.0;
|
||||||
|
const VOID_ZOOM_SPEED_PERSPECTIVE_MULT = 1.35;
|
||||||
|
const VOID_ZOOM_SPEED_ORTHO_MULT = 2.2;
|
||||||
|
|
||||||
|
class Trackball {
|
||||||
|
constructor(object, domElement, notify, slider) {
|
||||||
|
this.object = object;
|
||||||
|
this.domElement = domElement !== undefined ? domElement : document;
|
||||||
|
|
||||||
|
this.control = new TrackballControls(object, this.domElement);
|
||||||
|
this.control.staticMoving = true;
|
||||||
|
this.control.dynamicDampingFactor = 0;
|
||||||
|
this.control.rotateSpeed = VOID_ROTATE_SPEED;
|
||||||
|
this.control.zoomSpeed = 2.0;
|
||||||
|
this.control.panSpeed = VOID_PAN_SPEED_PERSPECTIVE;
|
||||||
|
|
||||||
|
this.target = this.control.target;
|
||||||
|
this.center = this.target;
|
||||||
|
|
||||||
|
this.mouseDefault = {
|
||||||
|
ORBIT: MOUSE.LEFT,
|
||||||
|
ZOOM: MOUSE.MIDDLE,
|
||||||
|
PAN: MOUSE.RIGHT
|
||||||
|
};
|
||||||
|
this.mouseOnshape = {
|
||||||
|
ORBIT: MOUSE.RIGHT,
|
||||||
|
ZOOM: MOUSE.LEFT,
|
||||||
|
PAN: MOUSE.MIDDLE
|
||||||
|
};
|
||||||
|
this.mouseVoid = {
|
||||||
|
ORBIT: MOUSE.RIGHT,
|
||||||
|
PAN: MOUSE.MIDDLE
|
||||||
|
};
|
||||||
|
this.mouseButtons = this.mouseDefault;
|
||||||
|
|
||||||
|
this.orbitPivotOnRight = false;
|
||||||
|
this.continuousRotate = true;
|
||||||
|
this.reverseZoom = false;
|
||||||
|
this.zoomSpeed = 1.0;
|
||||||
|
this._keysDisabled = false;
|
||||||
|
|
||||||
|
this.isTrackballAdapter = true;
|
||||||
|
|
||||||
|
const mapButtons = () => {
|
||||||
|
const actions = {
|
||||||
|
LEFT: -1,
|
||||||
|
MIDDLE: -1,
|
||||||
|
RIGHT: -1
|
||||||
|
};
|
||||||
|
const bind = this.mouseButtons || this.mouseDefault;
|
||||||
|
const buttonToAction = Object.create(null);
|
||||||
|
if (bind.ORBIT !== undefined) buttonToAction[bind.ORBIT] = ACTION.ROTATE;
|
||||||
|
if (bind.ZOOM !== undefined) buttonToAction[bind.ZOOM] = ACTION.DOLLY;
|
||||||
|
if (bind.PAN !== undefined) buttonToAction[bind.PAN] = ACTION.PAN;
|
||||||
|
actions.LEFT = buttonToAction[BUTTON.LEFT] ?? actions.LEFT;
|
||||||
|
actions.MIDDLE = buttonToAction[BUTTON.MIDDLE] ?? actions.MIDDLE;
|
||||||
|
actions.RIGHT = buttonToAction[BUTTON.RIGHT] ?? actions.RIGHT;
|
||||||
|
this.control.mouseButtons = actions;
|
||||||
|
};
|
||||||
|
|
||||||
|
mapButtons();
|
||||||
|
|
||||||
|
const emitNotify = (moved) => {
|
||||||
|
if (notify) notify(this.object.position, moved);
|
||||||
|
};
|
||||||
|
this._emitNotify = emitNotify;
|
||||||
|
this._lastPosition = this.object.position.clone();
|
||||||
|
this._lastQuaternion = this.object.quaternion.clone();
|
||||||
|
this._lastZoom = this.object.zoom !== undefined ? this.object.zoom : 1;
|
||||||
|
|
||||||
|
// Keep Space idle-halo semantics identical to Orbit: any control change
|
||||||
|
// is treated as active camera motion.
|
||||||
|
this.control.addEventListener('change', () => {
|
||||||
|
this._emitNotify?.(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mirror Orbit semantics: always signal notify from update(), with moved=true/false.
|
||||||
|
// Patch underlying control.update() so internal handlers also flow through this path.
|
||||||
|
const rawUpdate = this.control.update.bind(this.control);
|
||||||
|
this.control.update = (...args) => {
|
||||||
|
rawUpdate(...args);
|
||||||
|
const moved =
|
||||||
|
this._lastPosition.distanceToSquared(this.object.position) > EPS
|
||||||
|
|| 8 * (1 - this._lastQuaternion.dot(this.object.quaternion)) > EPS
|
||||||
|
|| Math.abs((this.object.zoom || 1) - this._lastZoom) > EPS;
|
||||||
|
this._emitNotify?.(moved);
|
||||||
|
if (moved) {
|
||||||
|
this._lastPosition.copy(this.object.position);
|
||||||
|
this._lastQuaternion.copy(this.object.quaternion);
|
||||||
|
this._lastZoom = this.object.zoom !== undefined ? this.object.zoom : 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this._animating = false;
|
||||||
|
this._raf = null;
|
||||||
|
this._tick = () => {
|
||||||
|
if (!this._animating) return;
|
||||||
|
if (this.control.enabled) {
|
||||||
|
this.control.update();
|
||||||
|
}
|
||||||
|
this._raf = self.requestAnimationFrame(this._tick);
|
||||||
|
};
|
||||||
|
this._startTick = () => {
|
||||||
|
if (this._animating) return;
|
||||||
|
this._animating = true;
|
||||||
|
this._tick();
|
||||||
|
};
|
||||||
|
this._stopTick = () => {
|
||||||
|
this._animating = false;
|
||||||
|
if (this._raf) {
|
||||||
|
self.cancelAnimationFrame(this._raf);
|
||||||
|
this._raf = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this._onPointerDown = (event) => {
|
||||||
|
const b = event?.button;
|
||||||
|
if (b === BUTTON.LEFT || b === BUTTON.MIDDLE || b === BUTTON.RIGHT) {
|
||||||
|
this._startTick();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this._onPointerUp = () => this._stopTick();
|
||||||
|
this._onPointerCancel = () => this._stopTick();
|
||||||
|
this._onWheel = () => {
|
||||||
|
if (!this.control.enabled) return;
|
||||||
|
// Run after Trackball's wheel handler mutates zoom deltas.
|
||||||
|
self.requestAnimationFrame(() => {
|
||||||
|
if (this.control.enabled) {
|
||||||
|
this.control.update();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if (this.domElement?.addEventListener) {
|
||||||
|
this.domElement.addEventListener('pointerdown', this._onPointerDown, true);
|
||||||
|
this.domElement.addEventListener('wheel', this._onWheel, false);
|
||||||
|
}
|
||||||
|
if (this.domElement?.ownerDocument?.addEventListener) {
|
||||||
|
this.domElement.ownerDocument.addEventListener('pointerup', this._onPointerUp, true);
|
||||||
|
this.domElement.ownerDocument.addEventListener('pointercancel', this._onPointerCancel, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(this, 'enabled', {
|
||||||
|
get: () => this.control.enabled,
|
||||||
|
set: (v) => { this.control.enabled = !!v; }
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, 'noKeys', {
|
||||||
|
get: () => !!this._keysDisabled,
|
||||||
|
set: (v) => {
|
||||||
|
const next = !!v;
|
||||||
|
if (next === this._keysDisabled) return;
|
||||||
|
this._keysDisabled = next;
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
if (next) {
|
||||||
|
window.removeEventListener('keydown', this.control._onKeyDown);
|
||||||
|
window.removeEventListener('keyup', this.control._onKeyUp);
|
||||||
|
} else {
|
||||||
|
window.addEventListener('keydown', this.control._onKeyDown);
|
||||||
|
window.addEventListener('keyup', this.control._onKeyUp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, 'minDistance', {
|
||||||
|
get: () => this.control.minDistance,
|
||||||
|
set: (v) => { this.control.minDistance = v; }
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, 'maxDistance', {
|
||||||
|
get: () => this.control.maxDistance,
|
||||||
|
set: (v) => { this.control.maxDistance = v; }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setMouse(bindings) {
|
||||||
|
this.mouseButtons = bindings || this.mouseDefault;
|
||||||
|
const bind = this.mouseButtons || this.mouseDefault;
|
||||||
|
const actions = {
|
||||||
|
LEFT: -1,
|
||||||
|
MIDDLE: -1,
|
||||||
|
RIGHT: -1
|
||||||
|
};
|
||||||
|
const buttonToAction = Object.create(null);
|
||||||
|
if (bind.ORBIT !== undefined) buttonToAction[bind.ORBIT] = ACTION.ROTATE;
|
||||||
|
if (bind.ZOOM !== undefined) buttonToAction[bind.ZOOM] = ACTION.DOLLY;
|
||||||
|
if (bind.PAN !== undefined) buttonToAction[bind.PAN] = ACTION.PAN;
|
||||||
|
actions.LEFT = buttonToAction[BUTTON.LEFT] ?? actions.LEFT;
|
||||||
|
actions.MIDDLE = buttonToAction[BUTTON.MIDDLE] ?? actions.MIDDLE;
|
||||||
|
actions.RIGHT = buttonToAction[BUTTON.RIGHT] ?? actions.RIGHT;
|
||||||
|
this.control.mouseButtons = actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOrbitPivotOnRight(enabled) {
|
||||||
|
this.orbitPivotOnRight = !!enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
setContinuousRotate(enabled) {
|
||||||
|
this.continuousRotate = !!enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
setZoom(reverse, speed) {
|
||||||
|
this.reverseZoom = !!reverse;
|
||||||
|
this.zoomSpeed = speed || 1.0;
|
||||||
|
const mult = this.object.isOrthographicCamera
|
||||||
|
? VOID_ZOOM_SPEED_ORTHO_MULT
|
||||||
|
: VOID_ZOOM_SPEED_PERSPECTIVE_MULT;
|
||||||
|
this.control.zoomSpeed = this.zoomSpeed * mult;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTarget() {
|
||||||
|
return this.control.target;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTarget(t) {
|
||||||
|
this.control.target.copy(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPosition(set) {
|
||||||
|
const t = this.control.target;
|
||||||
|
if (set.panX !== undefined) t.x = set.panX;
|
||||||
|
if (set.panY !== undefined) t.y = set.panY;
|
||||||
|
if (set.panZ !== undefined) t.z = set.panZ;
|
||||||
|
|
||||||
|
const hasCamPos = Number.isFinite(set?.camX) && Number.isFinite(set?.camY) && Number.isFinite(set?.camZ);
|
||||||
|
if (hasCamPos) {
|
||||||
|
this.object.position.set(set.camX, set.camY, set.camZ);
|
||||||
|
if (Number.isFinite(set?.upX) && Number.isFinite(set?.upY) && Number.isFinite(set?.upZ)) {
|
||||||
|
const up = new Vector3(set.upX, set.upY, set.upZ);
|
||||||
|
if (up.lengthSq() > 1e-12) this.object.up.copy(up.normalize());
|
||||||
|
}
|
||||||
|
this.object.lookAt(t);
|
||||||
|
if (set.scale !== undefined && isFinite(set.scale) && set.scale > 0) {
|
||||||
|
if (this.object.isPerspectiveCamera) {
|
||||||
|
const eye = this.object.position.clone().sub(t).multiplyScalar(set.scale);
|
||||||
|
this.object.position.copy(t).add(eye);
|
||||||
|
} else if (this.object.isOrthographicCamera) {
|
||||||
|
this.object.zoom = this.object.zoom / set.scale;
|
||||||
|
this.object.updateProjectionMatrix();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let off = this.object.position.clone().sub(t);
|
||||||
|
let radius = off.length();
|
||||||
|
if (!isFinite(radius) || radius <= 0) radius = 1;
|
||||||
|
|
||||||
|
const left = set.left !== undefined ? set.left : Math.atan2(off.x, off.z);
|
||||||
|
const up = set.up !== undefined ? set.up : Math.atan2(Math.sqrt(off.x * off.x + off.z * off.z), off.y);
|
||||||
|
|
||||||
|
off = new Vector3(
|
||||||
|
radius * Math.sin(up) * Math.sin(left),
|
||||||
|
radius * Math.cos(up),
|
||||||
|
radius * Math.sin(up) * Math.cos(left)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Avoid singular lookAt matrices at poles by ensuring camera.up is not parallel to view direction.
|
||||||
|
const viewDir = off.clone().normalize();
|
||||||
|
let upVec = null;
|
||||||
|
if (Number.isFinite(set?.upX) && Number.isFinite(set?.upY) && Number.isFinite(set?.upZ)) {
|
||||||
|
upVec = new Vector3(set.upX, set.upY, set.upZ);
|
||||||
|
} else {
|
||||||
|
upVec = this.object.up.clone();
|
||||||
|
}
|
||||||
|
if (upVec.lengthSq() < 1e-12) upVec.set(0, 1, 0);
|
||||||
|
upVec.projectOnPlane(viewDir);
|
||||||
|
if (upVec.lengthSq() < 1e-8) upVec = new Vector3(0, 0, 1).projectOnPlane(viewDir);
|
||||||
|
if (upVec.lengthSq() < 1e-8) upVec = new Vector3(1, 0, 0).projectOnPlane(viewDir);
|
||||||
|
if (upVec.lengthSq() > 1e-12) this.object.up.copy(upVec.normalize());
|
||||||
|
|
||||||
|
this.object.position.copy(t).add(off);
|
||||||
|
this.object.lookAt(t);
|
||||||
|
|
||||||
|
if (set.scale !== undefined && isFinite(set.scale) && set.scale > 0) {
|
||||||
|
if (this.object.isPerspectiveCamera) {
|
||||||
|
const eye = this.object.position.clone().sub(t).multiplyScalar(set.scale);
|
||||||
|
this.object.position.copy(t).add(eye);
|
||||||
|
} else if (this.object.isOrthographicCamera) {
|
||||||
|
this.object.zoom = this.object.zoom / set.scale;
|
||||||
|
this.object.updateProjectionMatrix();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPosition(scaled) {
|
||||||
|
const t = this.control.target;
|
||||||
|
const off = this.object.position.clone().sub(t);
|
||||||
|
const left = Math.atan2(off.x, off.z);
|
||||||
|
const up = Math.atan2(Math.sqrt(off.x * off.x + off.z * off.z), off.y);
|
||||||
|
return {
|
||||||
|
left,
|
||||||
|
up,
|
||||||
|
panX: t.x,
|
||||||
|
panY: t.y,
|
||||||
|
panZ: t.z,
|
||||||
|
camX: this.object.position.x,
|
||||||
|
camY: this.object.position.y,
|
||||||
|
camZ: this.object.position.z,
|
||||||
|
upX: this.object.up.x,
|
||||||
|
upY: this.object.up.y,
|
||||||
|
upZ: this.object.up.z,
|
||||||
|
scale: scaled ? (this.object.isOrthographicCamera ? 1 / this.object.zoom : 1) : 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
this.control.panSpeed = this.object.isOrthographicCamera
|
||||||
|
? VOID_PAN_SPEED_ORTHO
|
||||||
|
: VOID_PAN_SPEED_PERSPECTIVE;
|
||||||
|
const zoomMult = this.object.isOrthographicCamera
|
||||||
|
? VOID_ZOOM_SPEED_ORTHO_MULT
|
||||||
|
: VOID_ZOOM_SPEED_PERSPECTIVE_MULT;
|
||||||
|
this.control.zoomSpeed = this.zoomSpeed * zoomMult;
|
||||||
|
this.control.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener(type, listener) {
|
||||||
|
this.control.addEventListener(type, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeEventListener(type, listener) {
|
||||||
|
this.control.removeEventListener(type, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchEvent(event) {
|
||||||
|
this.control.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.control.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseUp() {
|
||||||
|
// TrackballControls manages pointer lifecycle internally.
|
||||||
|
}
|
||||||
|
|
||||||
|
resetInputState() {
|
||||||
|
// Clear any latched key/mouse state (e.g. if native prompt swallowed keyup/mouseup)
|
||||||
|
// and ensure key listeners are restored according to noKeys policy.
|
||||||
|
this.control.state = -1;
|
||||||
|
this.control.keyState = -1;
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.removeEventListener('keydown', this.control._onKeyDown);
|
||||||
|
window.removeEventListener('keyup', this.control._onKeyUp);
|
||||||
|
if (!this._keysDisabled) {
|
||||||
|
window.addEventListener('keydown', this.control._onKeyDown);
|
||||||
|
window.addEventListener('keyup', this.control._onKeyUp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this._stopTick();
|
||||||
|
if (this.domElement?.removeEventListener) {
|
||||||
|
this.domElement.removeEventListener('pointerdown', this._onPointerDown, true);
|
||||||
|
this.domElement.removeEventListener('wheel', this._onWheel, false);
|
||||||
|
}
|
||||||
|
if (this.domElement?.ownerDocument?.removeEventListener) {
|
||||||
|
this.domElement.ownerDocument.removeEventListener('pointerup', this._onPointerUp, true);
|
||||||
|
this.domElement.ownerDocument.removeEventListener('pointercancel', this._onPointerCancel, true);
|
||||||
|
}
|
||||||
|
this.control.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Trackball };
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue