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 }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
|
||||
steps:
|
||||
- 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:
|
||||
push:
|
||||
paths:
|
||||
|
|
@ -12,12 +12,19 @@ jobs:
|
|||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
bun-version: latest
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
- name: check format for docs
|
||||
run:
|
||||
bun docs-check
|
||||
node-version: '20'
|
||||
- name: Check docs formatting (retry once on transient network failure)
|
||||
shell: bash
|
||||
run: |
|
||||
for attempt in 1 2; do
|
||||
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/quickjs.js
|
||||
src/ext/three.js
|
||||
src/void/solver
|
||||
tmp
|
||||
tmp_*/
|
||||
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;
|
||||
|
||||
// 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({
|
||||
port: 5309,
|
||||
apps: basDir,
|
||||
|
|
@ -61,12 +78,15 @@ function createWindow() {
|
|||
if (url.endsWith('/mesh') || url.endsWith('/mesh/')) {
|
||||
return;
|
||||
}
|
||||
if (url.endsWith('/void') || url.endsWith('/void/')) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
shell.openExternal(url);
|
||||
});
|
||||
|
||||
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) {
|
||||
|
|
|
|||
57
app.js
57
app.js
|
|
@ -26,6 +26,7 @@ const mods = {};
|
|||
const load = [];
|
||||
const api = {};
|
||||
|
||||
let lastTouchTime = {};
|
||||
let forceUseCache = false;
|
||||
let serviceWorker = true;
|
||||
let crossOrigin = false;
|
||||
|
|
@ -134,10 +135,12 @@ function init(mod) {
|
|||
"/boot" : redir((pre??"") + "/boot/", 301),
|
||||
"/kiri" : redir((pre??"") + "/kiri/", 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),
|
||||
"/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(fixedmap("/api/", api));
|
||||
|
|
@ -153,12 +156,15 @@ function init(mod) {
|
|||
mod.static("/lib/", "alt");
|
||||
mod.static("/lib/", "src");
|
||||
mod.static("/obj/", "web/obj");
|
||||
mod.static("/font/", "web/font");
|
||||
mod.static("/boot/", "web/boot");
|
||||
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("/moto/", "web/moto");
|
||||
mod.static("/kiri/", "web/kiri");
|
||||
mod.static("/boot/", "web/boot");
|
||||
mod.static("/void/", "web/void");
|
||||
|
||||
// module loader
|
||||
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 optional local modules
|
||||
// load optional local modules (bambu)
|
||||
load_modules('mods');
|
||||
|
||||
// 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 (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)) {
|
||||
// append mains
|
||||
let src = `${dir}/src/main/${key}.js`;
|
||||
|
|
@ -218,6 +234,14 @@ function init(mod) {
|
|||
logger.log('missing', src);
|
||||
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 });
|
||||
let body = fs.readFileSync(src);
|
||||
fs.writeFileSync(`${dir}/alt/main/${key}.js`, body + val);
|
||||
|
|
@ -234,7 +258,7 @@ function init(mod) {
|
|||
} else {
|
||||
logger.log('skipping artifacts');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// either add module assets to path or require(init.js)
|
||||
function loadModule(mod, dir) {
|
||||
|
|
@ -257,7 +281,7 @@ function initModule(mod, file, dir) {
|
|||
logger.log({ module: file, dir });
|
||||
require_fresh(file)({
|
||||
// express functions added here show up at "/api/" url root
|
||||
api: api,
|
||||
api,
|
||||
adm: {
|
||||
setver(ver) { oversion = ver },
|
||||
crossOrigin(bool) { crossOrigin = bool }
|
||||
|
|
@ -329,8 +353,10 @@ function initModule(mod, file, dir) {
|
|||
const path = mod.dir + '/' + dir + '/' + file;
|
||||
try {
|
||||
const body = fs.readFileSync(path);
|
||||
if (debug && !single) logger.log({ inject: code, file, opt });
|
||||
if (opt.first) {
|
||||
if (debug && !single) {
|
||||
logger.log({ inject: code, file, opt });
|
||||
}
|
||||
if (opt.first && append[code]) {
|
||||
append[code] = body.toString() + '\n' + append[code];
|
||||
} else {
|
||||
append[code] += body.toString() + '\n';
|
||||
|
|
@ -369,8 +395,11 @@ function handleSetup(req, res, next) {
|
|||
}
|
||||
|
||||
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/mesh/work.js' : '/lib/pack/mesh-work.js',
|
||||
'/lib/main/kiri.js' : '/lib/pack/kiri-main.js',
|
||||
'/lib/kiri/run/engine.js' : '/lib/pack/kiri-eng.js',
|
||||
'/lib/kiri/run/minion.js' : '/lib/pack/kiri-pool.js',
|
||||
|
|
@ -487,7 +516,7 @@ function ifModifiedDate(req) {
|
|||
|
||||
function addCorsHeaders(req, res) {
|
||||
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'] || '*');
|
||||
if (req.headers['access-control-request-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/gpu/raster-worker.js", "dst": "lib/gpu/raster-worker.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": [
|
||||
"src/pack",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ const isProd = mode === 'prod';
|
|||
|
||||
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_EXTRAS = [ ];
|
||||
|
||||
|
|
@ -29,7 +32,7 @@ async function appendExtraModules(extras, outfile, minify = false) {
|
|||
const result = await transform(code, {
|
||||
minify: true,
|
||||
loader: 'js',
|
||||
target: 'es2020',
|
||||
target: 'es2022',
|
||||
});
|
||||
return result.code;
|
||||
})
|
||||
|
|
@ -92,7 +95,7 @@ const rec = {
|
|||
minify: isProd, // false for dev, true for prod
|
||||
platform: 'browser',
|
||||
sourcemap: false,
|
||||
target: 'es2020',
|
||||
target: 'es2022',
|
||||
};
|
||||
|
||||
async function buildApp() {
|
||||
|
|
@ -100,6 +103,20 @@ async function buildApp() {
|
|||
// Concatenate kiri devices
|
||||
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
|
||||
await build(Object.assign({}, rec, {
|
||||
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 { LineSegments2 } from '../node_modules/three/examples/jsm/lines/LineSegments2.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';
|
||||
|
||||
|
|
@ -19,5 +20,6 @@ export {
|
|||
LineGeometry,
|
||||
LineSegments2,
|
||||
LineSegmentsGeometry,
|
||||
TrackballControls,
|
||||
MeshBVHLib
|
||||
};
|
||||
|
|
@ -1,17 +1,19 @@
|
|||
src/ext/gerber.js,../../node_modules/@tracespace/parser/umd/parser.js
|
||||
src/ext/manifold.js,../../node_modules/manifold-3d/manifold.js
|
||||
src/ext/base64.js,../../node_modules/base64-js/base64js.min.js
|
||||
src/ext/earcut.js,../../node_modules/earcut/src/earcut.js
|
||||
src/ext/tween.js,../../node_modules/@tweenjs/tween.js/src/Tween.js
|
||||
src/ext/jszip.js,../../node_modules/jszip/dist/jszip.js
|
||||
src/wasm/manifold.wasm,../../node_modules/manifold-3d/manifold.wasm
|
||||
src/kiri/lang-en.js,../../web/kiri/lang/en.js
|
||||
web/fon2,../node_modules/bootstrap-icons/font/
|
||||
web/kiri/lang/pl.js,pl-pl.js
|
||||
web/kiri/lang/pt-pt.js,pt.js
|
||||
web/kiri/lang/da-dk.js,da.js
|
||||
web/kiri/lang/en-us.js,en.js
|
||||
web/kiri/lang/fr-fr.js,fr.js
|
||||
web/kiri/lang/de.js,de-de.js
|
||||
web/kiri/lang/es.js,es-es.js
|
||||
web/font,../node_modules/@fortawesome/fontawesome-free/
|
||||
src//gpu/raster.js,../../node_modules/@gridspace/raster-path/build/raster-path.js
|
||||
src//gpu/raster-worker.js,../../node_modules/@gridspace/raster-path/build/raster-worker.js
|
||||
src//ext/gerber.js,../../node_modules/@tracespace/parser/umd/parser.js
|
||||
src//ext/manifold.js,../../node_modules/manifold-3d/manifold.js
|
||||
src//ext/base64.js,../../node_modules/base64-js/base64js.min.js
|
||||
src//ext/earcut.js,../../node_modules/earcut/src/earcut.js
|
||||
src//ext/tween.js,../../node_modules/@tweenjs/tween.js/src/Tween.js
|
||||
src//ext/jszip.js,../../node_modules/jszip/dist/jszip.js
|
||||
src//wasm/manifold.wasm,../../node_modules/manifold-3d/manifold.wasm
|
||||
src//kiri/app/lang-en.js,../../../web/kiri/lang/en.js
|
||||
web//fon2,../node_modules/bootstrap-icons/font/
|
||||
web//kiri/lang/pl.js,pl-pl.js
|
||||
web//kiri/lang/pt-pt.js,pt.js
|
||||
web//kiri/lang/da-dk.js,da.js
|
||||
web//kiri/lang/en-us.js,en.js
|
||||
web//kiri/lang/fr-fr.js,fr.js
|
||||
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/
|
||||
|
|
@ -7,7 +7,7 @@ description: Keyboard Shortcuts and Mouse Controls
|
|||
## Layer Navigation
|
||||
|
||||
| Key | Action | Notes |
|
||||
| --- | ------ | ----- |
|
||||
| --------------- | ------------------------ | ------------------------------------ |
|
||||
| ` (backtick) | Show layer 0 | First layer |
|
||||
| 1-9 | Show layers at 10-90% | In 10% increments |
|
||||
| 0 | Show all layers (100%) | Show maximum layer |
|
||||
|
|
@ -19,7 +19,7 @@ description: Keyboard Shortcuts and Mouse Controls
|
|||
## View Navigation
|
||||
|
||||
| Key | Action | Notes |
|
||||
| --- | ------ | ----- |
|
||||
| --- | -------------------- | ------------------------------------- |
|
||||
| h | Home View | Default 45° view |
|
||||
| t | Top View | Top-down view |
|
||||
| f | Front View | Direct front side-view |
|
||||
|
|
@ -33,14 +33,14 @@ description: Keyboard Shortcuts and Mouse Controls
|
|||
## File Operations
|
||||
|
||||
| Key | Action | Notes |
|
||||
| --- | ------ | ----- |
|
||||
| --- | ------------------- | ------------------------ |
|
||||
| i | File Import Dialog | Import 3D models |
|
||||
| r | Recent Files Dialog | Open recently used files |
|
||||
|
||||
## Workflow Actions
|
||||
|
||||
| Key | Action | Notes |
|
||||
| --- | ------ | ----- |
|
||||
| ----- | ------------- | ------------------------------------------------------------ |
|
||||
| s / S | Slice Object | Process workspace objects |
|
||||
| p / P | Preview Paths | Route planning (hidden in SLA mode) |
|
||||
| g | CNC Animation | Enter milling animation mode (CNC only) |
|
||||
|
|
@ -50,7 +50,7 @@ description: Keyboard Shortcuts and Mouse Controls
|
|||
## Object Manipulation
|
||||
|
||||
| Key | Action | Notes |
|
||||
| --- | ------ | ----- |
|
||||
| ---------------------- | --------------------- | ------------------------------------ |
|
||||
| d | Duplicate Selected | Create copy of selected objects |
|
||||
| m | Mirror Selected | Mirror vertices of selected objects |
|
||||
| O | Manual Rotation Input | Enter rotation values manually |
|
||||
|
|
@ -61,7 +61,7 @@ description: Keyboard Shortcuts and Mouse Controls
|
|||
## Rotation & Movement
|
||||
|
||||
| Key | Action | Notes |
|
||||
| --- | ------ | ----- |
|
||||
| --------------- | ---------- | ------------------------------------ |
|
||||
| [arrow] | Rotate 90° | Arrow keys rotate on respective axes |
|
||||
| [shift + arrow] | Rotate 5° | Fine rotation control |
|
||||
| [alt + arrow] | Move 5mm | Move object in X/Y plane |
|
||||
|
|
@ -69,7 +69,7 @@ description: Keyboard Shortcuts and Mouse Controls
|
|||
## Rendering Modes
|
||||
|
||||
| Key | Action | Notes |
|
||||
| --- | ------ | ----- |
|
||||
| -------------- | ---------------------- | ---------------------------------------- |
|
||||
| w | Toggle Ghost Rendering | Cycle between solid and semi-transparent |
|
||||
| W | Toggle Wireframe | Cycle between solid and wireframe |
|
||||
| Ctrl+W / Cmd+W | Toggle Edge Rendering | Show/hide model edges |
|
||||
|
|
@ -77,7 +77,7 @@ description: Keyboard Shortcuts and Mouse Controls
|
|||
## Settings & Dialogs
|
||||
|
||||
| Key | Action | Notes |
|
||||
| --- | ------ | ----- |
|
||||
| --- | ------------------- | -------------------------------- |
|
||||
| e | Device Dialog | Select and customize devices |
|
||||
| o | Tool Dialog | CNC mode only |
|
||||
| q | Preferences Dialog | Change application behaviors |
|
||||
|
|
@ -88,7 +88,7 @@ description: Keyboard Shortcuts and Mouse Controls
|
|||
## Workspace Management
|
||||
|
||||
| Key | Action | Notes |
|
||||
| --- | ------ | ----- |
|
||||
| ------ | --------------------- | ------------------------------------------ |
|
||||
| Ctrl+S | Save Settings | Save current settings |
|
||||
| Cmd+S | Save Workspace | Save workspace state (macOS) |
|
||||
| Cmd+L | Restore Workspace | Restore saved workspace (macOS) |
|
||||
|
|
@ -99,7 +99,7 @@ description: Keyboard Shortcuts and Mouse Controls
|
|||
### General Navigation
|
||||
|
||||
| Input | Action | Notes |
|
||||
| ----- | ------ | ----- |
|
||||
| --------------- | ----------- | ------------------------------- |
|
||||
| [left + drag] | Rotate View | Orbit camera around focus point |
|
||||
| [right + drag] | Pan View | Move camera focus |
|
||||
| [meta + drag] | Pan View | Alternative pan (macOS) |
|
||||
|
|
@ -109,14 +109,14 @@ description: Keyboard Shortcuts and Mouse Controls
|
|||
### Object Interaction
|
||||
|
||||
| Input | Action | Notes |
|
||||
| ----- | ------ | ----- |
|
||||
| -------------- | -------- | ----------------------------------- |
|
||||
| [ctrl + click] | Lay Flat | Rotate clicked face toward platform |
|
||||
| [meta + click] | Lay Flat | Alternative (macOS) |
|
||||
|
||||
### FDM Support Mode
|
||||
|
||||
| Input | Action | Notes |
|
||||
| ----- | ------ | ----- |
|
||||
| ---------------------------- | --------------------- | ---------------------------- |
|
||||
| [left click] | Toggle Support Column | Add or remove support column |
|
||||
| [ctrl + drag] / [cmd + drag] | Erase Columns | Remove support columns |
|
||||
| [alt + drag] / [opt + drag] | Draw Columns | Add support columns |
|
||||
|
|
|
|||
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",
|
||||
"version": "4.6.0",
|
||||
"version": "4.7.0",
|
||||
"description": "grid.space 3d slicing & modeling tools",
|
||||
"author": "Stewart Allen <sa@grid.space>",
|
||||
"license": "MIT",
|
||||
|
|
@ -41,6 +41,7 @@
|
|||
"@gridspace/basic-ftp": "github:gridspace/basic-ftp#v5.0.5-gridspace",
|
||||
"@gridspace/net-level-client": "^0.2.3",
|
||||
"@gridspace/raster-path": "^1.1.1",
|
||||
"@salusoft89/planegcs": "^1.1.7",
|
||||
"@tracespace/parser": "^5.0.0-next.0",
|
||||
"@tweenjs/tween.js": "^16.6.0",
|
||||
"aedes": "^0.51.3",
|
||||
|
|
@ -77,7 +78,7 @@
|
|||
"cross-env": "^10.1.0",
|
||||
"docusaurus-lunr-search": "^3.6.0",
|
||||
"dotenv": "latest",
|
||||
"electron": "^35.0.1",
|
||||
"electron": "^40.0.0-beta.6",
|
||||
"electron-builder": "^24.9.1",
|
||||
"esbuild": "^0.25.5",
|
||||
"fs-extra": "^11.2.0",
|
||||
|
|
@ -103,6 +104,7 @@
|
|||
"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",
|
||||
"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-dev": "docusaurus start --config conf/docusaurus.config.js",
|
||||
"docs-serve": "docusaurus serve --config conf/docusaurus.config.js --port 4004",
|
||||
|
|
@ -121,7 +123,8 @@
|
|||
"start-ddb": "npm run prebuild && electron . --devel --debugg",
|
||||
"start-dev": "npm run prebuild prod && electron . --devel",
|
||||
"start": "npm run prebuild && electron .",
|
||||
"webpack-ext": "npm run webpack-three && npm run webpack-zip && 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-src": "node bin/esbuild.config.mjs",
|
||||
"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;
|
||||
}
|
||||
let area = poly.open ? 1 : poly.area();
|
||||
poly.forEachPoint(function (point, index) {
|
||||
poly.forEachPoint((point, index) => {
|
||||
dist = opt.weight ?
|
||||
startPoint.distTo3D(point) * area * area :
|
||||
startPoint.distTo2D(point);
|
||||
|
|
|
|||
|
|
@ -74,10 +74,23 @@ const POLYS = {
|
|||
union,
|
||||
unionFaces,
|
||||
xor,
|
||||
verify
|
||||
};
|
||||
|
||||
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) {
|
||||
for (let p of polys) {
|
||||
p.inner = undefined;
|
||||
|
|
@ -136,7 +149,7 @@ export function fromClipperNode(tnode, z) {
|
|||
export function fromClipperTree(tnode, z, tops, parent, minarea) {
|
||||
let poly,
|
||||
polys = tops || [],
|
||||
min = numOrDefault(minarea, 0.1);
|
||||
min = minarea ?? 0.1;
|
||||
|
||||
for (let child of tnode.m_Childs) {
|
||||
poly = fromClipperNode(child, z);
|
||||
|
|
@ -150,7 +163,7 @@ export function fromClipperTree(tnode, z, tops, parent, minarea) {
|
|||
polys.push(poly);
|
||||
}
|
||||
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
|
||||
* @returns {Polygon[]}
|
||||
*/
|
||||
export function trimTo(setA, setB) {
|
||||
export function trimTo(setA, setB, opt = {}) {
|
||||
// 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;
|
||||
util.doCombinations(setA, setB, {}, function(a, b) {
|
||||
if (tmp = a.mask(b)) {
|
||||
util.doCombinations(setA, setB, {}, (a, b) => {
|
||||
if (tmp = a.mask(b, opt.nullEq, opt.minArea)) {
|
||||
out.appendAll(tmp);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ export async function slice(points, options = {}) {
|
|||
zSum = 0.0, // sanity check that points enclose non-zere volume
|
||||
buckets = [], // banded/grouped faces to speed up slice/search
|
||||
overlapMax = options.overlap || 0.75,
|
||||
bucketMin = options.bucketMin ?? 1,
|
||||
bucketMax = options.bucketMax || 100,
|
||||
onupdate = options.onupdate || function() {},
|
||||
sliceFn = dval(options.slicer, sliceZ),
|
||||
|
|
@ -138,7 +139,7 @@ export async function slice(points, options = {}) {
|
|||
let zSpan = zMax - zMin;
|
||||
let zSpanAvg = zSum / points.length;
|
||||
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);
|
||||
|
||||
|
|
@ -211,7 +212,6 @@ export async function slice(points, options = {}) {
|
|||
let count = 0;
|
||||
let opt = { ...options, zMin, zMax, zIndexes };
|
||||
let ps = [];
|
||||
|
||||
for (let i = 0, l = buckets.length; i < l; i++) {
|
||||
let bucket = buckets[i];
|
||||
let { points, slices } = bucket;
|
||||
|
|
|
|||
|
|
@ -40,12 +40,14 @@ import { visuals } from './visuals.js';
|
|||
import { widgets } from './widgets.js';
|
||||
import { workspace } from './workspace.js';
|
||||
|
||||
import { OPFS } from '../../moto/opfs.js';
|
||||
|
||||
// environment setup
|
||||
let LOC = self.location,
|
||||
EVENT = broker,
|
||||
SETUP = utils.parseOpt(LOC.search.substring(1)),
|
||||
FILES = openFiles(new Index(SETUP.d ? SETUP.d[0] : 'kiri')),
|
||||
LOCAL = self.debug && !SETUP.remote,
|
||||
LOCAL = (LOC.host.startsWith('localhost') || self.debug) && !SETUP.remote,
|
||||
SECURE = isSecure(LOC.protocol);
|
||||
|
||||
// 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) },
|
||||
bind(t,m,o) { return EVENT.bind(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() },
|
||||
listeners(topic) { return EVENT.targets(topic) },
|
||||
on(t,l) { EVENT.on(t,l); return api.event },
|
||||
|
|
@ -227,6 +230,7 @@ export const api = {
|
|||
onkey(fn) {
|
||||
api.feature.on_key2.push(fn);
|
||||
},
|
||||
opfs: OPFS,
|
||||
platform,
|
||||
process: processModule,
|
||||
sdb: dataLocal,
|
||||
|
|
@ -234,6 +238,12 @@ export const api = {
|
|||
settings,
|
||||
show: {
|
||||
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') },
|
||||
devices: showDevices,
|
||||
import() { api.ui.import.style.display = '' },
|
||||
|
|
|
|||
|
|
@ -341,11 +341,11 @@ export const conf = {
|
|||
sliceSupportAngle: 50,
|
||||
sliceSupportDensity: 0.1,
|
||||
sliceSupportExtra: 0,
|
||||
sliceSupportGap: 1,
|
||||
sliceSupportGap: true,
|
||||
sliceSupportNozzle: 0,
|
||||
sliceSupportOffset: 1.0,
|
||||
sliceSupportOutline: true,
|
||||
sliceSupportSpan: 5,
|
||||
// sliceSupportSpan: 5,
|
||||
sliceSupportType: "disabled",
|
||||
sliceSupportTree: false,
|
||||
sliceTopLayers: 3,
|
||||
|
|
@ -604,9 +604,11 @@ export const conf = {
|
|||
camToolInit: true,
|
||||
camTraceDogbone: false,
|
||||
camTraceDown: 0,
|
||||
camTraceIgnore: false,
|
||||
camTraceLines: false,
|
||||
camTraceMerge: false,
|
||||
camTraceOffOver: 0,
|
||||
camTraceOffZ: 0,
|
||||
camTraceOffset: "none",
|
||||
camTraceOver: 0.5,
|
||||
camTracePlunge: 200,
|
||||
|
|
|
|||
|
|
@ -82,6 +82,8 @@ function updateProcessList() {
|
|||
load.onclick = (ev) => {
|
||||
api.conf.load(undefined, sk);
|
||||
updateProcessList();
|
||||
// update pulldowns
|
||||
api.devices.refresh();
|
||||
modal.hide();
|
||||
}
|
||||
load.appendChild(DOC.createTextNode(sk));
|
||||
|
|
|
|||
|
|
@ -172,9 +172,6 @@ function updateSettings(opt = {}) {
|
|||
same = false;
|
||||
}
|
||||
}
|
||||
|
||||
$('mode-device').innerText = device.deviceName;
|
||||
$('mode-profile').innerText = `${cproc[mode]}${same ? '' : ' *'}`;
|
||||
}
|
||||
|
||||
function updateSettingsFromFields(setrec, uirec = api.ui, changes) {
|
||||
|
|
@ -279,6 +276,7 @@ function updateFieldsFromSettings(setrec, uirec = api.ui, opt = {}) {
|
|||
let opt = document.createElement('option');
|
||||
opt.appendChild(document.createTextNode(el.name));
|
||||
opt.setAttribute('value', ev);
|
||||
if (id === '#') opt.setAttribute('disabled', true);
|
||||
uie.appendChild(opt);
|
||||
});
|
||||
if (chosen) {
|
||||
|
|
@ -526,7 +524,7 @@ function settingsExport(opts = {}) {
|
|||
const shot = opts.work || opts.screen ? space.screenshot() : undefined;
|
||||
const work = opts.work ? codec.encode(widgets,{_json_:true}) : 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
|
||||
for (let w of widgets) {
|
||||
setn.widget[w.id] = w.anno;
|
||||
|
|
@ -539,7 +537,7 @@ function settingsExport(opts = {}) {
|
|||
note: note,
|
||||
work: work,
|
||||
view: view,
|
||||
moto: moto.id,
|
||||
moto: self.moto?.id,
|
||||
init: local.getItem('kiri-init'),
|
||||
time: Date.now()
|
||||
};
|
||||
|
|
@ -559,6 +557,7 @@ function settingsImport(data, ask) {
|
|||
}
|
||||
|
||||
if (api.const.LOCAL) console.log('import', data);
|
||||
|
||||
let isSettings = (data.settings && data.time);
|
||||
let isProcess = (data.process && data.time && data.mode && data.name);
|
||||
let isDevice = (data.device && data.time);
|
||||
|
|
|
|||
|
|
@ -301,9 +301,7 @@ function setDeviceCode(code, devicename) {
|
|||
*/
|
||||
function renderDevices(devices) {
|
||||
let selected = api.device.get() || devices[0],
|
||||
features = api.feature,
|
||||
devs = setconf.get().devices,
|
||||
dfilter = typeof(features.device_filter) === 'function' ? features.device_filter : undefined;
|
||||
devs = setconf.get().devices;
|
||||
|
||||
for (let local in devs) {
|
||||
if (!(devs.hasOwnProperty(local) && devs[local])) {
|
||||
|
|
@ -367,6 +365,31 @@ function renderDevices(devices) {
|
|||
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 list_cdev = [];
|
||||
let list_mdev = [];
|
||||
|
|
@ -388,7 +411,7 @@ function renderDevices(devices) {
|
|||
}
|
||||
});
|
||||
|
||||
let dev_list = $('dev-list');
|
||||
let dev_list = selector;
|
||||
h.bind(dev_list, [
|
||||
h.option({ _: '-- My Devices --', disabled: true }),
|
||||
...list_mdev,
|
||||
|
|
@ -401,7 +424,6 @@ function renderDevices(devices) {
|
|||
const seldev = dev_list.options[dev_list.selectedIndex];
|
||||
selectDevice(seldev.innerText);
|
||||
api.platform.layout();
|
||||
updateDeviceList();
|
||||
}
|
||||
selectDevice(selected);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -247,13 +247,16 @@ function exportGCodeDialog(gcode, sections, info, names) {
|
|||
ajax = new XMLHttpRequest(),
|
||||
host = octo_host.value.toLowerCase(),
|
||||
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) {
|
||||
api.show.alert("host missing protocol (http:// or https://)");
|
||||
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");
|
||||
return;
|
||||
}
|
||||
|
|
@ -262,6 +265,11 @@ function exportGCodeDialog(gcode, sections, info, names) {
|
|||
localSet('octo-apik', apik.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;
|
||||
form.append("file", getBlob(), filename);
|
||||
ajax.onreadystatechange = function() {
|
||||
|
|
@ -297,6 +305,9 @@ function exportGCodeDialog(gcode, sections, info, names) {
|
|||
} else {
|
||||
ajax.open("POST", host + "/api/files/local");
|
||||
}
|
||||
if (proxy) {
|
||||
ajax.setRequestHeader("X-Host", proxy);
|
||||
}
|
||||
if (apik) {
|
||||
ajax.setRequestHeader("X-Api-Key", apik);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,36 +22,58 @@ function loadImageDialog(image, name, force) {
|
|||
}
|
||||
});
|
||||
}
|
||||
const opt = {pre: [
|
||||
"<div class='f-col a-center'>",
|
||||
" <h3>Image Conversion</h3>",
|
||||
" <p class='t-just' style='width:300px;line-height:1.5em'>",
|
||||
" This will create a 3D model from a 2D PNG image. Photos must",
|
||||
" be blurred to be usable. Values from 0=off to 50=high are suggested.",
|
||||
" Higher values incur more processing time.",
|
||||
" </p>",
|
||||
" <div class='f-row t-right'><table>",
|
||||
" <tr><th>blur value</th><td><input id='png-blur' value='0' size='3'></td>",
|
||||
" <th> invert image</th><td><input id='png-inv' type='checkbox'></td></tr>",
|
||||
" <tr><th>base size</th><td><input id='png-base' value='0' size='3'></td>",
|
||||
" <th> invert alpha</th><td><input id='alpha-inv' type='checkbox'></td></tr>",
|
||||
" <tr><th>border size</th><td><input id='png-border' value='0' size='3'></td>",
|
||||
" <th></th><td></td></tr>",
|
||||
" </table></div>",
|
||||
"</div>"
|
||||
]};
|
||||
api.uc.confirm(undefined, {convert:true, cancel:false}, undefined, opt).then((ok) => {
|
||||
if (ok) {
|
||||
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">Image Conversion</h3>`,
|
||||
` <p class="image-convert-copy t-just">`,
|
||||
` This will create a 3D model from a 2D PNG image. Photos must`,
|
||||
` be blurred to be usable. Values from 0=off to 50=high are suggested.`,
|
||||
` Higher values incur more processing time.`,
|
||||
` </p>`,
|
||||
` <div class="f-row t-right image-convert-fields"><table>`,
|
||||
` <tr><th>blur value</th><td><input id="png-blur-${rnd}" value="0" size="3"></td>`,
|
||||
` <th>invert image</th><td><input id="png-inv-${rnd}" type="checkbox"></td></tr>`,
|
||||
` <tr><th>base size</th><td><input id="png-base-${rnd}" value="0" size="3"></td>`,
|
||||
` <th>invert alpha</th><td><input id="alpha-inv-${rnd}" type="checkbox"></td></tr>`,
|
||||
` <tr><th>border size</th><td><input id="png-border-${rnd}" value="0" size="3"></td>`,
|
||||
` <th></th><td></td></tr>`,
|
||||
` </table></div>`,
|
||||
` <div class="f-row j-end image-convert-actions">`,
|
||||
` <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, {
|
||||
file: name,
|
||||
blur: parseInt($('png-blur').value) || 0,
|
||||
base: parseInt($('png-base').value) || 0,
|
||||
border: parseInt($('png-border').value) || 0,
|
||||
inv_image: $('png-inv').checked,
|
||||
inv_alpha: $('alpha-inv').checked
|
||||
});
|
||||
}
|
||||
blur: parseInt(blur.value) || 0,
|
||||
base: parseInt(base.value) || 0,
|
||||
border: parseInt(border.value) || 0,
|
||||
inv_image: invImage.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 = {}) {
|
||||
const info = Object.assign({settings: settings.get(), png:image}, opt);
|
||||
api.client.image2mesh(info, progress => {
|
||||
api.show.progress(progress, "converting");
|
||||
api.client.image2mesh(info, () => {
|
||||
api.show.busy('converting');
|
||||
}, vertices => {
|
||||
api.show.progress(0);
|
||||
api.show.busy(false);
|
||||
const widget = newWidget().loadVertices(vertices);
|
||||
widget.meta.file = opt.file;
|
||||
platform.add(widget);
|
||||
|
|
|
|||
|
|
@ -105,8 +105,10 @@ export function bind() {
|
|||
range: $('slider-center'),
|
||||
},
|
||||
|
||||
loading: $('progress').style,
|
||||
progress: $('progbar').style,
|
||||
loading: $('progress-overlay')?.style,
|
||||
progressOverlay: $('progress-overlay'),
|
||||
progress: $('progress-ring'),
|
||||
progressPct: $('progress-pct'),
|
||||
prostatus: $('progtxt'),
|
||||
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;
|
||||
}
|
||||
|
||||
// upon restore, seed presets
|
||||
api.event.emit('preset', api.conf.dbo());
|
||||
|
||||
// api.event.on("set.threaded", bool => setThreaded(bool));
|
||||
|
||||
export function onBooleanClick(el) {
|
||||
|
|
@ -142,6 +139,11 @@ function onResize() {
|
|||
} else {
|
||||
ui.modalBox.classList.remove('mh85');
|
||||
}
|
||||
if (WIN.innerWidth < 800) {
|
||||
$('app').classList.add('slideshow');
|
||||
} else {
|
||||
api.prefs.updateDrawer();
|
||||
}
|
||||
api.view.update_slider();
|
||||
}
|
||||
|
||||
|
|
@ -174,6 +176,7 @@ export function init_input() {
|
|||
event.on('resize', onResize);
|
||||
|
||||
// configure moto.space
|
||||
space.view.setFitPadding({ perspective: 0.8 });
|
||||
space.sky.showGrid(false);
|
||||
space.sky.setColor(controller.dark ? 0 : 0xffffff);
|
||||
space.setAntiAlias(controller.antiAlias);
|
||||
|
|
@ -211,6 +214,10 @@ export function init_input() {
|
|||
// api augmentation with local functions
|
||||
api.device.export = settingsOps.export_device;
|
||||
|
||||
let driven = true,
|
||||
hideable = true,
|
||||
separator = true;
|
||||
|
||||
Object.assign(ui, {
|
||||
tracker: tracker,
|
||||
container: container,
|
||||
|
|
@ -320,6 +327,10 @@ export function init_input() {
|
|||
|
||||
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 */
|
||||
...menuFDM(),
|
||||
|
||||
|
|
@ -349,6 +360,8 @@ export function init_input() {
|
|||
// override old style settings two-button menu
|
||||
ui.settingsSave.onclick = () => {
|
||||
settingsOps.settings_save(undefined, ui.settingsName.value);
|
||||
// update pulldowns
|
||||
api.devices.refresh();
|
||||
};
|
||||
|
||||
// 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();
|
||||
|
||||
// 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
|
||||
if (beta && beta > 0 && sdb.kiri_beta != beta) {
|
||||
|
|
@ -143,6 +145,9 @@ export async function init_sync() {
|
|||
history.replaceState({}, '', wlp.substring(0,kio + 6));
|
||||
}
|
||||
|
||||
// upon restore, seed presets
|
||||
api.event.emitDefer('preset', api.conf.get());
|
||||
|
||||
// lift curtain
|
||||
$('curtain').style.display = 'none';
|
||||
}
|
||||
|
|
@ -215,22 +220,126 @@ function ui_sync() {
|
|||
}
|
||||
|
||||
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
|
||||
ui.acct.help.onclick = (ev) => { ev.stopPropagation(); api.help.show() };
|
||||
ui.acct.don8.onclick = (ev) => { ev.stopPropagation(); api.modal.show('don8') };
|
||||
ui.acct.mesh.onclick = (ev) => { ev.stopPropagation(); WIN.location = "/mesh" };
|
||||
ui.acct.export.onclick = (ev) => { ev.stopPropagation(); settingsOps.export_profile() };
|
||||
ui.acct.export.title = LANG.acct_xpo;
|
||||
ui.func.slice.onclick = (ev) => { ev.stopPropagation(); api.function.slice() };
|
||||
ui.func.preview.onclick = (ev) => { ev.stopPropagation(); api.function.print() };
|
||||
ui.func.animate.onclick = (ev) => { ev.stopPropagation(); api.function.animate() };
|
||||
ui.func.export.onclick = (ev) => { ev.stopPropagation(); api.function.export() };
|
||||
// prevent modal input from propagating to parents
|
||||
ui.modalBox.onclick = (ev) => { ev.stopPropagation() };
|
||||
|
||||
$('export-support-a').onclick = (ev) => { ev.stopPropagation(); api.modal.show('don8') };
|
||||
$('mode-device').onclick = api.show.devices;
|
||||
$('mode-profile').onclick = settingsOps.settings_load;
|
||||
$('mode-fdm').onclick = () => api.mode.set('FDM');
|
||||
$('mode-cam').onclick = () => api.mode.set('CAM');
|
||||
$('mode-sla').onclick = () => api.mode.set('SLA');
|
||||
|
|
@ -245,13 +354,9 @@ function setup_keybd_nav() {
|
|||
$('file-new').onclick = (ev) => { ev.stopPropagation(); settingsOps.new_workspace() };
|
||||
$('file-recent').onclick = () => { api.modal.show('files') };
|
||||
$('file-import').onclick = (ev) => { api.event.import(ev); };
|
||||
$('view-arrange').onclick = api.platform.layout;
|
||||
$('view-top').onclick = space.view.top;
|
||||
$('view-home').onclick = space.view.home;
|
||||
$('view-front').onclick = space.view.front;
|
||||
$('view-back').onclick = space.view.back;
|
||||
$('view-left').onclick = space.view.left;
|
||||
$('view-right').onclick = space.view.right;
|
||||
|
||||
$('unrotate').onclick = () => {
|
||||
api.widgets.for(w => w.unrotate());
|
||||
selection.update_info();
|
||||
|
|
@ -274,7 +379,10 @@ function setup_keybd_nav() {
|
|||
$('rot_z_gt').onclick = () => { selection.rotate(0,0,-d * $('rot_z').value) };
|
||||
|
||||
// rendering options
|
||||
$('render-edges').onclick = () => { api.view.set_edges({ toggle: true }); api.conf.save() };
|
||||
$('render-edges').onclick = () => {
|
||||
api.view.set_edges({ toggle: true });
|
||||
api.conf.save()
|
||||
};
|
||||
$('render-ghost').onclick = () => {
|
||||
const opacity = api.view.is_arrange() ? 0.4 : 0.25;
|
||||
api.view.set_wireframe(false);
|
||||
|
|
@ -285,7 +393,7 @@ function setup_keybd_nav() {
|
|||
};
|
||||
$('render-wire').onclick = () => {
|
||||
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();
|
||||
};
|
||||
$('render-solid').onclick = () => {
|
||||
|
|
@ -301,15 +409,24 @@ function setup_keybd_nav() {
|
|||
$('mesh-split').onclick = selection.isolateBodies;
|
||||
$('context-duplicate').onclick = selection.duplicate;
|
||||
$('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-lefty').onclick = view_tools.startLeftAlign;
|
||||
$('context-setfocus').onclick = () => {
|
||||
view_tools.startFocus(ev => api.space.set_focus(undefined, ev.object.point));
|
||||
};
|
||||
$('context-contents').onclick = api.const.SPACE.view.fit;
|
||||
// $('context-contents').onclick = api.const.SPACE.view.fit;
|
||||
$('view-fit').onclick = api.const.SPACE.view.fit;
|
||||
$('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
|
||||
$('mod-x').onclick = api.modal.hide;
|
||||
|
||||
|
|
@ -319,6 +436,13 @@ function setup_keybd_nav() {
|
|||
sdb.gdpr = Date.now();
|
||||
};
|
||||
|
||||
// add app name hover info
|
||||
$('app-info').innerText = version;
|
||||
// fix file input on iOS
|
||||
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 {
|
||||
row.setAttribute("source", source || "tools");
|
||||
}
|
||||
row.setAttribute("class", "var-row");
|
||||
row.classList.add('var-row');
|
||||
row.style.display = hide ? 'none' : '';
|
||||
if (options.id) ip.setAttribute("id", options.id);
|
||||
if (options.convert) ip.convert = options.convert.bind(ip);
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ function setMode(mode, lock, then) {
|
|||
// change mode constants
|
||||
current.mode = mode;
|
||||
MODE = MODES[mode];
|
||||
document.title = 'Kiri:Moto | ' + mode;
|
||||
// gcode edit area for any non-SLA mode
|
||||
api.uc.setVisible($('gcode-edit'), mode !== 'SLA');
|
||||
// highlight selected mode menu item
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { api } from './api.js';
|
|||
import { MODES } from './consts.js';
|
||||
import { colorSchemeRegistry } from './color/schemes.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 { Packer } from './pack.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.opacity(0.05);
|
||||
space.sky.set({ color: 0, ambient: { intensity: 0.6 } });
|
||||
document.body.classList.add('dark');
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
} else {
|
||||
space.platform.set({ light: 0.08 });
|
||||
space.platform.setFont({rulerColor:'#333333'});
|
||||
space.platform.setGrid(gridMajor, gridMinor, scheme.grid.major, scheme.grid.minor);
|
||||
space.platform.opacity(0.2);
|
||||
space.sky.set({ color: 0xffffff, ambient: { intensity: 1.1 } });
|
||||
document.body.classList.remove('dark');
|
||||
document.documentElement.setAttribute('data-theme', 'light');
|
||||
}
|
||||
space.platform.setSize();
|
||||
}
|
||||
|
|
@ -491,7 +492,7 @@ function load_stl(url, onload, formdata, credentials, headers) {
|
|||
*/
|
||||
function load_url(url, options = {}) {
|
||||
platform.group();
|
||||
file_load.URL.load(url, options).then(objects => {
|
||||
url_load(url, options).then(objects => {
|
||||
let widgets = [];
|
||||
for (let object of objects) {
|
||||
let widget = newWidget(undefined, options.group).loadVertices(object.mesh);
|
||||
|
|
@ -978,6 +979,7 @@ function load_files(files, group) {
|
|||
isobj = lower.endsWith(".obj"),
|
||||
is3mf = lower.endsWith(".3mf"),
|
||||
issvg = lower.endsWith(".svg"),
|
||||
isdxf = lower.endsWith(".dxf"),
|
||||
ispng = lower.endsWith(".png"),
|
||||
isjpg = lower.endsWith(".jpg"),
|
||||
iskmz = lower.endsWith(".kmz"),
|
||||
|
|
@ -1079,6 +1081,19 @@ function load_files(files, group) {
|
|||
}
|
||||
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 (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).
|
||||
* Finds maximum Y dimension of all widgets and expands bed if needed.
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ function booleanSave() {
|
|||
control.assembly = ui.assembly.checked;
|
||||
control.autoLayout = ui.autoLayout.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.drawer = ui.drawer.checked;
|
||||
control.exportOcto = ui.exportOcto.checked;
|
||||
|
|
@ -109,6 +109,7 @@ function booleanSave() {
|
|||
updateDrawer();
|
||||
api.event.emit('boolean.update');
|
||||
space.view.setProjection(control.ortho ? 'orthographic' : 'perspective');
|
||||
setDarkLight(control.dark);
|
||||
}
|
||||
|
||||
function updateDrawer() {
|
||||
|
|
@ -127,3 +128,5 @@ export const preferences = {
|
|||
booleanSave,
|
||||
updateDrawer
|
||||
};
|
||||
|
||||
api.prefs = preferences;
|
||||
|
|
@ -6,6 +6,7 @@ import { THREE } from '../../ext/three.js';
|
|||
import { tool as MeshTool } from '../../mesh/tool.js';
|
||||
import { encode as objEncode } from '../../load/obj.js';
|
||||
import { encode as stlEncode } from '../../load/stl.js';
|
||||
import { $ } from '../../moto/webui.js';
|
||||
|
||||
/**
|
||||
* Array of currently selected widget meshes.
|
||||
|
|
|
|||
|
|
@ -42,11 +42,11 @@ function setViewMode(mode) {
|
|||
['view-arrange','act-slice','act-preview','act-animate'].forEach(el => {
|
||||
$(el).classList.remove('selected')
|
||||
});
|
||||
$('render-tools').classList.add('hide');
|
||||
// $('render-tools').classList.add('hide');
|
||||
switch (mode) {
|
||||
case VIEWS.ARRANGE:
|
||||
$('view-arrange').classList.add('selected');
|
||||
$('render-tools').classList.remove('hide');
|
||||
// $('render-tools').classList.remove('hide');
|
||||
api.function.clear_progress();
|
||||
api.client.clear();
|
||||
STACKS.clear();
|
||||
|
|
|
|||
|
|
@ -315,17 +315,41 @@ function applyVisualState(widget) {
|
|||
* @param {string} [msg] - Optional status message to display
|
||||
*/
|
||||
function setProgress(value = 0, msg) {
|
||||
value = (value * 100).round(4);
|
||||
api.ui.progress.width = value+'%';
|
||||
if (self.debug) {
|
||||
// console.log(msg, value.round(2));
|
||||
api.ui.prostatus.style.display = 'flex';
|
||||
if (msg) {
|
||||
api.ui.prostatus.innerHTML = msg;
|
||||
const overlay = api.ui.progressOverlay;
|
||||
const ring = api.ui.progress;
|
||||
const pct = api.ui.progressPct;
|
||||
const text = api.ui.prostatus;
|
||||
|
||||
if (!overlay || !ring || !text) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 {
|
||||
api.ui.prostatus.innerHTML = '';
|
||||
}
|
||||
ring.classList.add('indeterminate');
|
||||
ring.style.removeProperty('--progress-deg');
|
||||
if (pct) pct.innerText = '';
|
||||
}
|
||||
|
||||
text.innerText = msg || '';
|
||||
}
|
||||
|
||||
let statsTimer;
|
||||
|
|
|
|||
|
|
@ -288,11 +288,18 @@ class Print {
|
|||
if (safeEval) {
|
||||
safeEval.setContext(consts);
|
||||
}
|
||||
function doSafeEval(tok) {
|
||||
try {
|
||||
return safeEval ? safeEval.eval(tok) : undefined;
|
||||
} catch (e) {
|
||||
console.log({ macro_error: tok });
|
||||
}
|
||||
}
|
||||
function tryeval(str) {
|
||||
try {
|
||||
return eval(`{ ${str} }`)
|
||||
} catch (e) {
|
||||
console.log({ eval_error: e, str });
|
||||
console.log({ macro_error: e, 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(`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;
|
||||
if (pad === 666) {
|
||||
return evl;
|
||||
|
|
|
|||
|
|
@ -786,7 +786,7 @@ class Widget {
|
|||
return this.cache.shadow = stack;
|
||||
}
|
||||
|
||||
async computeShadowStack(zlist, progress, pocket) {
|
||||
async computeShadowStack(zlist, progress, pocket, up = 1) {
|
||||
let shadow_stack = this.cache.shadow_stack;
|
||||
if (!shadow_stack) {
|
||||
shadow_stack = this.cache.shadow_stack = {};
|
||||
|
|
@ -803,7 +803,7 @@ class Widget {
|
|||
let p = work.minions.queueAsync({
|
||||
cmd: 'cam_shadow_z',
|
||||
z: z - 0.005,
|
||||
t: z + 1
|
||||
t: z + up
|
||||
}).then(reply => {
|
||||
shadow_stack[z - 0.005] = decode(reply.data);
|
||||
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,
|
||||
"gcodeResetA": ["G92.4 A0 R0"],
|
||||
"new": false,
|
||||
"deviceName": "My Makera Carvera",
|
||||
"deviceName": "Makera Carvera",
|
||||
"maxHeight": 150,
|
||||
"useLaser": true,
|
||||
"useIndexed": true,
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export function animate_clear(api) {
|
|||
}
|
||||
|
||||
export function animate(api, delay) {
|
||||
let alert = api.alerts.show("building animation");
|
||||
api.show.busy("building animation");
|
||||
let settings = api.conf.get();
|
||||
client.animate_setup(settings, data => {
|
||||
checkMeshCommands(data);
|
||||
|
|
@ -80,7 +80,7 @@ export function animate(api, delay) {
|
|||
button.pause.style.display = 'none';
|
||||
|
||||
api.event.emit('animate', 'CAM');
|
||||
api.alerts.hide(alert);
|
||||
api.show.busy(false);
|
||||
space.platform.showGridBelow(false);
|
||||
toggleTrans(0,api.local.getBoolean('cam.anim.trans', true));
|
||||
toggleModel(0,api.local.getBoolean('cam.anim.model', false));
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export function animate_clear2(api) {
|
|||
}
|
||||
|
||||
export function animate2(api, delay) {
|
||||
let alert = api.alerts.show("building animation");
|
||||
api.show.busy("building animation");
|
||||
let settings = api.conf.get();
|
||||
dark = settings.controller.dark;
|
||||
manifold = settings.controller.manifold;
|
||||
|
|
@ -92,7 +92,7 @@ export function animate2(api, delay) {
|
|||
button.pause.style.display = 'none';
|
||||
|
||||
api.event.emit('animate', 'CAM');
|
||||
api.alerts.hide(alert);
|
||||
api.show.busy(false);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -485,8 +485,10 @@ export function createPopOps() {
|
|||
rate: 'camTraceSpeed',
|
||||
plunge: 'camTracePlunge',
|
||||
offover: 'camTraceOffOver',
|
||||
offz: 'camTraceOffZ',
|
||||
dogbone: 'camTraceDogbone',
|
||||
revbone: 'camTraceDogbone',
|
||||
ignore: 'camTraceIgnore',
|
||||
merge: 'camTraceMerge',
|
||||
ov_topz: 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" }),
|
||||
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" }),
|
||||
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 }),
|
||||
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 }),
|
||||
revbone: UC.newBoolean(LANG.co_dogr_s, undefined, { title: LANG.co_dogr_l, show: canDogBonesRev }),
|
||||
exp: UC.newExpand("feeds & speeds", { }),
|
||||
|
|
@ -774,6 +778,7 @@ export function createPopOps() {
|
|||
revbones: 'camAreaRevbones',
|
||||
ov_topz: 0,
|
||||
ov_botz: 0,
|
||||
finish_cut: 0,
|
||||
}).inputs = {
|
||||
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"),
|
||||
|
|
@ -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 }),
|
||||
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() }),
|
||||
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 }),
|
||||
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 }),
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ function zAnchorSave() {
|
|||
api.platform.update_top_z();
|
||||
}
|
||||
|
||||
const hideable = true;
|
||||
const bottom = true;
|
||||
const top = true;
|
||||
|
||||
|
|
@ -118,7 +119,7 @@ export function 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}),
|
||||
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}),
|
||||
|
|
@ -130,7 +131,7 @@ export function menu() {
|
|||
(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>'}))
|
||||
], {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}),
|
||||
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}),
|
||||
|
|
@ -142,7 +143,7 @@ export function menu() {
|
|||
// camStockManual: newRow([
|
||||
// (ui.stockPlace = newButton('position', onButtonClick, { })),
|
||||
// ], {class:"ext-buttons f-row"}),
|
||||
_____: newGroup(LANG.cc_menu, $('cam-limits'), { modes:CAM, driven, separator }),
|
||||
_____: newGroup(LANG.cc_menu, $('cam-limits'), { modes:CAM, driven, separator, hideable, group:"cam-limits" }),
|
||||
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}),
|
||||
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 }),
|
||||
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}),
|
||||
_____: 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}),
|
||||
camDepthFirst: newBoolean(LANG.ou_depf_s, onBooleanClick, {title:LANG.ou_depf_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 }),
|
||||
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)}),
|
||||
_____: 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}),
|
||||
camOriginCenter: newBoolean(LANG.or_cntr_s, onBooleanClick, {title:LANG.or_cntr_l}),
|
||||
separator: newBlank({ class:"set-sep", driven }),
|
||||
|
|
@ -174,7 +175,7 @@ export function menu() {
|
|||
newButton("select", originSelect),
|
||||
newButton("reset", originReset),
|
||||
], { 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 }),
|
||||
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}),
|
||||
|
|
|
|||
|
|
@ -339,7 +339,7 @@ export function opRender() {
|
|||
`<div id="${mark + i}" class="${clazz.join(' ')}"${title}>`,
|
||||
`<label class="label">${label}</label>`,
|
||||
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>`
|
||||
]);
|
||||
bind[mark + i] = rec;
|
||||
|
|
@ -421,9 +421,10 @@ export function opRender() {
|
|||
const brect = ev.target.getBoundingClientRect();
|
||||
const prect = parent.getBoundingClientRect();
|
||||
const Prect = poprec.div.getBoundingClientRect();
|
||||
const tdiff = prect.top - brect.top;
|
||||
const botoff = innerHeight - (brect.top + Prect.height);
|
||||
const offpx = -tdiff + (botoff < 0 ? botoff : -Prect.height/3);
|
||||
const topmv = brect.top - prect.top - Math.min(50, Prect.height/5);
|
||||
const topnu = prect.top + topmv;
|
||||
const botof = innerHeight - (topnu + Prect.height);
|
||||
const offpx = botof < 0 ? topmv + botof : topmv;
|
||||
poprec.div.style.transform = `translateY(${offpx}px)`;
|
||||
poprec.div.onmouseenter = () => { inside = true };
|
||||
poprec.div.onmouseleave = onLeave;
|
||||
|
|
@ -587,7 +588,7 @@ export function zPlaneSelect({ which, onselect }) {
|
|||
}
|
||||
clearPops();
|
||||
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
|
||||
};
|
||||
|
||||
// console.log({ offset, origin, stock });
|
||||
|
||||
function section(section) {
|
||||
append();
|
||||
online({ section });
|
||||
|
|
|
|||
|
|
@ -134,12 +134,15 @@ class OpArea extends CamOp {
|
|||
polys = POLY.union(nupolys, 0.00001, true);
|
||||
}
|
||||
|
||||
// filter out invalid polys
|
||||
polys = polys.filter(p => p && p.length > 2);
|
||||
|
||||
// process each area separately
|
||||
let proc = 0;
|
||||
let pinc = 1 / polys.length;
|
||||
for (let area of polys) {
|
||||
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 = {
|
||||
arc: 250,
|
||||
join: roundSharps ? ClipperLib.JoinType.jtRound : undefined,
|
||||
|
|
@ -175,7 +178,7 @@ class OpArea extends CamOp {
|
|||
for (let z of zs) {
|
||||
let slice = newLayer(z);
|
||||
let layers = slice.output();
|
||||
let shadow = await shadowAt(z);
|
||||
let shadow = await shadowAt(z + 0.01);
|
||||
let tool_shadow = [
|
||||
...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 clip = [];
|
||||
let firstOff = -(toolDiam / 2 + (op.leave_xy ?? 0));
|
||||
// remove shadow from area
|
||||
if (op.ignore) {
|
||||
clip = [ area ];
|
||||
} else {
|
||||
POLY.subtract([ area ], shadow, clip, undefined, undefined, 0);
|
||||
POLY.offset(clip, [ firstOff, -toolOver ], {
|
||||
}
|
||||
//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
|
||||
});
|
||||
// if we see no offsets, re-check the mesh bottom Z then exit
|
||||
|
|
@ -248,14 +266,15 @@ class OpArea extends CamOp {
|
|||
progress(proc, 'clear');
|
||||
} else
|
||||
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 zroc = 0;
|
||||
let zinc = 1 / zs.length;
|
||||
if (tr_offz) zs = zs.map(z => z - tr_offz);
|
||||
for (let z of zs) {
|
||||
let slice = newLayer(z);
|
||||
let layers = slice.output();
|
||||
let shadow = await shadowAt(z);
|
||||
let shadow = op.base ? state.shadow.base : await shadowAt(z);
|
||||
let outs = [];
|
||||
if (tr_type === 'none') {
|
||||
// todo: move this out of the zs loop and only setZ when needed
|
||||
|
|
@ -462,7 +481,8 @@ class OpArea extends CamOp {
|
|||
while (areas?.length) {
|
||||
let min = {
|
||||
dist: Infinity,
|
||||
area: undefined
|
||||
area: undefined,
|
||||
point: undefined
|
||||
};
|
||||
|
||||
for (let area of areas.filter(p => !p.used)) {
|
||||
|
|
@ -477,16 +497,19 @@ class OpArea extends CamOp {
|
|||
if (find.distance < min.dist) {
|
||||
min.area = area;
|
||||
min.dist = find.distance;
|
||||
min.point = find.point
|
||||
}
|
||||
}
|
||||
|
||||
// if we have a next-closest top poly, pocket that
|
||||
if (min.area) {
|
||||
min.area.used = true;
|
||||
printPoint = min.point;
|
||||
pocket({
|
||||
cutdir: op.ov_conv,
|
||||
depthFirst: process.camDepthFirst && !op.drape,
|
||||
depthFirst: process.camDepthFirst,
|
||||
easeDown: op.down && process.easeDown ? op.down : 0,
|
||||
outline: op.drape || op.mode === 'trace',
|
||||
progress: (n,m) => progress(n/m, "area"),
|
||||
slices: min.area.filter(slice => slice.camLines)
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ function createFilter(op, origin, axis) {
|
|||
let index = 0;
|
||||
const accept = [];
|
||||
filter = function (slices) {
|
||||
for (let slice of slices) {
|
||||
for (let slice of slices.filter(s => s.camLines)) {
|
||||
if (slice_fn && slice_fn(slice, index++)) {
|
||||
accept.push(slice);
|
||||
} else if (box) {
|
||||
|
|
|
|||
|
|
@ -13,8 +13,7 @@ class OpDrill extends CamOp {
|
|||
|
||||
async slice(progress) {
|
||||
let { op, state } = this;
|
||||
let { settings, addSlices, widget, updateToolDiams } = state;
|
||||
let { color } = state;
|
||||
let { color, settings, addSlices, widget, updateToolDiams, zBottom } = state;
|
||||
let { drills } = op
|
||||
|
||||
let drillTool = new Tool(settings, op.tool),
|
||||
|
|
@ -39,6 +38,10 @@ class OpDrill extends CamOp {
|
|||
}
|
||||
|
||||
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
|
||||
if ((op.thru > 0)) {
|
||||
drill.zBottom -= op.thru;
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ class OpLevel extends CamOp {
|
|||
let zBot = zTop - down;
|
||||
let zList = stepz && down ? util.lerp(zTop, zBot, stepz) : [ zBot ];
|
||||
|
||||
// ensure zList is descending
|
||||
zList.sort((a,b) => b - a);
|
||||
|
||||
if (share.ran) {
|
||||
console.log('skip');
|
||||
this.skip = true;
|
||||
|
|
@ -49,6 +52,7 @@ class OpLevel extends CamOp {
|
|||
POLY.fillArea(clear, 1090, stepOver, points);
|
||||
|
||||
let layers = this.layers = [];
|
||||
|
||||
for (let z of zList) {
|
||||
let lines = [];
|
||||
layers.push(lines);
|
||||
|
|
|
|||
|
|
@ -25,9 +25,11 @@ class OpOutline extends CamOp {
|
|||
let areas = shadow.base.clone(true);
|
||||
ops_list.push(new OpArea(state, {
|
||||
areas: { [widget.id]: areas.map(p => p.toArray()) },
|
||||
base: true,
|
||||
direction,
|
||||
dogbones,
|
||||
down,
|
||||
drape: true,
|
||||
expand: 0,
|
||||
mode: 'trace',
|
||||
omitinner: omitvoid,
|
||||
|
|
|
|||
|
|
@ -147,13 +147,13 @@ class OpRegister extends CamOp {
|
|||
|
||||
prepare(ops, progress) {
|
||||
let { op } = this;
|
||||
let { emitDrills, setDrill, setTool, setTravelBoundary } = ops;
|
||||
let { emitDrills, emitTraces, setDrill, setTool, setTravelBoundary } = ops;
|
||||
|
||||
setTravelBoundary();
|
||||
if (op.axis === '-' || op.axis === '=') {
|
||||
setTool(op.tool, op.feed, op.rate);
|
||||
for (let slice of this.sliceOut) {
|
||||
ops.emitTrace(slice);
|
||||
emitTraces(slice.camLines);
|
||||
}
|
||||
} else {
|
||||
setTool(op.tool, undefined, op.rate);
|
||||
|
|
|
|||
|
|
@ -83,6 +83,10 @@ class OpRough extends CamOp {
|
|||
|
||||
// outside only if we're not clearing all of stock
|
||||
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, {
|
||||
rename: op.rename ?? "cutout",
|
||||
spindle: op.spindle,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class OpTrace extends CamOp {
|
|||
|
||||
async slice(progress) {
|
||||
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 trace = {
|
||||
areas,
|
||||
|
|
@ -21,6 +21,7 @@ class OpTrace extends CamOp {
|
|||
down,
|
||||
expand,
|
||||
follow,
|
||||
ignore,
|
||||
mode: mode === 'clear' ? 'clear' : 'trace',
|
||||
outline,
|
||||
ov_botz,
|
||||
|
|
@ -39,6 +40,7 @@ class OpTrace extends CamOp {
|
|||
tool,
|
||||
thru,
|
||||
tr_over: offover,
|
||||
tr_offz: offz,
|
||||
tr_type: offset
|
||||
};
|
||||
this.op_trace = new OpArea(state, trace);
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
|||
{ alignTop } = settings.controller,
|
||||
{ camArcEnabled, camArcResolution, camArcTolerance } = process,
|
||||
{ camDepthFirst, camEaseAngle, camEaseDown } = process,
|
||||
{ camFastFeed, camFastFeedZ } = process,
|
||||
{ camFastFeed, camFastFeedZ, camZTop } = process,
|
||||
{ camStockX, camStockY, camStockZ, camStockIndexed, camStockOffset } = process,
|
||||
{ camForceZMax, camFullEngage, camInnerFirst, camOriginCenter } = process,
|
||||
{ camOriginOffX, camOriginOffY, camOriginOffZ, camZClearance } = process,
|
||||
|
|
@ -110,7 +110,7 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
|||
wmx = wmpos.x,
|
||||
wmy = wmpos.y,
|
||||
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),
|
||||
originy = (camOriginCenter ? 0 : -stock.y / 2) + (camOriginOffY || 0),
|
||||
origin = newPoint(originx, originy, zSafe),
|
||||
|
|
@ -138,6 +138,7 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
|||
toolType,
|
||||
toolDiam,
|
||||
toolDiamMove,
|
||||
toolDiamEpsilon,
|
||||
travelBounds,
|
||||
spindle = 0,
|
||||
spindleMax = device.spindleMax,
|
||||
|
|
@ -194,6 +195,7 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
|||
}
|
||||
|
||||
function setSpindle(speed) {
|
||||
// console.trace({ setSpindle: speed });
|
||||
spindle = Math.min(speed, spindleMax);
|
||||
}
|
||||
|
||||
|
|
@ -211,6 +213,7 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
|||
toolType = tool.getType();
|
||||
toolDiam = tool.fluteDiameter();
|
||||
toolDiamMove = (tool.hasTaper() ? tolerance ?? toolDiam : toolDiam) * 2;
|
||||
toolDiamEpsilon = toolDiam * 0.01,
|
||||
lastTool = toolID;
|
||||
}
|
||||
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
|
||||
* instead route around the bounding area of the contour
|
||||
* whih we call the coastline.
|
||||
* which we call the coastline.
|
||||
*/
|
||||
function coastlineMove(point) {
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
let minz = Math.min(from.z, to.z);
|
||||
let start = { dist: 1, poly: 0, pt: from };
|
||||
let end = { dist: 1, poly: 1, pt: to };
|
||||
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++) {
|
||||
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;
|
||||
}
|
||||
|
|
@ -567,7 +573,7 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
|||
} else
|
||||
// otherwise move over before descending
|
||||
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);
|
||||
newLayer();
|
||||
}
|
||||
|
|
@ -590,9 +596,11 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
|||
if (lastTravelBounds) check.push(...lastTravelBounds);
|
||||
let from = toWidgetCoords(printPoint);
|
||||
let to = toWidgetCoords(point);
|
||||
let ep = toolDiamEpsilon;
|
||||
for (let poly of check) {
|
||||
let ints = poly.intersections(from, to);
|
||||
if (ints.length) {
|
||||
let ints = poly.intersections(from, to) ?? [];
|
||||
let far = ints.filter(p => p.distTo2D(to) > ep && p.distTo2D(from) > ep);
|
||||
if (far.length) {
|
||||
if (debug) console.log({ ints, poly, deltaXY, deltaZ });
|
||||
upAndOver = "bounds";
|
||||
break;
|
||||
|
|
@ -636,7 +644,7 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
|||
|
||||
// plunge safety catch
|
||||
if (deltaZ < 0 && !contouring) {
|
||||
if (debug) console.log('deltaZ snap', rate, plungeRate);
|
||||
if (debug) console.log('plunge safety', deltaZ, rate, plungeRate);
|
||||
emit = 1;
|
||||
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} 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 depthData = [];
|
||||
|
||||
for (let slice of slices) {
|
||||
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 is child if has 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 }
|
||||
// place poly into top or child bucket
|
||||
if (child) c.push(poly); else t.push(poly);
|
||||
polys.push(poly);
|
||||
});
|
||||
|
|
@ -687,6 +700,7 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
|||
POLY.setWinding(c, !cutdir);
|
||||
|
||||
if (depthFirst) {
|
||||
// re-nest layer polys and add to depth stack
|
||||
polys = POLY.nest(polys,true,true);
|
||||
polys.tool_shadow = POLY.flatten(slice.tool_shadow.clone(true));
|
||||
depthData.push(polys);
|
||||
|
|
@ -706,18 +720,18 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
|||
|
||||
if (depthFirst) {
|
||||
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;
|
||||
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 (inside) {
|
||||
flat = flat.filter(p => p.isNested(inside));
|
||||
flat = flat.filter(p => p.isInside(inside));
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
|
|
@ -736,7 +750,13 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
|||
polyEmit(poly, CLOSEST_TO_PP, engage);
|
||||
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 {
|
||||
return;
|
||||
}
|
||||
|
|
@ -826,20 +846,31 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
|
|||
|
||||
// calculate ease down for poly path output
|
||||
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 lp;
|
||||
for (let i=0; ; i++) {
|
||||
let ii = i % points.length;
|
||||
let len = points.length;
|
||||
let lp, lz = Infinity;
|
||||
// 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];
|
||||
if (zat <= pt.z) {
|
||||
// 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;
|
||||
}
|
||||
if (i > 0) {
|
||||
let dd = lp.distTo2D(pt);
|
||||
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));
|
||||
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 });
|
||||
} else if (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 });
|
||||
} else {
|
||||
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,
|
||||
}, 3);
|
||||
|
||||
// console.log({ bounds, stock, track, workarea });
|
||||
// console.log({ bounds, stock, track, workarea, camZBottom });
|
||||
|
||||
return structuredClone(workarea);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -51,18 +51,20 @@ export class Topo {
|
|||
stepsY = Math.ceil(boundsY / resolution),
|
||||
widtopo = widget.topo,
|
||||
topoCache = widtopo
|
||||
&& widtopo.resolution === resolution
|
||||
&& widtopo.tolerance === tolerance
|
||||
&& widtopo.diameter === toolDiameter
|
||||
? widtopo : undefined,
|
||||
topo = widget.topo = topoCache || {
|
||||
axis,
|
||||
data: new Float32Array(new SharedArrayBuffer(stepsX * stepsY * 4)),
|
||||
stepsX: stepsX,
|
||||
stepsY: stepsY,
|
||||
bounds: bounds,
|
||||
stepsX,
|
||||
stepsY,
|
||||
bounds,
|
||||
diameter: toolDiameter,
|
||||
resolution: resolution,
|
||||
resolution,
|
||||
tolerance,
|
||||
profile: toolOffset,
|
||||
widget: widget,
|
||||
widget,
|
||||
raster: true,
|
||||
slices: null
|
||||
},
|
||||
|
|
@ -120,14 +122,15 @@ export class Topo {
|
|||
}
|
||||
let toolData = { positions: toolPos, bounds: toolBounds };
|
||||
|
||||
const vertices = widget.getGeoVertices({ unroll: true, translate: true });
|
||||
const wbounds = widget.getBoundingBox();
|
||||
let vertices = widget.getGeoVertices({ unroll: true, translate: true });
|
||||
let wbounds = widget.getBoundingBox();
|
||||
if (!inside) {
|
||||
wbounds.expandByVector({ x: toolDiameter/2 + resolution, y: toolDiameter/2 + resolution, z: 0 });
|
||||
}
|
||||
|
||||
// swap XY vertices (unswap later after polylines generated)
|
||||
if (contourY) {
|
||||
vertices = vertices.slice();
|
||||
for (let i=0; i<vertices.length; i+= 3) {
|
||||
let tmp = vertices[i+1];
|
||||
vertices[i+1] = vertices[i+0];
|
||||
|
|
@ -796,7 +799,7 @@ export class Trace {
|
|||
ok++;
|
||||
}
|
||||
}
|
||||
return ok === clips.length;
|
||||
return ok > 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,12 +121,13 @@ export function menu() {
|
|||
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)}),
|
||||
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)}),
|
||||
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 }),
|
||||
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 }),
|
||||
sliceSupportManual: newRow([
|
||||
(ui.ssmAdd = newButton(undefined, onButtonClick, {icon:'<i class="fas fa-plus"></i>'})),
|
||||
|
|
|
|||
|
|
@ -380,6 +380,8 @@ function supportDone() {
|
|||
// Use popVisualState to restore original material
|
||||
w.popVisualState('paint');
|
||||
});
|
||||
api.conf.save();
|
||||
api.space.save();
|
||||
}
|
||||
|
||||
// manual supports clear
|
||||
|
|
|
|||
|
|
@ -577,10 +577,12 @@ export function fdm_export(print, online, ondone, ondebug) {
|
|||
lastFanSpeed = fanSpeed;
|
||||
}
|
||||
if (bedTemp !== lastBedTemp) {
|
||||
// console.log({ bed_temp_change_to: bedTemp, from: lastBedTemp });
|
||||
append(`M140 S${bedTemp} T0`);
|
||||
lastBedTemp = bedTemp;
|
||||
}
|
||||
if (nozzleTemp !== lastNozzleTemp) {
|
||||
// console.log({ temp_change_to: nozzleTemp, from: lastNozzleTemp });
|
||||
if (t0) append(`M104 S${nozzleTemp} T0`);
|
||||
if (t1) append(`M104 S${nozzleTemp} T1`);
|
||||
if (!(t0 || t1)) append(`M104 S${nozzleTemp} T${tool}`);
|
||||
|
|
|
|||
|
|
@ -173,6 +173,29 @@ function vopt(opt, ctx) {
|
|||
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
|
||||
*
|
||||
|
|
@ -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
|
||||
// zGen() produces the list (or empty for slicer auto-detected)
|
||||
slice(points, {
|
||||
|
|
@ -303,6 +330,7 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
|||
xray: process.xray,
|
||||
zMin: bounds.min.z,
|
||||
zMax: bounds.max.z - zCut,
|
||||
bucketMin: minions.concurrent * 5,
|
||||
union: controller.healMesh,
|
||||
indices: process.indices || process.xray,
|
||||
useAssembly,
|
||||
|
|
@ -328,6 +356,9 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
|||
})
|
||||
.then(decodeSlices)
|
||||
.then(processSlices)
|
||||
.then(() => {
|
||||
minions.setPoints([]);
|
||||
})
|
||||
.then(ondone);
|
||||
|
||||
// z index generator (bottom up)
|
||||
|
|
@ -441,17 +472,18 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
|||
}).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()
|
||||
function doupdate(index, from, to, msg) {
|
||||
trackupdate(index / slices.length, from, to, msg);
|
||||
}
|
||||
|
||||
// slicing is the first 50% of the update "time"
|
||||
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()
|
||||
// for each slice, perform a function and call doupdate()
|
||||
function forSlices(from, to, fn, msg) {
|
||||
slices.forEach(slice => {
|
||||
fn(slice);
|
||||
|
|
@ -462,11 +494,12 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
|||
/**
|
||||
* Process automatic and manual shadow-based support generation
|
||||
*/
|
||||
async function processSupports() {
|
||||
async function processSupports(plo, phi) {
|
||||
if (process.sliceSupportType === 'disabled') {
|
||||
return;
|
||||
}
|
||||
|
||||
let div = divide(plo, phi, [ 0.5, 0.5 ]);
|
||||
let stack = slices.slice();
|
||||
let indices = stack.map(s => s.z);
|
||||
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
|
||||
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
|
||||
let { paint } = widget.anno;
|
||||
let { belt } = widget;
|
||||
|
||||
// apply belt transformations, if needed
|
||||
if (belt && paint) {
|
||||
if (manual && belt && paint?.length) {
|
||||
let { anchor, angle, dy, slope } = belt;
|
||||
// make a copy we can modify
|
||||
paint = structuredClone(paint);
|
||||
|
|
@ -496,10 +550,14 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// convert paint points to circles on matching slices
|
||||
if (manual && paint?.length) {
|
||||
let hpi = Math.PI/2;
|
||||
for (let slice of stack) {
|
||||
if (!slice.up) {
|
||||
continue;
|
||||
}
|
||||
let polys = [];
|
||||
for (let rec of paint) {
|
||||
let { point, radius } = rec;
|
||||
|
|
@ -510,31 +568,34 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
|||
polys.push(newPolygon().centerCircle(point, radius, 10));
|
||||
}
|
||||
}
|
||||
slice.shadow = POLY.union(polys, 0, true);
|
||||
}
|
||||
}
|
||||
|
||||
// create automatic shadows supports when on manual paint
|
||||
if (!manual) {
|
||||
await widget.computeShadowStack(indices, progress => {
|
||||
trackupdate(progress, 0.05, 0.10, "shadow");
|
||||
}, zAngNorm);
|
||||
|
||||
for (let slice of stack) {
|
||||
slice.shadow = await widget.shadowAt(slice.z, true);
|
||||
// trim polys to part overhangs
|
||||
let top = slice.up.topPolys();
|
||||
let bot = slice.topPolys();
|
||||
let bridge = [];
|
||||
let propose = POLY.setZ(POLY.union(polys, 0, true), slice.z);
|
||||
POLY.subtract(top, bot, bridge, undefined, slice.z, 0, { wasm: true });
|
||||
propose = POLY.trimTo(propose, bridge, { minArea: 0 });
|
||||
slice.shadow = propose;
|
||||
// if (devel) slice.output().setLayer("over", 0x8844aa).addPolys(bridge);
|
||||
}
|
||||
}
|
||||
|
||||
// 1. accumulate / union shadow coverage top down
|
||||
// 2. trim to area outside slice.clips
|
||||
let minArea = lineWidth;
|
||||
let minArea = lineWidth * lineWidth;
|
||||
let shadowSum;
|
||||
let length = stack.length;
|
||||
let count = 0;
|
||||
|
||||
// convert shadows to trees, when specified
|
||||
if (process.sliceSupportTree) {
|
||||
// console.log('TREE OUTPUT');
|
||||
}
|
||||
|
||||
// perform accumulation top down
|
||||
for (let slice of stack.reverse()) {
|
||||
for (let slice of stack.slice().reverse()) {
|
||||
let shadow = slice.shadow ?? [];
|
||||
if (devel) slice.output().setLayer("shadow", 0xff0000).addPolys(shadow);
|
||||
if (process.sliceSupportExtra) {
|
||||
shadow = POLY.offset(shadow, process.sliceSupportExtra);
|
||||
}
|
||||
|
|
@ -542,14 +603,10 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
|||
if (shadowSum) {
|
||||
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) {
|
||||
let rem = [];
|
||||
let clips = [
|
||||
slice.up?.clips,
|
||||
slice.clips,
|
||||
slice.down?.clips
|
||||
].filter(v => v).flat();
|
||||
let clips = [ slice.topPolys() ].filter(v => v).flat();
|
||||
clips = POLY.union(clips, minArea, true);
|
||||
POLY.subtract(shadow, clips, rem, null, slice.z, minArea, { wasm: false });
|
||||
shadow = rem;
|
||||
|
|
@ -570,11 +627,47 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
|||
.centerRectangle(newPoint(0, 0, slice.z), boundsx, boundsy)
|
||||
.move({ x: 0, y: -boundsy / 2 + skewy, z: 0 });
|
||||
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;
|
||||
if (devel) slice.output().setLayer("shadow", 0xff0000).addPolys(shadow);
|
||||
trackupdate((++count/length), 0.10, 0.15, "support");
|
||||
// if (devel) slice.output().setLayer("shadow", 0xff0000).addPolys(shadow);
|
||||
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
|
||||
* layers detected and marked for solid fill
|
||||
*/
|
||||
async function processSolidLayers() {
|
||||
forSlices(0.15, 0.2, slice => {
|
||||
async function processSolidLayers(plo, phi) {
|
||||
forSlices(plo, phi, slice => {
|
||||
let range = slice.params;
|
||||
let isBottom = slice.index < bottomLayers;
|
||||
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
|
||||
*/
|
||||
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
|
||||
let promises = [];
|
||||
profileStart("delta");
|
||||
forSlices(0.2, 0.33, slice => {
|
||||
forSlices(div[0].lo, div[1].hi, slice => {
|
||||
let params = slice.params || process;
|
||||
let solidMinArea = params.sliceSolidMinArea;
|
||||
let sliceMinThick = params.sliceSolidMinThick;
|
||||
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");
|
||||
await Promise.all(promises);
|
||||
profileEnd();
|
||||
// project bridges and flats up and down into part
|
||||
profileStart("delta-project");
|
||||
forSlices(0.33, 0.34, slice => {
|
||||
forSlices(div[1].lo, div[1].hi, slice => {
|
||||
let params = slice.params || process;
|
||||
topLayers = params.sliceTopLayers || 0;
|
||||
bottomLayers = params.sliceBottomLayers || 0;
|
||||
|
|
@ -628,7 +730,7 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
|||
profileEnd();
|
||||
// union solid areas
|
||||
profileStart("solid-union");
|
||||
forSlices(0.34, 0.35, slice => {
|
||||
forSlices(div[2].lo, div[2].hi, slice => {
|
||||
if (slice.solids) {
|
||||
slice.solids = POLY.union(slice.solids, 0, true);
|
||||
}
|
||||
|
|
@ -639,10 +741,11 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
|||
/**
|
||||
* Process solid fill patterns
|
||||
*/
|
||||
async function processSolidFills() {
|
||||
async function processSolidFills(plo, phi) {
|
||||
profileStart("solid-fill")
|
||||
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 solidWidth = params.sliceFillWidth || 1;
|
||||
let fillSpace = fillSpacing * solidWidth;
|
||||
|
|
@ -654,7 +757,7 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
|||
slices.last().finishSolids = true
|
||||
if (promises) {
|
||||
await tracker(promises, (i, t) => {
|
||||
trackupdate(i / t, 0.4, 0.5);
|
||||
trackupdate(i / t, div[1].lo, div[1].hi);
|
||||
});
|
||||
}
|
||||
profileEnd();
|
||||
|
|
@ -663,10 +766,11 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
|||
/**
|
||||
* Process sparse infill patterns
|
||||
*/
|
||||
async function processSparseInfill() {
|
||||
async function processSparseInfill(plo, phi) {
|
||||
let lastType;
|
||||
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;
|
||||
if (!params.sliceFillSparse) {
|
||||
return;
|
||||
|
|
@ -689,7 +793,7 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
|||
}, "infill");
|
||||
if (promises) {
|
||||
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
|
||||
|
|
@ -715,10 +819,11 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
|||
/**
|
||||
* Process support structure fills
|
||||
*/
|
||||
async function processSupportFills() {
|
||||
async function processSupportFills(plo, phi) {
|
||||
profileStart("support-fill");
|
||||
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 density = params.sliceSupportDensity;
|
||||
layerSupportFill({
|
||||
|
|
@ -733,7 +838,7 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
|||
}, "support fill");
|
||||
if (promises) {
|
||||
await tracker(promises, (i, t) => {
|
||||
trackupdate(i / t, 0.88, 0.9);
|
||||
trackupdate(i / t, div[1].lo, div[1].hi);
|
||||
});
|
||||
}
|
||||
profileEnd();
|
||||
|
|
@ -788,8 +893,8 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
|||
slices.forEach((s,i) => s.index = i);
|
||||
}
|
||||
|
||||
async function renderSlices() {
|
||||
forSlices(0.9, 1.0, slice => {
|
||||
async function renderSlices(plo, phi) {
|
||||
forSlices(plo, phi, slice => {
|
||||
let params = slice.params || process;
|
||||
layerRender(slice, params, {
|
||||
dark: controller.dark,
|
||||
|
|
@ -850,7 +955,7 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
|||
}
|
||||
|
||||
// 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)
|
||||
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
|
||||
// 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
|
||||
|
|
@ -894,24 +995,27 @@ export function sliceOne(settings, widget, onupdate, ondone) {
|
|||
// for "real" objects, fill the remaining voids with sparse fill
|
||||
// sparse layers only present when non-vase mode and sparse % > 0
|
||||
if (!vaseMode) {
|
||||
await processLayerDiffs();
|
||||
await processSolidFills();
|
||||
await processSparseInfill();
|
||||
await processLayerDiffs(0.2, 0.4);
|
||||
// support generation using either
|
||||
// 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)
|
||||
if (supportDensity) {
|
||||
await processSupportFills();
|
||||
await processSupportFills(0.8, 0.84);
|
||||
}
|
||||
|
||||
// brick/interleave mode processing
|
||||
if (isBrick) {
|
||||
await processBrickMode();
|
||||
await processBrickMode(0.84, 0.85);
|
||||
}
|
||||
|
||||
// render if not explicitly disabled
|
||||
if (render) {
|
||||
await renderSlices();
|
||||
await renderSlices(0.85, 1.0);
|
||||
}
|
||||
|
||||
if (isBelt) {
|
||||
|
|
@ -1086,6 +1190,7 @@ export function slicePost(settings, onupdate) {
|
|||
// assign grid_id which can be embedded in gcode and
|
||||
// used by the controller to cancel objects during print
|
||||
let { bounds } = settings;
|
||||
if (!bounds) return;
|
||||
for (let widget of widgets) {
|
||||
let { pos, box } = widget.track;
|
||||
// calculate top/left coordinate for widget
|
||||
|
|
@ -1375,19 +1480,36 @@ export function layerDiff(slice, options = {}) {
|
|||
let newBridges = [];
|
||||
let newFlats = [];
|
||||
|
||||
if (options.async) {
|
||||
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 });
|
||||
newBridges = newBridges.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) {
|
||||
newBridges = POLY.offset(newBridges, grow);
|
||||
newBridges = POLY.offset(newBridges, grow, { z: slice.z });
|
||||
}
|
||||
if (grow > 0 && newFlats.length) {
|
||||
newFlats = POLY.offset(newFlats, grow);
|
||||
newFlats = POLY.offset(newFlats, grow, { z: slice.z });
|
||||
}
|
||||
|
||||
bridges.appendAll(newBridges);
|
||||
|
|
|
|||
|
|
@ -19,13 +19,15 @@ class Engine {
|
|||
filter: { FDM: "internal" },
|
||||
device: conf.defaults.fdm.d, // device profile
|
||||
process: conf.defaults.fdm.p, // slicing settings
|
||||
widget: { [this.widget.id]: {} }
|
||||
widget: { [this.widget.id]: {} },
|
||||
time: Date.now()
|
||||
};
|
||||
this.listener = () => { };
|
||||
try {
|
||||
client.setWorkPath(workURL);
|
||||
client.setPoolPath(poolURL);
|
||||
client.restart();
|
||||
client.pool.start();
|
||||
} catch (error) {
|
||||
console.log({ error });
|
||||
}
|
||||
|
|
@ -37,6 +39,7 @@ class Engine {
|
|||
new load.STL().load(url, vertices => {
|
||||
this.listener({ loaded: url, vertices });
|
||||
this.widget.loadVertices(vertices).center();
|
||||
this.setTopOffset(0);
|
||||
accept(this);
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
@ -49,6 +52,10 @@ class Engine {
|
|||
api.platform.clear();
|
||||
}
|
||||
|
||||
workspace() {
|
||||
return api.settings.export({ engine: this.settings });
|
||||
}
|
||||
|
||||
parse(data) {
|
||||
return new Promise((accept, reject) => {
|
||||
try {
|
||||
|
|
@ -63,11 +70,8 @@ class Engine {
|
|||
}
|
||||
|
||||
setThreading(bool) {
|
||||
if (bool) {
|
||||
client.pool.start();
|
||||
} else {
|
||||
client.pool.stop();
|
||||
}
|
||||
console.log('setThreading() deprecated');
|
||||
return this;
|
||||
}
|
||||
|
||||
setListener(listener) {
|
||||
|
|
@ -86,7 +90,15 @@ class Engine {
|
|||
* @returns {Engine} this
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -123,17 +135,24 @@ class Engine {
|
|||
process.camStockX = stock.x;
|
||||
process.camStockY = stock.y;
|
||||
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;
|
||||
}
|
||||
|
||||
setTopOffset(offset = 0) {
|
||||
this.topOffset = offset;
|
||||
let wbb = this.widget.getBoundingBox();
|
||||
this.widget.setTopZ(wbb.max.z - offset);
|
||||
return this;
|
||||
}
|
||||
|
||||
setOrigin(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;
|
||||
}
|
||||
|
||||
|
|
@ -158,11 +177,11 @@ class Engine {
|
|||
}
|
||||
|
||||
slice() {
|
||||
this.widget.setTopZ((this.settings?.stock?.z || 0) - (this.topOffset || 0));
|
||||
return new Promise((accept, reject) => {
|
||||
client.clear();
|
||||
client.sync([this.widget]);
|
||||
client.rotate(this.settings);
|
||||
client.slicePre(this.settings, () => {});
|
||||
client.slice(this.settings, this.widget, msg => {
|
||||
this.listener({ slice: msg });
|
||||
if (msg.error) {
|
||||
|
|
@ -170,6 +189,7 @@ class Engine {
|
|||
}
|
||||
if (msg.done) {
|
||||
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) {
|
||||
if (!(data.polys && data.polys.length)) {
|
||||
reply({ seq, union: codec.encode([]) });
|
||||
|
|
@ -140,14 +154,11 @@ const funcs = self.minion = {
|
|||
|
||||
sliceZ(data, seq) {
|
||||
debug('minion.sliceZ', { data, seq });
|
||||
let { z, points, options } = 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);
|
||||
}
|
||||
let { z, options } = data;
|
||||
let { points } = cache;
|
||||
let state = { zero: [] };
|
||||
let output = [];
|
||||
sliceZ(z, realp, {
|
||||
sliceZ(z, points, {
|
||||
...options,
|
||||
each(out) { output.push(out) }
|
||||
}).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) {
|
||||
const { key, data } = msg;
|
||||
// log({ minion_putCache: key, data });
|
||||
if (data) {
|
||||
cache[key] = data;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ let drivers = {
|
|||
WJET
|
||||
},
|
||||
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 = {
|
||||
print: null,
|
||||
snap: null,
|
||||
|
|
@ -101,6 +101,9 @@ function minhandler(msg) {
|
|||
|
||||
// for concurrent operations
|
||||
const minwork = {
|
||||
|
||||
// core functions
|
||||
|
||||
get concurrent() {
|
||||
return concurrent
|
||||
},
|
||||
|
|
@ -137,6 +140,76 @@ const minwork = {
|
|||
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) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (concurrent < 2 || polys.length < concurrent * 2 || POLY.points(polys) < concurrent * 50) {
|
||||
|
|
@ -222,17 +295,9 @@ const minwork = {
|
|||
reject("concurrent slice unavaiable");
|
||||
}
|
||||
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({
|
||||
cmd: "sliceZ",
|
||||
z,
|
||||
points: floatP,
|
||||
options: codec.toCodable(options)
|
||||
}, data => {
|
||||
let recs = codec.decode(data.output);
|
||||
|
|
@ -242,43 +307,9 @@ const minwork = {
|
|||
}
|
||||
}
|
||||
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"}`);
|
||||
|
|
@ -586,9 +617,9 @@ const dispatch = {
|
|||
const { process } = settings;
|
||||
const origin = settings.origin;
|
||||
const offset = {
|
||||
x: origin.x - (process.camOriginOffX ?? 0),
|
||||
y: -origin.y - (process.camOriginOffY ?? 0),
|
||||
z: origin.z - (process.camOriginOffZ ?? 0)
|
||||
x: origin.x,// - (process.camOriginOffX ?? 0),
|
||||
y: -origin.y,// - (process.camOriginOffY ?? 0),
|
||||
z: origin.z,// + (process.camOriginOffZ ?? 0)
|
||||
};
|
||||
const device = settings.device;
|
||||
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 TMF from './3mf.js';
|
||||
import * as SVG from './svg.js';
|
||||
import * as DXF from './dxf.js';
|
||||
import * as GBR from './gbr.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 } }));
|
||||
},
|
||||
|
||||
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 = {}) {
|
||||
pngLoad.PNG.parse(data, {
|
||||
...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 };
|
||||
|
|
|
|||
116
src/load/stl.js
116
src/load/stl.js
|
|
@ -13,14 +13,26 @@ const { Vector3, computeFaceNormal } = THREE;
|
|||
* @returns {Uint8Array} binary STL data
|
||||
*/
|
||||
export function encode(recs, header = '') {
|
||||
// Calculate total vertices
|
||||
let vtot = 0;
|
||||
for (let rec of recs) {
|
||||
vtot += (rec.varr.length / 3);
|
||||
// Calculate total triangles with strict validation.
|
||||
let triCount = 0;
|
||||
for (let rec of recs || []) {
|
||||
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)
|
||||
let stl = new Uint8Array(80 + 4 + vtot/3 * 50);
|
||||
let stl = new Uint8Array(byteLen);
|
||||
let dat = new DataView(stl.buffer);
|
||||
let pos = 84;
|
||||
|
||||
|
|
@ -32,11 +44,12 @@ export function encode(recs, header = '') {
|
|||
}
|
||||
|
||||
// Write triangle count at byte 80
|
||||
dat.setInt32(80, vtot/3, true);
|
||||
dat.setUint32(80, triCount, true);
|
||||
|
||||
// Write triangles
|
||||
for (let rec of recs) {
|
||||
let { varr } = rec;
|
||||
if (!varr || (varr.length % 9) !== 0) continue;
|
||||
for (let i = 0, l = varr.length; i < l;) {
|
||||
// Read three vertices
|
||||
let p0 = new Vector3(varr[i++], varr[i++], varr[i++]);
|
||||
|
|
@ -72,6 +85,97 @@ export function encode(recs, header = '') {
|
|||
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 {
|
||||
constructor() {
|
||||
this.vertices = null;
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export function parse(text, opt = { }) {
|
|||
if (points.length < 3) {
|
||||
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 (type === 'polyline') poly.setOpen(true);
|
||||
poly._svg = { width, miter };
|
||||
|
|
|
|||
|
|
@ -2,11 +2,9 @@
|
|||
|
||||
'use strict';
|
||||
|
||||
import { STL } from './stl.js';
|
||||
import { OBJ } from './obj.js';
|
||||
import { TMF } from './3mf.js';
|
||||
import { SVG } from './svg.js';
|
||||
import { load_file } from './file.js';
|
||||
|
||||
const { STL, OBJ, TMF, SVG, DXF } = load_file;
|
||||
const CDH = 'Content-Disposition';
|
||||
|
||||
export function load_url(url, options = {}) {
|
||||
|
|
@ -14,7 +12,7 @@ export function load_url(url, options = {}) {
|
|||
let xhr = new XMLHttpRequest();
|
||||
let file = options.file || options.filename || (((url.split('?')[0]).split('#')[0]).split('/')).pop();
|
||||
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 formdata = options.formdata;
|
||||
|
||||
|
|
@ -57,6 +55,9 @@ export function load_url(url, options = {}) {
|
|||
case "svg":
|
||||
resolve(SVG.parse(data).map(m => { return {mesh: m.toFloat32(), file} }));
|
||||
break;
|
||||
case "dxf":
|
||||
resolve(DXF.parse(data).map(m => { return {mesh: m.toFloat32(), file} }));
|
||||
break;
|
||||
default:
|
||||
reject(`unknown file type: "${ext}" from ${url}`);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { api } from '../kiri/app/api.js';
|
|||
import { init_lang } from '../kiri/app/init/lang.js';
|
||||
import { init_input } from '../kiri/app/init/input.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 load = [];
|
||||
|
|
@ -36,6 +37,7 @@ async function checkReady() {
|
|||
{
|
||||
api.client.start();
|
||||
await init_lang();
|
||||
surfaces.build();
|
||||
await init_input();
|
||||
await init_sync();
|
||||
}
|
||||
|
|
@ -44,11 +46,11 @@ async function checkReady() {
|
|||
}
|
||||
load = undefined;
|
||||
api.event.emit('load-done', stats);
|
||||
api.event.emit('resize');
|
||||
if (api.electron) {
|
||||
$('install').classList.add('hide');
|
||||
$('app-quit').classList.remove('hide');
|
||||
$('app-name-text').innerText = "More Info";
|
||||
$('top-sep').style.display = 'flex';
|
||||
[...document.getElementsByClassName('el-app-hide')].forEach(el => el.classList.add('hide'));
|
||||
} else if (bootctrl) {
|
||||
$('install').classList.add('hide');
|
||||
$('uninstall').classList.remove('hide');
|
||||
|
|
@ -57,6 +59,7 @@ async function checkReady() {
|
|||
location.reload();
|
||||
}
|
||||
} else {
|
||||
[...document.getElementsByClassName('app-hide')].forEach(el => el.classList.add('hide'));
|
||||
$('install').onclick = () => {
|
||||
location.replace('/boot');
|
||||
}
|
||||
|
|
|
|||
347
src/main/mesh.js
347
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 { load as fileLoad } from '../load/file.js';
|
||||
import { THREE } from '../ext/three.js';
|
||||
import { createDocumentManager } from '../mesh/document.js';
|
||||
|
||||
const version = '1.5.7';
|
||||
const call = broker.send;
|
||||
const dbindex = [ "admin", "space" ];
|
||||
const dbindex = [ "admin", "documents", "versions" ];
|
||||
const DOC_META_KEY = '__doc';
|
||||
|
||||
const { Quaternion } = THREE;
|
||||
|
||||
|
|
@ -28,10 +30,67 @@ function log() {
|
|||
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
|
||||
// this is the main() entrypoint called after all dependents load
|
||||
function init() {
|
||||
let stores = dataOpen('mesh', { stores: dbindex, version: 4 }).init(),
|
||||
let stores = dataOpen('mesh', { stores: dbindex, version: 5 }).init(),
|
||||
dark = false,
|
||||
ortho = false,
|
||||
zoomrev = true,
|
||||
|
|
@ -39,11 +98,21 @@ function init() {
|
|||
platform = space.platform,
|
||||
db = api.db = {
|
||||
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)
|
||||
api.init();
|
||||
boot_start('initializing mesh:tool');
|
||||
|
||||
// mark init time and use count
|
||||
db.admin.put("init", Date.now());
|
||||
|
|
@ -70,26 +139,23 @@ function init() {
|
|||
colorX: 0xff7777, colorY: 0x7777ff },
|
||||
});
|
||||
platform.onMove(() => {
|
||||
// save last location and focus
|
||||
db.admin.put('camera', {
|
||||
place: space.view.save(),
|
||||
focus: space.view.getFocus()
|
||||
});
|
||||
// save camera per-document
|
||||
save_camera_to_document();
|
||||
}, 100);
|
||||
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
|
||||
motoClient.on('ready', restore_space);
|
||||
|
||||
// start worker
|
||||
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
|
||||
let wlp = window.location.pathname;
|
||||
let mio = wlp.indexOf('/mesh/');
|
||||
|
|
@ -102,32 +168,27 @@ function init() {
|
|||
self.electron = navigator.userAgent.includes('Electron');
|
||||
}
|
||||
|
||||
// restore space layout and view from previous session
|
||||
async function restore_space() {
|
||||
const db_admin = api.db.admin;
|
||||
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);
|
||||
function clear_workspace() {
|
||||
api.selection.clear();
|
||||
for (let sk of api.sketch.list().slice()) {
|
||||
sk.remove();
|
||||
}
|
||||
});
|
||||
const mcache = await db_admin.get("meta") || {};
|
||||
for (let grp of api.group.list().slice()) {
|
||||
grp.remove();
|
||||
}
|
||||
}
|
||||
|
||||
async function restore_workspace_from_state(cached = {}, mcache = {}) {
|
||||
const db_space = api.db.space;
|
||||
let count = 0;
|
||||
await db_space.iterate({ map: true }).then(cached => {
|
||||
await Promise.resolve(cached).then(cached => {
|
||||
const keys = [];
|
||||
const claimed = [];
|
||||
for (let [id, data] of Object.entries(cached)) {
|
||||
// console.log({ id, data });
|
||||
keys.push(id);
|
||||
if (count++ === 0) {
|
||||
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)) {
|
||||
claimed.push(id);
|
||||
let models = data
|
||||
|
|
@ -135,11 +196,11 @@ async function restore_space() {
|
|||
claimed.push(id);
|
||||
return { id, md: cached[id] }
|
||||
})
|
||||
.filter(r => r.md) // filter cache misses
|
||||
.map(r => new meshModel(r.md, r.id).applyMeta(mcache[r.id]))
|
||||
.filter(r => r.md)
|
||||
.map(r => new meshModel(r.md, r.id).applyMeta(mcache[r.id]));
|
||||
if (models.length) {
|
||||
log(`restored ${models.length} model(s)`);
|
||||
api.group.new(models, id).applyMeta(mcache[id])
|
||||
api.group.new(models, id).applyMeta(mcache[id]);
|
||||
} else {
|
||||
log(`removed empty group ${id}`);
|
||||
db_space.remove(id);
|
||||
|
|
@ -155,22 +216,17 @@ async function restore_space() {
|
|||
if (keys.length) {
|
||||
log(`removing ${keys.length} unclaimed meshes`);
|
||||
}
|
||||
// clear out meshes left in the space db along with their meta-data
|
||||
for (let id of keys) {
|
||||
db_space.remove(id);
|
||||
delete mcache[id];
|
||||
}
|
||||
// restore global cache only after objects are restored
|
||||
// otherwise their setup will corrupt the cache for other restores
|
||||
metaCache = mcache;
|
||||
store_meta();
|
||||
api.document.setMeta(metaCache);
|
||||
}).then(() => {
|
||||
// restore preferences after models are restored
|
||||
return api.prefs.load().then(() => {
|
||||
let { map } = api.prefs;
|
||||
let { space, mode } = map;
|
||||
api.grid(space.grid);
|
||||
// restore selected state
|
||||
let selist = space.select || [];
|
||||
let smodel = api.model.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 sklist = api.sketch.list().filter(s => selist.contains(s.id));
|
||||
api.selection.set([...smodel, ...sgroup, ...sklist], [...tmodel, ...tgroup]);
|
||||
// restore edit mode
|
||||
api.mode[mode]();
|
||||
// restore dark mode
|
||||
set_darkmode(map.space.dark);
|
||||
set_darkmode();
|
||||
});
|
||||
}).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
|
||||
$d('curtain','none');
|
||||
boot_done();
|
||||
// restore handles visibility
|
||||
handles.setEnabled(api.prefs.map.space.bounds ?? false);
|
||||
// restore script if was showing
|
||||
|
|
@ -196,15 +276,103 @@ async function restore_space() {
|
|||
if (api.prefs.map.info.welcome !== false) {
|
||||
api.welcome(version);
|
||||
}
|
||||
api.file.set_doc_name(currentDoc?.name || 'Untitled');
|
||||
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
|
||||
function space_init(data) {
|
||||
let platcolor = 0x00ff00;
|
||||
let { space, platform } = data;
|
||||
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
|
||||
space.event.addHandlers(self, [
|
||||
'drop', (evt) => {
|
||||
|
|
@ -222,6 +390,10 @@ function space_init(data) {
|
|||
'dragleave', evt => {
|
||||
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 => {
|
||||
if (api.modal.showing) {
|
||||
return;
|
||||
|
|
@ -229,7 +401,8 @@ function space_init(data) {
|
|||
if (evt.key === '?') {
|
||||
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') {
|
||||
api.script.changed();
|
||||
return;
|
||||
|
|
@ -250,7 +423,11 @@ function space_init(data) {
|
|||
case 'KeyB':
|
||||
return selection.boundsBox({toggle:true});
|
||||
case 'KeyC':
|
||||
if (shiftKey) {
|
||||
return selection.floor();
|
||||
} else {
|
||||
return selection.centerXY().focus();
|
||||
}
|
||||
case 'KeyD':
|
||||
return shiftKey && api.tool.duplicate();
|
||||
case 'KeyE':
|
||||
|
|
@ -259,14 +436,14 @@ function space_init(data) {
|
|||
return api.sketch.extrude();
|
||||
}
|
||||
return;
|
||||
case 'KeyF':
|
||||
return shiftKey ? selection.focus() : selection.floor().focus();
|
||||
case 'KeyG':
|
||||
return shiftKey ?
|
||||
(api.mode.is([ api.modes.sketch ]) ? api.sketch.arrange.group() : api.tool.regroup()) :
|
||||
api.grid();
|
||||
case 'KeyH':
|
||||
return shiftKey ? selection.hide() : space.view.home();
|
||||
if (shiftKey) return selection.hide();
|
||||
schedule_camera_save(180);
|
||||
return space.view.home();
|
||||
case 'KeyI':
|
||||
return api.file.import();
|
||||
case 'KeyL':
|
||||
|
|
@ -283,7 +460,9 @@ function space_init(data) {
|
|||
if (!api.mode.is([ api.modes.object ])) return;
|
||||
return shiftKey ? selection.visible({toggle:true}) : meshSplit.start();
|
||||
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':
|
||||
return shiftKey && api.tool.union();
|
||||
case 'KeyV':
|
||||
|
|
@ -295,7 +474,9 @@ function space_init(data) {
|
|||
}
|
||||
},
|
||||
'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 (code === 'Tab') {
|
||||
estop(evt);
|
||||
|
|
@ -320,14 +501,36 @@ function space_init(data) {
|
|||
delete keyOnce[code];
|
||||
return once(evt);
|
||||
}
|
||||
let rv = (Math.PI / 12);
|
||||
if (api.modal.showing) {
|
||||
if (code === 'Escape') {
|
||||
api.modal.cancel();
|
||||
}
|
||||
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) {
|
||||
case 'KeyA':
|
||||
estop(evt);
|
||||
|
|
@ -353,7 +556,9 @@ function space_init(data) {
|
|||
if (metaKey || ctrlKey) {
|
||||
return shiftKey ? api.history.redo() : api.history.undo();
|
||||
} else {
|
||||
return space.view.reset();
|
||||
space.view.reset();
|
||||
schedule_camera_save(220);
|
||||
return;
|
||||
}
|
||||
case 'Escape':
|
||||
if (selection.clear()) {
|
||||
|
|
@ -664,7 +869,7 @@ function key_once_cancel(code) {
|
|||
}
|
||||
|
||||
function store_meta() {
|
||||
api.db.admin.put("meta", metaCache);
|
||||
api.document?.setMeta?.(metaCache);
|
||||
}
|
||||
|
||||
function update_meta(id, data) {
|
||||
|
|
@ -694,28 +899,19 @@ function object_destroy(id) {
|
|||
function set_darkmode(dark) {
|
||||
let { prefs, model } = api;
|
||||
let { sky, platform } = space;
|
||||
prefs.map.space.dark = dark;
|
||||
if (dark) {
|
||||
dark = true;
|
||||
prefs.map.space.dark = true;
|
||||
materials.wireframe.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({
|
||||
color: dark ? 0 : 0xffffff,
|
||||
ambient: { intensity: dark ? 0.55 : 1.1 }
|
||||
color: 0,
|
||||
ambient: { intensity: 0.55 }
|
||||
});
|
||||
platform.set({
|
||||
light: dark ? 0.08 : 0.08,
|
||||
grid: dark ? {
|
||||
light: 0.08,
|
||||
grid: {
|
||||
colorMajor: 0x666666,
|
||||
colorMinor: 0x333333,
|
||||
} : {
|
||||
colorMajor: 0xcccccc,
|
||||
colorMinor: 0xeeeeee,
|
||||
},
|
||||
});
|
||||
api.updateFog();
|
||||
|
|
@ -741,11 +937,8 @@ function set_normals_length(length) {
|
|||
function set_normals_color(color) {
|
||||
let { prefs, model } = api;
|
||||
let { map } = prefs;
|
||||
if (map.space.dark) {
|
||||
map.normals.color_dark = color || 0;
|
||||
} else {
|
||||
map.normals.color_lite = color || 0;
|
||||
}
|
||||
prefs.save();
|
||||
// Update existing normals
|
||||
for (let m of model.list()) {
|
||||
|
|
@ -809,7 +1002,9 @@ broker.listeners({
|
|||
set_surface_radius,
|
||||
set_wireframe_opacity,
|
||||
set_wireframe_fog,
|
||||
set_snap_value
|
||||
set_snap_value,
|
||||
document_new,
|
||||
document_open
|
||||
});
|
||||
|
||||
init();
|
||||
|
|
@ -837,5 +1032,7 @@ export {
|
|||
set_surface_radius,
|
||||
set_wireframe_opacity,
|
||||
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();
|
||||
}
|
||||
111
src/mesh/api.js
111
src/mesh/api.js
|
|
@ -972,6 +972,93 @@ let add = {
|
|||
};
|
||||
|
||||
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() {
|
||||
// binding created in mesh.build
|
||||
$('import').click();
|
||||
|
|
@ -1348,7 +1435,7 @@ const mode = {
|
|||
$(`mode-${key}`).classList.remove('selected');
|
||||
}
|
||||
$(`mode-${mode}`).classList.add('selected');
|
||||
$('mode-label').innerText = mode;
|
||||
$('top-mode-label').innerText = mode;
|
||||
api.mode.check();
|
||||
meshEdges?.end();
|
||||
if (mode === 'sketch') {
|
||||
|
|
@ -1572,11 +1659,31 @@ const api = {
|
|||
|
||||
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() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// @param object {MeshObject | MeshObject[] | Object}
|
||||
|
|
@ -1701,6 +1808,8 @@ const api = {
|
|||
|
||||
sketch,
|
||||
|
||||
space: motoSpace,
|
||||
|
||||
tool,
|
||||
|
||||
isDebug: self.debug === true
|
||||
|
|
|
|||
|
|
@ -19,7 +19,10 @@ let deg = Math.PI / 180;
|
|||
let und = undefined;
|
||||
|
||||
broker.listeners({
|
||||
ui_build
|
||||
ui_build,
|
||||
app_ready() {
|
||||
api.file?.set_doc_name?.(api.document?.current?.name || 'Untitled');
|
||||
}
|
||||
});
|
||||
|
||||
let spin_timer;
|
||||
|
|
@ -240,14 +243,10 @@ api.welcome = function(version = "unknown") {
|
|||
api.settings = function() {
|
||||
const { prefs } = api;
|
||||
const { surface, normals, space, sketch, wireframe } = prefs.map;
|
||||
const { dark } = space;
|
||||
const dark = true;
|
||||
|
||||
const set1 = div([
|
||||
label('dark mode'),
|
||||
input({ type: "checkbox",
|
||||
onchange: ev => call.set_darkmode(ev.target.checked),
|
||||
[ dark ? 'checked' : 'unchecked' ] : 1
|
||||
}),
|
||||
label({ class: "header", _: 'auto'}),
|
||||
label('auto floor'),
|
||||
input({ type: "checkbox",
|
||||
onchange: ev => prefs.save( space.floor = !space.floor ),
|
||||
|
|
@ -391,20 +390,27 @@ function ui_build() {
|
|||
|
||||
// top left drop menus
|
||||
bind($('top-left'), [
|
||||
div({ _: 'Mesh:Tool', class: "title" }),
|
||||
div({ class: "menubar-separator" }),
|
||||
div({ class: "menu" }, [
|
||||
div('File'),
|
||||
div({ class: "menu-items" }, [
|
||||
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) }
|
||||
}),
|
||||
menu_item('New', file.new),
|
||||
menu_item('Open', file.open),
|
||||
hr(),
|
||||
menu_item('Import', file.import, 'I'),
|
||||
menu_item('Export', file.export, 'X'),
|
||||
hr(),
|
||||
menu_item('Slicer', api.kirimoto),
|
||||
menu_item('Script', api.script.toggle),
|
||||
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" }, [
|
||||
|
|
@ -467,7 +473,6 @@ function ui_build() {
|
|||
menu_item('Face', mode.face, '5', 'mode-face'),
|
||||
menu_item('Edge', mode.edge, '6', 'mode-edge'),
|
||||
]),
|
||||
div({ id: "mode-label" })
|
||||
]),
|
||||
div({ class: "menu sketch-on" }, [
|
||||
div('Items'),
|
||||
|
|
@ -510,7 +515,7 @@ function ui_build() {
|
|||
div({ class: "menu sketch-off" }, [
|
||||
div('Faces'),
|
||||
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('To Sketch', tool.toSketch),
|
||||
hr(),
|
||||
|
|
@ -535,6 +540,7 @@ function ui_build() {
|
|||
])
|
||||
]),
|
||||
div({ class: "menu" }, [
|
||||
// div({ class: "fas fa-question" }),
|
||||
div('Help'),
|
||||
div({ class: "menu-items" }, [
|
||||
menu_item('About', () => { api.welcome(version) }),
|
||||
|
|
@ -546,15 +552,15 @@ function ui_build() {
|
|||
menu_item('Versions', api.version),
|
||||
])
|
||||
]),
|
||||
div({ class: "menubar-separator" }),
|
||||
div({ id: "top-mode-label" }),
|
||||
]);
|
||||
|
||||
// add help buttons
|
||||
bind($('top-right'), [
|
||||
div({ id: "top-settings", onclick: api.settings }, [
|
||||
div({ class: "fas fa-gear" }),
|
||||
div('Settings')
|
||||
]),
|
||||
div({ id: "top-doc-name", onclick: () => api.file.rename(), _: 'Untitled' }),
|
||||
]);
|
||||
api.file?.set_doc_name?.(api.document?.current?.name || 'Untitled');
|
||||
|
||||
// modal dialog and page blocker
|
||||
bind($('modal_page'), [
|
||||
|
|
@ -600,32 +606,55 @@ function ui_build() {
|
|||
return div({ onclick: fn, class: "tool" }, [ bicon(icon), div([ label(help) ]) ]);
|
||||
}
|
||||
|
||||
function toolbar_separator() {
|
||||
return div({ class: "toolbar-separator" });
|
||||
}
|
||||
|
||||
// bind sketch chiclets
|
||||
bind(sketchtools, div([
|
||||
tool_item('bi-plus', 'New Sketch', add.sketch),
|
||||
toolbar_separator(),
|
||||
tool_item('bi-circle', 'Add Circle', api.add.circle),
|
||||
toolbar_separator(),
|
||||
tool_item('bi-square', 'Add Rectangle', api.add.rectangle),
|
||||
toolbar_separator(),
|
||||
tool_item('bi-symmetry-vertical', 'Flip Horizontal', api.sketch.arrange.fliph),
|
||||
toolbar_separator(),
|
||||
tool_item('bi-symmetry-horizontal', 'Flip Vertical', api.sketch.arrange.flipv),
|
||||
toolbar_separator(),
|
||||
tool_item('bi-arrow-clockwise', 'Rotate', api.sketch.arrange.rotate),
|
||||
toolbar_separator(),
|
||||
tool_item('bi-union', 'Union', sketch.boolean.union),
|
||||
toolbar_separator(),
|
||||
tool_item('bi-intersect', 'Intersect', sketch.boolean.intersect),
|
||||
toolbar_separator(),
|
||||
tool_item('bi-exclude', 'Difference', sketch.boolean.difference),
|
||||
toolbar_separator(),
|
||||
tool_item('bi-pip', 'Nest', sketch.boolean.nest),
|
||||
toolbar_separator(),
|
||||
tool_item('bi-layers', 'Flatten', sketch.boolean.flatten),
|
||||
toolbar_separator(),
|
||||
tool_item('bi-cookie', 'Even Odd', sketch.boolean.evenodd),
|
||||
toolbar_separator(),
|
||||
tool_item('bi-arrow-bar-up', 'Extrude', () => sketch.extrude()),
|
||||
]));
|
||||
|
||||
// bind object chiclets
|
||||
bind(objecttools, div([
|
||||
tool_item('bi-pencil', 'New Sketch', add.sketch),
|
||||
toolbar_separator(),
|
||||
tool_item('bi-box', 'New Cube', add.cube),
|
||||
toolbar_separator(),
|
||||
tool_item('bi-database', 'New Cylinder', add.cylinder),
|
||||
toolbar_separator(),
|
||||
tool_item('bi-gear', 'New Gear', add.gear),
|
||||
toolbar_separator(),
|
||||
tool_item('bi-union', 'Union', tool.union),
|
||||
toolbar_separator(),
|
||||
tool_item('bi-subtract', 'Subtract', tool.subtract),
|
||||
toolbar_separator(),
|
||||
tool_item('bi-intersect', 'Intersect', tool.intersect),
|
||||
toolbar_separator(),
|
||||
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';
|
||||
const { Matrix4, Matrix3, Vector3, Box3 } = THREE;
|
||||
import { license as motoLicense } from '../moto/license.js';
|
||||
|
||||
// geometry helper functions
|
||||
const geom = {
|
||||
|
|
|
|||
|
|
@ -929,7 +929,7 @@ class MeshTool {
|
|||
}
|
||||
|
||||
pitch = p.round(3);
|
||||
geom.log(`gear pitch radius: ${pitch}`);
|
||||
log(`gear pitch radius: ${pitch}`);
|
||||
}
|
||||
|
||||
return { gear, pitch };
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ function log(msg) {
|
|||
return worker.publish("mesh.log", msg);
|
||||
}
|
||||
|
||||
self.log = log;
|
||||
|
||||
function cacheUpdate(id, data) {
|
||||
return Object.assign(cache[id], data);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
const terms = {
|
||||
COPYRIGHT: "Copyright (C) Stewart Allen <sa@grid.space> - All Rights Reserved",
|
||||
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 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
|
||||
};
|
||||
|
||||
// Onshape
|
||||
this.mouseVoid = {
|
||||
ORBIT: MOUSE.RIGHT,
|
||||
// ZOOM: MOUSE.LEFT,
|
||||
PAN: MOUSE.MIDDLE
|
||||
};
|
||||
|
||||
this.mouseButtons = this.mouseDefault;
|
||||
|
||||
this.setMouse = function(bindings) {
|
||||
|
|
@ -116,6 +123,7 @@ class Orbit extends EventDispatcher {
|
|||
pan = new Vector3(),
|
||||
lastPosition = new Vector3(),
|
||||
lastQuaternion = new Quaternion(),
|
||||
lastZoom = object.zoom !== undefined ? object.zoom : 1,
|
||||
// so camera.up is the orbit axis
|
||||
quat = new Quaternion().setFromUnitVectors(object.up, new Vector3(0, 1, 0)),
|
||||
quatInverse = quat.clone().invert(),
|
||||
|
|
@ -227,15 +235,33 @@ class Orbit extends EventDispatcher {
|
|||
this.setPosition = function(set) {
|
||||
thetaSet = firstValue([set.left, set.theta, thetaSet]);
|
||||
phiSet = firstValue([set.up, set.phi, phiSet]);
|
||||
if (set.panX !== undefined) this.target.x = set.panX;
|
||||
if (set.panY !== undefined) this.target.y = set.panY;
|
||||
if (set.panZ !== undefined) this.target.z = set.panZ;
|
||||
let target = this.target;
|
||||
let position = this.object.position;
|
||||
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;
|
||||
else scale = 1;
|
||||
this.update();
|
||||
};
|
||||
|
||||
this.getPosition = function(scaled) {
|
||||
this.getPosition = function({ scaled } = { scaled: false }) {
|
||||
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;
|
||||
};
|
||||
|
||||
|
|
@ -325,11 +351,13 @@ class Orbit extends EventDispatcher {
|
|||
// min(camera displacement, camera rotation in radians)^2 > EPS
|
||||
// using small-angle approximation cos(x/2) = 1 - x^2 / 8
|
||||
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);
|
||||
lastPosition.copy(this.object.position);
|
||||
lastQuaternion.copy(this.object.quaternion);
|
||||
lastZoom = this.object.zoom;
|
||||
if (notify) notify(position, true);
|
||||
} else {
|
||||
if (notify) notify(position, false);
|
||||
|
|
@ -362,6 +390,15 @@ class Orbit extends EventDispatcher {
|
|||
if (scope.enabled === false) return;
|
||||
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) {
|
||||
case scope.mouseButtons.ORBIT:
|
||||
state = event.metaKey ? STATE.PAN : STATE.ROTATE;
|
||||
|
|
@ -373,6 +410,7 @@ class Orbit extends EventDispatcher {
|
|||
state = STATE.PAN;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
case STATE.ROTATE:
|
||||
|
|
@ -467,14 +505,14 @@ class Orbit extends EventDispatcher {
|
|||
if (event.wheelDelta !== undefined) {
|
||||
// Chrome/Safari wheelDelta: scroll up = +120, scroll down = -120
|
||||
// Negate to match deltaY convention
|
||||
delta = -event.wheelDelta;
|
||||
delta = event.wheelDelta;
|
||||
} else if (event.detail !== undefined) {
|
||||
// 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) {
|
||||
// Modern browsers deltaY: scroll up = negative, scroll down = positive
|
||||
// Already matches our convention
|
||||
delta = event.deltaY;
|
||||
delta = -event.deltaY;
|
||||
// Firefox's deltaMode indicates the unit of deltaY
|
||||
// DOM_DELTA_PIXEL (0x00) - pixels
|
||||
// DOM_DELTA_LINE (0x01) - lines (default for Firefox, ~3 units per notch)
|
||||
|
|
@ -681,7 +719,23 @@ class Orbit extends EventDispatcher {
|
|||
|
||||
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('wheel', onMouseWheel, false); // Modern standard (Chrome, Safari, Firefox)
|
||||
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