updating mesh visuals

add document history
This commit is contained in:
Stewart Allen 2026-02-17 23:48:25 -05:00
commit 913977ec64
18 changed files with 1050 additions and 274 deletions

25
app.js
View file

@ -26,6 +26,7 @@ const mods = {};
const load = []; const load = [];
const api = {}; const api = {};
let lastTouchTime = {};
let forceUseCache = false; let forceUseCache = false;
let serviceWorker = true; let serviceWorker = true;
let crossOrigin = false; let crossOrigin = false;
@ -213,9 +214,19 @@ function init(mod) {
} }
} }
// create alt artifacts with module extensions // synthesize new main when applicable
createArtifacts();
}
// create alt artifacts with module extensions
function createArtifacts() {
if (dryrun || !isElectron) { if (dryrun || !isElectron) {
logger.log('creating artifacts', Object.keys(append)); if (debug) {
setTimeout(createArtifacts, 1000);
}
if (Object.keys(lastTouchTime).length === 0) {
logger.log('creating artifacts', Object.keys(append));
}
for (let [ key, val ] of Object.entries(append)) { for (let [ key, val ] of Object.entries(append)) {
// append mains // append mains
let src = `${dir}/src/main/${key}.js`; let src = `${dir}/src/main/${key}.js`;
@ -223,6 +234,14 @@ function init(mod) {
logger.log('missing', src); logger.log('missing', src);
continue; continue;
} }
let ltt = fs.statSync(src).mtimeMs;
if (lastTouchTime[src] === ltt) {
continue;
} else if (debug) {
logger.log('changed', src);
}
lastTouchTime[src] = ltt;
// console.log({ src, ltt });
fs.mkdirSync(`${dir}/alt/main`, { recursive: true }); fs.mkdirSync(`${dir}/alt/main`, { recursive: true });
let body = fs.readFileSync(src); let body = fs.readFileSync(src);
fs.writeFileSync(`${dir}/alt/main/${key}.js`, body + val); fs.writeFileSync(`${dir}/alt/main/${key}.js`, body + val);
@ -239,7 +258,7 @@ function init(mod) {
} else { } else {
logger.log('skipping artifacts'); logger.log('skipping artifacts');
} }
}; }
// either add module assets to path or require(init.js) // either add module assets to path or require(init.js)
function loadModule(mod, dir) { function loadModule(mod, dir) {

View file

@ -171,6 +171,7 @@ export function init_input() {
event.on('resize', onResize); event.on('resize', onResize);
// configure moto.space // configure moto.space
space.view.setFitPadding({ perspective: 0.8 });
space.sky.showGrid(false); space.sky.showGrid(false);
space.sky.setColor(controller.dark ? 0 : 0xffffff); space.sky.setColor(controller.dark ? 0 : 0xffffff);
space.setAntiAlias(controller.antiAlias); space.setAntiAlias(controller.antiAlias);

View file

@ -172,14 +172,14 @@ function update_size(updateDark = true) {
space.platform.setGrid(gridMajor, gridMinor, scheme.grid.major, scheme.grid.minor); space.platform.setGrid(gridMajor, gridMinor, scheme.grid.major, scheme.grid.minor);
space.platform.opacity(0.05); space.platform.opacity(0.05);
space.sky.set({ color: 0, ambient: { intensity: 0.6 } }); space.sky.set({ color: 0, ambient: { intensity: 0.6 } });
document.body.classList.add('dark'); document.documentElement.classList.add('dark');
} else { } else {
space.platform.set({ light: 0.08 }); space.platform.set({ light: 0.08 });
space.platform.setFont({rulerColor:'#333333'}); space.platform.setFont({rulerColor:'#333333'});
space.platform.setGrid(gridMajor, gridMinor, scheme.grid.major, scheme.grid.minor); space.platform.setGrid(gridMajor, gridMinor, scheme.grid.major, scheme.grid.minor);
space.platform.opacity(0.2); space.platform.opacity(0.2);
space.sky.set({ color: 0xffffff, ambient: { intensity: 1.1 } }); space.sky.set({ color: 0xffffff, ambient: { intensity: 1.1 } });
document.body.classList.remove('dark'); document.documentElement.classList.remove('dark');
} }
space.platform.setSize(); space.platform.setSize();
} }

View file

@ -83,7 +83,7 @@ function booleanSave() {
control.assembly = ui.assembly.checked; control.assembly = ui.assembly.checked;
control.autoLayout = ui.autoLayout.checked; control.autoLayout = ui.autoLayout.checked;
control.autoSave = ui.autoSave.checked; control.autoSave = ui.autoSave.checked;
control.dark = ui.dark.checked; control.dark = api.sdb['kiri-dark'] = ui.dark.checked;
control.devel = ui.devel.checked; control.devel = ui.devel.checked;
control.drawer = ui.drawer.checked; control.drawer = ui.drawer.checked;
control.exportOcto = ui.exportOcto.checked; control.exportOcto = ui.exportOcto.checked;

View file

@ -387,7 +387,7 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
/** /**
* when moving between contour endpoints, check if we can * when moving between contour endpoints, check if we can
* instead route around the bounding area of the contour * instead route around the bounding area of the contour
* whih we call the coastline. * which we call the coastline.
*/ */
function coastlineMove(point) { function coastlineMove(point) {
let from = toWidgetCoords(printPoint); let from = toWidgetCoords(printPoint);
@ -395,6 +395,7 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
if (!coastline || from.distTo2D(to) < 0.01) { if (!coastline || from.distTo2D(to) < 0.01) {
return false; return false;
} }
let minz = Math.min(from.z, to.z);
let start = { dist: 1, poly: 0, pt: from }; let start = { dist: 1, poly: 0, pt: from };
let end = { dist: 1, poly: 1, pt: to }; let end = { dist: 1, poly: 1, pt: to };
for (let poly of coastline) { for (let poly of coastline) {
@ -445,7 +446,9 @@ export async function prepare_one(widget, settings, print, firstPoint, update) {
} }
} }
for (let i=sp, d=0; d < dist; i += dir, d++) { for (let i=sp, d=0; d < dist; i += dir, d++) {
layerPush(toWorkCoords(points[i % pl]), 1, 0, tool); let cp = points[i % pl].clone();
cp.z = Math.max(minz, cp.z);
layerPush(toWorkCoords(cp), 1, 0, tool);
} }
return true; return true;
} }

View file

@ -17,10 +17,12 @@ import { edges as meshEdges } from '../mesh/edges.js';
import { open as dataOpen } from '../data/index.js'; import { open as dataOpen } from '../data/index.js';
import { load as fileLoad } from '../load/file.js'; import { load as fileLoad } from '../load/file.js';
import { THREE } from '../ext/three.js'; import { THREE } from '../ext/three.js';
import { createDocumentManager } from '../mesh/document.js';
const version = '1.5.7'; const version = '1.5.7';
const call = broker.send; const call = broker.send;
const dbindex = [ "admin", "space" ]; const dbindex = [ "admin", "documents", "versions" ];
const DOC_META_KEY = '__doc';
const { Quaternion } = THREE; const { Quaternion } = THREE;
@ -28,10 +30,67 @@ function log() {
return api.log.emit(...arguments); return api.log.emit(...arguments);
} }
function boot_status(message = 'loading...') {
const curtain = $('curtain');
if (!curtain) return;
curtain.textContent = String(message || 'loading...');
}
function boot_start(message = 'loading...') {
$('app')?.classList?.add('booting');
boot_status(message);
$d('curtain', 'flex');
}
function boot_done() {
$('app')?.classList?.remove('booting');
$d('curtain', 'none');
}
function get_doc_meta(meta = metaCache) {
if (!meta || typeof meta !== 'object') return {};
return meta[DOC_META_KEY] || {};
}
function capture_camera_state() {
return {
place: space.view.save(),
focus: space.view.getFocus()
};
}
function apply_camera_state(camera) {
if (!camera) return;
if (camera.place) {
space.view.load(camera.place);
}
if (camera.focus) {
space.view.setFocus(camera.focus);
}
}
function save_camera_to_document() {
const dmeta = get_doc_meta(metaCache);
metaCache[DOC_META_KEY] = {
...dmeta,
camera: capture_camera_state()
};
store_meta();
}
let cameraSaveTimer = null;
function schedule_camera_save(delay = 120) {
clearTimeout(cameraSaveTimer);
cameraSaveTimer = setTimeout(() => {
cameraSaveTimer = null;
save_camera_to_document();
}, delay);
}
// set below. called once the DOM readyState = complete // set below. called once the DOM readyState = complete
// this is the main() entrypoint called after all dependents load // this is the main() entrypoint called after all dependents load
function init() { function init() {
let stores = dataOpen('mesh', { stores: dbindex, version: 4 }).init(), let stores = dataOpen('mesh', { stores: dbindex, version: 5 }).init(),
dark = false, dark = false,
ortho = false, ortho = false,
zoomrev = true, zoomrev = true,
@ -39,11 +98,21 @@ function init() {
platform = space.platform, platform = space.platform,
db = api.db = { db = api.db = {
admin: stores.promise('admin'), admin: stores.promise('admin'),
space: stores.promise('space') documents: stores.promise('documents'),
versions: stores.promise('versions')
}; };
const docman = api.document = createDocumentManager({
admin: db.admin,
documents: db.documents,
versions: db.versions,
maxRevisions: 200
});
db.space = docman.spaceStore;
// initialize the API (to avoid circular dependencies) // initialize the API (to avoid circular dependencies)
api.init(); api.init();
boot_start('initializing mesh:tool');
// mark init time and use count // mark init time and use count
db.admin.put("init", Date.now()); db.admin.put("init", Date.now());
@ -70,26 +139,23 @@ function init() {
colorX: 0xff7777, colorY: 0x7777ff }, colorX: 0xff7777, colorY: 0x7777ff },
}); });
platform.onMove(() => { platform.onMove(() => {
// save last location and focus // save camera per-document
db.admin.put('camera', { save_camera_to_document();
place: space.view.save(),
focus: space.view.getFocus()
});
}, 100); }, 100);
space.view.setZoom(zoomrev, zoomspd); space.view.setZoom(zoomrev, zoomspd);
// trigger ui building
call.ui_build();
// trigger space event binding
call.space_init({ space: space, platform });
// reload stored space when worker is ready // reload stored space when worker is ready
motoClient.on('ready', restore_space); motoClient.on('ready', restore_space);
// start worker // start worker
motoClient.start('../lib/mesh/work.js?' + version); motoClient.start('../lib/mesh/work.js?' + version);
// trigger space event binding
call.space_init({ space: space, platform });
// trigger ui building
call.ui_build();
// hide url params // hide url params
let wlp = window.location.pathname; let wlp = window.location.pathname;
let mio = wlp.indexOf('/mesh/'); let mio = wlp.indexOf('/mesh/');
@ -102,32 +168,27 @@ function init() {
self.electron = navigator.userAgent.includes('Electron'); self.electron = navigator.userAgent.includes('Electron');
} }
// restore space layout and view from previous session function clear_workspace() {
async function restore_space() { api.selection.clear();
const db_admin = api.db.admin; for (let sk of api.sketch.list().slice()) {
sk.remove();
}
for (let grp of api.group.list().slice()) {
grp.remove();
}
}
async function restore_workspace_from_state(cached = {}, mcache = {}) {
const db_space = api.db.space; const db_space = api.db.space;
// let mcache = {};
await db_admin.get("camera")
.then(saved => {
if (saved) {
space.view.load(saved.place);
space.view.setFocus(saved.focus);
}
});
const mcache = await db_admin.get("meta") || {};
let count = 0; let count = 0;
await db_space.iterate({ map: true }).then(cached => { await Promise.resolve(cached).then(cached => {
const keys = []; const keys = [];
const claimed = []; const claimed = [];
for (let [id, data] of Object.entries(cached)) { for (let [id, data] of Object.entries(cached)) {
// console.log({ id, data });
keys.push(id); keys.push(id);
if (count++ === 0) { if (count++ === 0) {
log(`restoring workspace`); log(`restoring workspace`);
} }
// restore object based on type
// group arrays load models they contain
// sketches are loaded by type since they're not grouped
if (Array.isArray(data)) { if (Array.isArray(data)) {
claimed.push(id); claimed.push(id);
let models = data let models = data
@ -135,11 +196,11 @@ async function restore_space() {
claimed.push(id); claimed.push(id);
return { id, md: cached[id] } return { id, md: cached[id] }
}) })
.filter(r => r.md) // filter cache misses .filter(r => r.md)
.map(r => new meshModel(r.md, r.id).applyMeta(mcache[r.id])) .map(r => new meshModel(r.md, r.id).applyMeta(mcache[r.id]));
if (models.length) { if (models.length) {
log(`restored ${models.length} model(s)`); log(`restored ${models.length} model(s)`);
api.group.new(models, id).applyMeta(mcache[id]) api.group.new(models, id).applyMeta(mcache[id]);
} else { } else {
log(`removed empty group ${id}`); log(`removed empty group ${id}`);
db_space.remove(id); db_space.remove(id);
@ -155,22 +216,17 @@ async function restore_space() {
if (keys.length) { if (keys.length) {
log(`removing ${keys.length} unclaimed meshes`); log(`removing ${keys.length} unclaimed meshes`);
} }
// clear out meshes left in the space db along with their meta-data
for (let id of keys) { for (let id of keys) {
db_space.remove(id); db_space.remove(id);
delete mcache[id]; delete mcache[id];
} }
// restore global cache only after objects are restored
// otherwise their setup will corrupt the cache for other restores
metaCache = mcache; metaCache = mcache;
store_meta(); api.document.setMeta(metaCache);
}).then(() => { }).then(() => {
// restore preferences after models are restored
return api.prefs.load().then(() => { return api.prefs.load().then(() => {
let { map } = api.prefs; let { map } = api.prefs;
let { space, mode } = map; let { space, mode } = map;
api.grid(space.grid); api.grid(space.grid);
// restore selected state
let selist = space.select || []; let selist = space.select || [];
let smodel = api.model.list().filter(m => selist.contains(m.id)); let smodel = api.model.list().filter(m => selist.contains(m.id));
let sgroup = api.group.list().filter(m => selist.contains(m.id)); let sgroup = api.group.list().filter(m => selist.contains(m.id));
@ -179,14 +235,38 @@ async function restore_space() {
let tgroup = api.group.list().filter(m => tolist.contains(m.id)); let tgroup = api.group.list().filter(m => tolist.contains(m.id));
let sklist = api.sketch.list().filter(s => selist.contains(s.id)); let sklist = api.sketch.list().filter(s => selist.contains(s.id));
api.selection.set([...smodel, ...sgroup, ...sklist], [...tmodel, ...tgroup]); api.selection.set([...smodel, ...sgroup, ...sklist], [...tmodel, ...tgroup]);
// restore edit mode
api.mode[mode](); api.mode[mode]();
// restore dark mode set_darkmode();
set_darkmode(map.space.dark);
}); });
}).finally(() => { });
}
// restore space layout and view from previous session
async function restore_space() {
const db_admin = api.db.admin;
const db_space = api.db.space;
const docman = api.document;
boot_status('loading document');
const currentDoc = await docman.restoreOrCreate();
const mcache = docman.getMeta() || {};
const oldCamera = await db_admin.get("camera");
const docCamera = get_doc_meta(mcache).camera || oldCamera || null;
boot_status('restoring workspace');
const cached = await db_space.iterate({ map: true }) || {};
docman.pause();
try {
await restore_workspace_from_state(cached, mcache);
} finally {
docman.resume();
}
boot_status('restoring view');
apply_camera_state(docCamera);
space.update();
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
boot_status('finalizing');
Promise.resolve().finally(() => {
// hide loading curtain // hide loading curtain
$d('curtain','none'); boot_done();
// restore handles visibility // restore handles visibility
handles.setEnabled(api.prefs.map.space.bounds ?? false); handles.setEnabled(api.prefs.map.space.bounds ?? false);
// restore script if was showing // restore script if was showing
@ -196,15 +276,103 @@ async function restore_space() {
if (api.prefs.map.info.welcome !== false) { if (api.prefs.map.info.welcome !== false) {
api.welcome(version); api.welcome(version);
} }
api.file.set_doc_name(currentDoc?.name || 'Untitled');
broker.publish("app_ready"); broker.publish("app_ready");
}); });
} }
async function document_new(opt = {}) {
const docman = api.document;
await docman.flush();
await docman.commit('document.autosave', 'document.autosave');
docman.pause();
try {
clear_workspace();
space.view.home();
metaCache = {
[DOC_META_KEY]: {
camera: capture_camera_state()
}
};
await docman.create(opt.name || 'Untitled');
docman.setMeta(metaCache);
} finally {
docman.resume();
}
await docman.commit('document.new', 'document.new');
api.file.set_doc_name(docman.current?.name || 'Untitled');
}
async function document_open(opt = {}) {
const docman = api.document;
const id = String(opt?.id || '');
if (!id) return;
await docman.flush();
await docman.open(id, { autosave: opt.autosave !== false });
const cached = docman.getSpace() || {};
const mcache = docman.getMeta() || {};
docman.pause();
try {
clear_workspace();
await restore_workspace_from_state(cached, mcache);
} finally {
docman.resume();
}
apply_camera_state(get_doc_meta(mcache).camera);
api.file.set_doc_name(docman.current?.name || 'Untitled');
}
// add space event bindings // add space event bindings
function space_init(data) { function space_init(data) {
let platcolor = 0x00ff00; let platcolor = 0x00ff00;
let { space, platform } = data; let { space, platform } = data;
let { selection } = api; let { selection } = api;
function selection_or_visible_entities() {
const selected = api.selection.list(true);
if (selected?.length) return selected;
return [
...api.group.list().filter(g => g.visible()),
...api.sketch.list().filter(s => s.visible())
];
}
function fit_visible() {
const entities = selection_or_visible_entities();
const objects = entities.map(e => e?.object).filter(o => o);
return space.view.fit(undefined, {
padding: 1,
visibleOnly: true,
objects: objects.length ? objects : undefined
});
}
function focus_visible() {
const entities = selection_or_visible_entities();
if (entities.length) {
return api.focus(entities);
}
return api.focus([
...api.group.list(),
...api.sketch.list()
]);
}
function norm_code(evt) {
if (evt?.code) return evt.code;
const key = evt?.key;
if (!key) return '';
if (key === ' ') return 'Space';
if (key === 'Spacebar') return 'Space';
if (key === 'Escape') return 'Escape';
if (key.length === 1) {
const up = key.toUpperCase();
if (up >= 'A' && up <= 'Z') return `Key${up}`;
if (up >= '0' && up <= '9') return `Digit${up}`;
}
return key;
}
// add file drop handler // add file drop handler
space.event.addHandlers(self, [ space.event.addHandlers(self, [
'drop', (evt) => { 'drop', (evt) => {
@ -222,6 +390,10 @@ function space_init(data) {
'dragleave', evt => { 'dragleave', evt => {
platform.set({ opacity: 0, color: platcolor }); platform.set({ opacity: 0, color: platcolor });
}, },
// camera interactions (orbit/pan/dolly) are not guaranteed to trigger platform.onMove
'wheel', () => schedule_camera_save(),
'mouseup', () => schedule_camera_save(),
'touchend', () => schedule_camera_save(),
'keypress', evt => { 'keypress', evt => {
if (api.modal.showing) { if (api.modal.showing) {
return; return;
@ -229,7 +401,8 @@ function space_init(data) {
if (evt.key === '?') { if (evt.key === '?') {
return api.welcome(version); return api.welcome(version);
} }
let { shiftKey, metaKey, ctrlKey, code, target } = evt; let { shiftKey, metaKey, ctrlKey, target } = evt;
let code = norm_code(evt);
if (target.nodeName === 'TEXTAREA') { if (target.nodeName === 'TEXTAREA') {
api.script.changed(); api.script.changed();
return; return;
@ -250,7 +423,11 @@ function space_init(data) {
case 'KeyB': case 'KeyB':
return selection.boundsBox({toggle:true}); return selection.boundsBox({toggle:true});
case 'KeyC': case 'KeyC':
return selection.centerXY().focus(); if (shiftKey) {
return selection.floor();
} else {
return selection.centerXY().focus();
}
case 'KeyD': case 'KeyD':
return shiftKey && api.tool.duplicate(); return shiftKey && api.tool.duplicate();
case 'KeyE': case 'KeyE':
@ -259,14 +436,14 @@ function space_init(data) {
return api.sketch.extrude(); return api.sketch.extrude();
} }
return; return;
case 'KeyF':
return shiftKey ? selection.focus() : selection.floor().focus();
case 'KeyG': case 'KeyG':
return shiftKey ? return shiftKey ?
(api.mode.is([ api.modes.sketch ]) ? api.sketch.arrange.group() : api.tool.regroup()) : (api.mode.is([ api.modes.sketch ]) ? api.sketch.arrange.group() : api.tool.regroup()) :
api.grid(); api.grid();
case 'KeyH': case 'KeyH':
return shiftKey ? selection.hide() : space.view.home(); if (shiftKey) return selection.hide();
schedule_camera_save(180);
return space.view.home();
case 'KeyI': case 'KeyI':
return api.file.import(); return api.file.import();
case 'KeyL': case 'KeyL':
@ -283,7 +460,9 @@ function space_init(data) {
if (!api.mode.is([ api.modes.object ])) return; if (!api.mode.is([ api.modes.object ])) return;
return shiftKey ? selection.visible({toggle:true}) : meshSplit.start(); return shiftKey ? selection.visible({toggle:true}) : meshSplit.start();
case 'KeyT': case 'KeyT':
return shiftKey ? api.tool.triangulate() : space.view.top(); if (shiftKey) return api.tool.triangulate();
schedule_camera_save(180);
return space.view.top();
case 'KeyU': case 'KeyU':
return shiftKey && api.tool.union(); return shiftKey && api.tool.union();
case 'KeyV': case 'KeyV':
@ -295,7 +474,9 @@ function space_init(data) {
} }
}, },
'keydown', evt => { 'keydown', evt => {
let { shiftKey, metaKey, ctrlKey, code, target } = evt; let { shiftKey, metaKey, ctrlKey, target } = evt;
let code = norm_code(evt);
const key = evt?.key;
if (target.nodeName === 'TEXTAREA') { if (target.nodeName === 'TEXTAREA') {
if (code === 'Tab') { if (code === 'Tab') {
estop(evt); estop(evt);
@ -320,14 +501,36 @@ function space_init(data) {
delete keyOnce[code]; delete keyOnce[code];
return once(evt); return once(evt);
} }
let rv = (Math.PI / 12);
if (api.modal.showing) { if (api.modal.showing) {
if (code === 'Escape') { if (code === 'Escape') {
api.modal.cancel(); api.modal.cancel();
} }
return; return;
} }
let rot, floor = api.prefs.map.space.floor !== false; const isFit = code === 'KeyF' ||
key === 'f' ||
key === 'F';
if (isFit && !(metaKey || ctrlKey)) {
estop(evt);
const rv = shiftKey ? focus_visible() : fit_visible();
schedule_camera_save(220);
return rv;
}
const isSpace = code === 'Space' ||
code === 'Spacebar' ||
key === ' ' ||
key === 'Spacebar';
if (isSpace) {
if (selection.clear()) {
meshEdges.clear();
meshSplit.end();
}
estop(evt);
return;
}
let rv = (Math.PI / 12);
let rot;
let floor = api.prefs.map.space.floor !== false;
switch (code) { switch (code) {
case 'KeyA': case 'KeyA':
estop(evt); estop(evt);
@ -353,7 +556,9 @@ function space_init(data) {
if (metaKey || ctrlKey) { if (metaKey || ctrlKey) {
return shiftKey ? api.history.redo() : api.history.undo(); return shiftKey ? api.history.redo() : api.history.undo();
} else { } else {
return space.view.reset(); space.view.reset();
schedule_camera_save(220);
return;
} }
case 'Escape': case 'Escape':
if (selection.clear()) { if (selection.clear()) {
@ -664,7 +869,7 @@ function key_once_cancel(code) {
} }
function store_meta() { function store_meta() {
api.db.admin.put("meta", metaCache); api.document?.setMeta?.(metaCache);
} }
function update_meta(id, data) { function update_meta(id, data) {
@ -694,28 +899,19 @@ function object_destroy(id) {
function set_darkmode(dark) { function set_darkmode(dark) {
let { prefs, model } = api; let { prefs, model } = api;
let { sky, platform } = space; let { sky, platform } = space;
prefs.map.space.dark = dark; dark = true;
if (dark) { prefs.map.space.dark = true;
materials.wireframe.color.set(0xaaaaaa); materials.wireframe.color.set(0xaaaaaa);
materials.wireline.color.set(0xaaaaaa); materials.wireline.color.set(0xaaaaaa);
$('app').classList.add('dark');
} else {
materials.wireframe.color.set(0,0,0);
materials.wireline.color.set(0,0,0);
$('app').classList.remove('dark');
}
sky.set({ sky.set({
color: dark ? 0 : 0xffffff, color: 0,
ambient: { intensity: dark ? 0.55 : 1.1 } ambient: { intensity: 0.55 }
}); });
platform.set({ platform.set({
light: dark ? 0.08 : 0.08, light: 0.08,
grid: dark ? { grid: {
colorMajor: 0x666666, colorMajor: 0x666666,
colorMinor: 0x333333, colorMinor: 0x333333,
} : {
colorMajor: 0xcccccc,
colorMinor: 0xeeeeee,
}, },
}); });
api.updateFog(); api.updateFog();
@ -741,11 +937,8 @@ function set_normals_length(length) {
function set_normals_color(color) { function set_normals_color(color) {
let { prefs, model } = api; let { prefs, model } = api;
let { map } = prefs; let { map } = prefs;
if (map.space.dark) { map.normals.color_dark = color || 0;
map.normals.color_dark = color || 0; map.normals.color_lite = color || 0;
} else {
map.normals.color_lite = color || 0;
}
prefs.save(); prefs.save();
// Update existing normals // Update existing normals
for (let m of model.list()) { for (let m of model.list()) {
@ -809,7 +1002,9 @@ broker.listeners({
set_surface_radius, set_surface_radius,
set_wireframe_opacity, set_wireframe_opacity,
set_wireframe_fog, set_wireframe_fog,
set_snap_value set_snap_value,
document_new,
document_open
}); });
init(); init();
@ -837,5 +1032,7 @@ export {
set_surface_radius, set_surface_radius,
set_wireframe_opacity, set_wireframe_opacity,
set_wireframe_fog, set_wireframe_fog,
set_snap_value set_snap_value,
document_new,
document_open
}; };

View file

@ -109,10 +109,11 @@ function setupLeftPanelResize(db) {
event.stopPropagation(); event.stopPropagation();
}; };
window.addEventListener('resize', () => { const onResize = () => {
const curr = left.getBoundingClientRect().width; applyWidth(left.getBoundingClientRect().width);
applyWidth(curr); }
});
window.addEventListener('resize', onResize);
} }
// Main initialization function // Main initialization function
@ -289,26 +290,20 @@ async function init() {
toolbar.updateDocumentTitle(); toolbar.updateDocumentTitle();
tree.render(); tree.render();
// TEST: Add example overlay elements const { THREE } = window;
// These demonstrate the 2D overlay tracking 3D points
if (true) { // Set to false to disable test overlays
const { THREE } = window;
// Show overlay // Show overlay
overlay.show(); overlay.show();
// Add test points at origin and along axes // Add origin
overlay.add('origin-point', 'point', { overlay.add('origin-point', 'point', {
pos3d: new THREE.Vector3(0, 0, 0), pos3d: new THREE.Vector3(0, 0, 0),
radius: 4.8, radius: 4.8,
color: 'rgba(140, 140, 140, 0.45)', color: 'rgba(140, 140, 140, 0.45)',
stroke: '#5a9fd4', stroke: '#5a9fd4',
strokeWidth: 2 strokeWidth: 2
}); });
api.origin.syncOverlayPoint(); api.origin.syncOverlayPoint();
console.log({ test_overlays_added: 1 });
}
// Hide loading curtain // Hide loading curtain
const curtain = $('curtain'); const curtain = $('curtain');
@ -320,6 +315,9 @@ async function init() {
}, 300); }, 300);
} }
// update canvas based on left panel size
space.event.onResize();
console.log({ void_form_ready: true }); console.log({ void_form_ready: true });
} }

View file

@ -972,6 +972,93 @@ let add = {
}; };
let file = { let file = {
set_doc_name(name = 'Untitled') {
const label = String(name || 'Untitled').trim() || 'Untitled';
document.title = `${label} | Mesh:Tool`;
const el = $('top-doc-name');
if (el) {
el.textContent = label;
el.title = `click to rename (${label})`;
}
},
async new() {
await api.document?.flush?.();
await call.document_new({ name: 'Untitled' });
api.file.set_doc_name('Untitled');
},
async open() {
const docs = await (api.document?.list?.() || Promise.resolve([]));
const rows = docs.map(doc => h.div({ class: "doc-open-row", onclick: async function() {
api.modal.hide();
await call.document_open({ id: doc.id });
api.file.set_doc_name(doc.name || 'Untitled');
} }, [
h.div({ class: "doc-open-name", _: `${doc.name || 'Untitled'}` }),
h.button({ _: "rename", onclick(evt) {
evt?.stopPropagation?.();
api.modal.hide();
setTimeout(() => api.file.rename(doc), 0);
} }),
h.button({ class: "doc-open-del", _: "×", title: "delete", onclick: async (evt) => {
evt?.stopPropagation?.();
await api.file.delete(doc);
} })
]));
api.modal.dialog({
title: "open document",
body: [ h.div({ class: "doc-open-list" }, [
...rows,
rows.length ? undefined : h.div({ class: "doc-open-empty", _: "no documents" }),
h.hr(),
h.button({ class: "doc-open-new", _: "new", onclick() {
api.modal.hide();
api.file.new();
} })
].filter(v => v)) ]
});
},
async rename(doc = api.document?.current) {
const current = doc || api.document?.current;
if (!current?.id) return;
if (api.modal?.showing) {
api.modal.hide();
await Promise.resolve();
}
let onclick = onkeydown = (ev) => {
if (!tempedit || (ev.code && ev.code !== 'Enter')) {
return;
}
api.document.rename(current.id, tempedit.value).then(rec => {
if (rec?.id && api.document?.current?.id === rec.id) {
api.file.set_doc_name(rec.name || 'Untitled');
}
}).finally(() => api.modal.hide());
};
let { tempedit } = api.modal.show(`rename document`, h.div({ class: "rename"}, [
h.input({ id: "tempedit", value: current.name || 'Untitled', onkeydown }),
h.button({ _: 'ok', onclick })
]));
tempedit.setSelectionRange(0,1000);
tempedit.focus();
},
async delete(doc = api.document?.current) {
const current = doc || api.document?.current;
if (!current?.id) return;
const result = await api.document?.delete?.(current.id);
if (result?.switched && result?.current?.id) {
await call.document_open({ id: result.current.id, autosave: false });
api.file.set_doc_name(result.current.name || 'Untitled');
}
api.modal.hide();
api.file.open();
},
import() { import() {
// binding created in mesh.build // binding created in mesh.build
$('import').click(); $('import').click();
@ -1348,7 +1435,7 @@ const mode = {
$(`mode-${key}`).classList.remove('selected'); $(`mode-${key}`).classList.remove('selected');
} }
$(`mode-${mode}`).classList.add('selected'); $(`mode-${mode}`).classList.add('selected');
$('mode-label').innerText = mode; $('top-mode-label').innerText = mode;
api.mode.check(); api.mode.check();
meshEdges?.end(); meshEdges?.end();
if (mode === 'sketch') { if (mode === 'sketch') {
@ -1572,10 +1659,30 @@ const api = {
history: { history: {
undo() { undo() {
history.undo(); if (api.document?.undo) {
api.document.undo().then(changed => {
if (changed) {
call.document_open({ id: api.document.current?.id, autosave: false });
} else {
history.undo();
}
});
} else {
history.undo();
}
}, },
redo() { redo() {
history.redo(); if (api.document?.redo) {
api.document.redo().then(changed => {
if (changed) {
call.document_open({ id: api.document.current?.id, autosave: false });
} else {
history.redo();
}
});
} else {
history.redo();
}
} }
}, },
@ -1701,6 +1808,8 @@ const api = {
sketch, sketch,
space: motoSpace,
tool, tool,
isDebug: self.debug === true isDebug: self.debug === true

View file

@ -19,7 +19,10 @@ let deg = Math.PI / 180;
let und = undefined; let und = undefined;
broker.listeners({ broker.listeners({
ui_build ui_build,
app_ready() {
api.file?.set_doc_name?.(api.document?.current?.name || 'Untitled');
}
}); });
let spin_timer; let spin_timer;
@ -240,14 +243,10 @@ api.welcome = function(version = "unknown") {
api.settings = function() { api.settings = function() {
const { prefs } = api; const { prefs } = api;
const { surface, normals, space, sketch, wireframe } = prefs.map; const { surface, normals, space, sketch, wireframe } = prefs.map;
const { dark } = space; const dark = true;
const set1 = div([ const set1 = div([
label('dark mode'), label({ class: "header", _: 'auto'}),
input({ type: "checkbox",
onchange: ev => call.set_darkmode(ev.target.checked),
[ dark ? 'checked' : 'unchecked' ] : 1
}),
label('auto floor'), label('auto floor'),
input({ type: "checkbox", input({ type: "checkbox",
onchange: ev => prefs.save( space.floor = !space.floor ), onchange: ev => prefs.save( space.floor = !space.floor ),
@ -391,6 +390,8 @@ function ui_build() {
// top left drop menus // top left drop menus
bind($('top-left'), [ bind($('top-left'), [
div({ _: 'Mesh:Tool', class: "title" }),
div({ class: "menubar-separator" }),
div({ class: "menu" }, [ div({ class: "menu" }, [
div('File'), div('File'),
div({ class: "menu-items" }, [ div({ class: "menu-items" }, [
@ -398,12 +399,17 @@ function ui_build() {
id: "import", type: "file", class: ["hide"], multiple: true, accept:".stl,.obj", id: "import", type: "file", class: ["hide"], multiple: true, accept:".stl,.obj",
onchange(evt) { broker.send.load_files(evt.target.files) } onchange(evt) { broker.send.load_files(evt.target.files) }
}), }),
menu_item('New', file.new),
menu_item('Open', file.open),
hr(),
menu_item('Import', file.import, 'I'), menu_item('Import', file.import, 'I'),
menu_item('Export', file.export, 'X'), menu_item('Export', file.export, 'X'),
hr(), hr(),
menu_item('Slicer', api.kirimoto), menu_item('Slicer', api.kirimoto),
menu_item('Script', api.script.toggle), menu_item('Script', api.script.toggle),
hr(), hr(),
menu_item('Preferences', api.settings, 'Q'),
hr(),
menu_item('Close', () => window.close() || api.kirimoto()), menu_item('Close', () => window.close() || api.kirimoto()),
]) ])
]), ]),
@ -467,7 +473,6 @@ function ui_build() {
menu_item('Face', mode.face, '5', 'mode-face'), menu_item('Face', mode.face, '5', 'mode-face'),
menu_item('Edge', mode.edge, '6', 'mode-edge'), menu_item('Edge', mode.edge, '6', 'mode-edge'),
]), ]),
div({ id: "mode-label" })
]), ]),
div({ class: "menu sketch-on" }, [ div({ class: "menu sketch-on" }, [
div('Items'), div('Items'),
@ -510,7 +515,7 @@ function ui_build() {
div({ class: "menu sketch-off" }, [ div({ class: "menu sketch-off" }, [
div('Faces'), div('Faces'),
div({ class: "menu-items" }, [ div({ class: "menu-items" }, [
menu_item('Flip Normals', tool.invert, ['bi-shift','I']), menu_item('Flip Normals', tool.invert, ['bi-shift','N']),
menu_item('Triangulate', tool.triangulate, ['bi-shift','T']), menu_item('Triangulate', tool.triangulate, ['bi-shift','T']),
menu_item('To Sketch', tool.toSketch), menu_item('To Sketch', tool.toSketch),
hr(), hr(),
@ -535,6 +540,7 @@ function ui_build() {
]) ])
]), ]),
div({ class: "menu" }, [ div({ class: "menu" }, [
// div({ class: "fas fa-question" }),
div('Help'), div('Help'),
div({ class: "menu-items" }, [ div({ class: "menu-items" }, [
menu_item('About', () => { api.welcome(version) }), menu_item('About', () => { api.welcome(version) }),
@ -546,15 +552,15 @@ function ui_build() {
menu_item('Versions', api.version), menu_item('Versions', api.version),
]) ])
]), ]),
div({ class: "menubar-separator" }),
div({ id: "top-mode-label" }),
]); ]);
// add help buttons // add help buttons
bind($('top-right'), [ bind($('top-right'), [
div({ id: "top-settings", onclick: api.settings }, [ div({ id: "top-doc-name", onclick: () => api.file.rename(), _: 'Untitled' }),
div({ class: "fas fa-gear" }),
div('Settings')
]),
]); ]);
api.file?.set_doc_name?.(api.document?.current?.name || 'Untitled');
// modal dialog and page blocker // modal dialog and page blocker
bind($('modal_page'), [ bind($('modal_page'), [
@ -600,32 +606,55 @@ function ui_build() {
return div({ onclick: fn, class: "tool" }, [ bicon(icon), div([ label(help) ]) ]); return div({ onclick: fn, class: "tool" }, [ bicon(icon), div([ label(help) ]) ]);
} }
function toolbar_separator() {
return div({ class: "toolbar-separator" });
}
// bind sketch chiclets // bind sketch chiclets
bind(sketchtools, div([ bind(sketchtools, div([
tool_item('bi-plus', 'New Sketch', add.sketch), tool_item('bi-plus', 'New Sketch', add.sketch),
toolbar_separator(),
tool_item('bi-circle', 'Add Circle', api.add.circle), tool_item('bi-circle', 'Add Circle', api.add.circle),
toolbar_separator(),
tool_item('bi-square', 'Add Rectangle', api.add.rectangle), tool_item('bi-square', 'Add Rectangle', api.add.rectangle),
toolbar_separator(),
tool_item('bi-symmetry-vertical', 'Flip Horizontal', api.sketch.arrange.fliph), tool_item('bi-symmetry-vertical', 'Flip Horizontal', api.sketch.arrange.fliph),
toolbar_separator(),
tool_item('bi-symmetry-horizontal', 'Flip Vertical', api.sketch.arrange.flipv), tool_item('bi-symmetry-horizontal', 'Flip Vertical', api.sketch.arrange.flipv),
toolbar_separator(),
tool_item('bi-arrow-clockwise', 'Rotate', api.sketch.arrange.rotate), tool_item('bi-arrow-clockwise', 'Rotate', api.sketch.arrange.rotate),
toolbar_separator(),
tool_item('bi-union', 'Union', sketch.boolean.union), tool_item('bi-union', 'Union', sketch.boolean.union),
toolbar_separator(),
tool_item('bi-intersect', 'Intersect', sketch.boolean.intersect), tool_item('bi-intersect', 'Intersect', sketch.boolean.intersect),
toolbar_separator(),
tool_item('bi-exclude', 'Difference', sketch.boolean.difference), tool_item('bi-exclude', 'Difference', sketch.boolean.difference),
toolbar_separator(),
tool_item('bi-pip', 'Nest', sketch.boolean.nest), tool_item('bi-pip', 'Nest', sketch.boolean.nest),
toolbar_separator(),
tool_item('bi-layers', 'Flatten', sketch.boolean.flatten), tool_item('bi-layers', 'Flatten', sketch.boolean.flatten),
toolbar_separator(),
tool_item('bi-cookie', 'Even Odd', sketch.boolean.evenodd), tool_item('bi-cookie', 'Even Odd', sketch.boolean.evenodd),
toolbar_separator(),
tool_item('bi-arrow-bar-up', 'Extrude', () => sketch.extrude()), tool_item('bi-arrow-bar-up', 'Extrude', () => sketch.extrude()),
])); ]));
// bind object chiclets // bind object chiclets
bind(objecttools, div([ bind(objecttools, div([
tool_item('bi-pencil', 'New Sketch', add.sketch), tool_item('bi-pencil', 'New Sketch', add.sketch),
toolbar_separator(),
tool_item('bi-box', 'New Cube', add.cube), tool_item('bi-box', 'New Cube', add.cube),
toolbar_separator(),
tool_item('bi-database', 'New Cylinder', add.cylinder), tool_item('bi-database', 'New Cylinder', add.cylinder),
toolbar_separator(),
tool_item('bi-gear', 'New Gear', add.gear), tool_item('bi-gear', 'New Gear', add.gear),
toolbar_separator(),
tool_item('bi-union', 'Union', tool.union), tool_item('bi-union', 'Union', tool.union),
toolbar_separator(),
tool_item('bi-subtract', 'Subtract', tool.subtract), tool_item('bi-subtract', 'Subtract', tool.subtract),
toolbar_separator(),
tool_item('bi-intersect', 'Intersect', tool.intersect), tool_item('bi-intersect', 'Intersect', tool.intersect),
toolbar_separator(),
tool_item('bi-exclude', 'Difference', tool.difference), tool_item('bi-exclude', 'Difference', tool.difference),
])); ]));

316
src/mesh/document.js Normal file
View 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
};
}

View file

@ -235,15 +235,33 @@ class Orbit extends EventDispatcher {
this.setPosition = function(set) { this.setPosition = function(set) {
thetaSet = firstValue([set.left, set.theta, thetaSet]); thetaSet = firstValue([set.left, set.theta, thetaSet]);
phiSet = firstValue([set.up, set.phi, phiSet]); phiSet = firstValue([set.up, set.phi, phiSet]);
if (set.panX !== undefined) this.target.x = set.panX; let target = this.target;
if (set.panY !== undefined) this.target.y = set.panY; let position = this.object.position;
if (set.panZ !== undefined) this.target.z = set.panZ; if (set.posX !== undefined) position.x = set.posX;
if (set.posY !== undefined) position.y = set.posY;
if (set.posZ !== undefined) position.z = set.posZ;
if (set.panX !== undefined) target.x = set.panX;
if (set.panY !== undefined) target.y = set.panY;
if (set.panZ !== undefined) target.z = set.panZ;
if (set.scale !== undefined) scale = set.scale; if (set.scale !== undefined) scale = set.scale;
else scale = 1;
this.update();
}; };
this.getPosition = function(scaled) { this.getPosition = function({ scaled } = { scaled: false }) {
let t = this.target, let t = this.target,
pos = { left:theta, up:phi, panX:t.x, panY:t.y, panZ:t.z, scale:scaled ? scaleSave : 1 }; p = this.object.position,
pos = {
left: theta,
up: phi,
panX: t.x,
panY: t.y,
panZ: t.z,
posX: p.x,
posY: p.y,
posZ: p.z,
scale: scaled ? scaleSave : undefined
};
return pos; return pos;
}; };

View file

@ -1560,26 +1560,49 @@ let Space = {
front: (then) => { runPreset(0, PI2, then) }, front: (then) => { runPreset(0, PI2, then) },
right: (then) => { runPreset(PI2, PI2, then) }, right: (then) => { runPreset(PI2, PI2, then) },
left: (then) => { runPreset(-PI2, PI2, then) }, left: (then) => { runPreset(-PI2, PI2, then) },
reset: () => { viewControl.reset(); requestRefresh() }, reset: () => {
load: (cam) => { viewControl.setPosition(cam); requestRefresh() }, viewControl.reset();
save: () => { return viewControl.getPosition(true) }, requestRefresh()
panTo: (x,y,z,l,u,t,upVec) => { tweenCamPan(x,y,z,l,u,t,upVec) }, },
setZoom: (r,v) => { viewControl.setZoom(r,v) }, load: (cam) => {
fit: (then, opts = {}) => { viewControl.setPosition(cam);
requestRefresh();
},
save: () => {
return viewControl.getPosition(true);
},
panTo: (x,y,z,l,u,t,upVec) => {
tweenCamPan(x,y,z,l,u,t,upVec);
},
setZoom: (r,v) => {
viewControl.setZoom(r,v);
},
fit: (then, opts = {}) => {
// Calculate bounding box of all objects in the workspace // Calculate bounding box of all objects in the workspace
const box = new THREE.Box3(); const box = new THREE.Box3();
let hasObjects = false; let hasObjects = false;
const visibleOnly = opts.visibleOnly !== undefined ? !!opts.visibleOnly : fitVisibleOnly; const visibleOnly = opts.visibleOnly !== undefined ? !!opts.visibleOnly : fitVisibleOnly;
const targetObjects = Array.isArray(opts.objects) ? opts.objects.filter(Boolean) : null;
// Recursively expand box for all visible objects with geometry if (targetObjects && targetObjects.length) {
WORLD.traverse(obj => { // Fit only the supplied objects (selection-driven fit in app code)
if (!obj.geometry) return; for (const obj of targetObjects) {
if (visibleOnly && !isEffectivelyVisible(obj)) return; if (!obj) continue;
if (obj.visible) { if (visibleOnly && !isEffectivelyVisible(obj)) continue;
box.expandByObject(obj); box.expandByObject(obj);
hasObjects = true; hasObjects = true;
} }
}); } else {
// Recursively expand box for all visible objects with geometry
WORLD.traverse(obj => {
if (!obj.geometry) return;
if (visibleOnly && !isEffectivelyVisible(obj)) return;
if (obj.visible) {
box.expandByObject(obj);
hasObjects = true;
}
});
}
// If no objects, fall back to platform bounds // If no objects, fall back to platform bounds
if (!hasObjects) { if (!hasObjects) {
@ -1690,7 +1713,7 @@ let Space = {
const newPanX = center.x; const newPanX = center.x;
const newPanY = center.y; const newPanY = center.y;
const newPanZ = center.z; const newPanZ = center.z;
const currentScaleSave = viewControl.getPosition(true).scale || 1; const currentScaleSave = viewControl.getPosition({ scaled: true }).scale || 1;
const currentDistToCenter = camera.position.distanceTo(center); const currentDistToCenter = camera.position.distanceTo(center);
const fitPos = { const fitPos = {

View file

@ -693,7 +693,7 @@ async function applyChamferFeature(solids, meshCache, feature, makeBodyId, bodyS
solids.splice(targetIndex, 1, nextSolid); solids.splice(targetIndex, 1, nextSolid);
meshCache.delete(solidId); meshCache.delete(solidId);
meshCache.set(nextId, result.mesh); meshCache.set(nextId, result.mesh);
console.log('void.chamfer.applied', { if (false) console.log('void.chamfer.applied', {
featureId: feature?.id, featureId: feature?.id,
solidId, solidId,
cutters: tools.length, cutters: tools.length,

View file

@ -675,7 +675,7 @@ const toolbar = {
this.docNameEl.textContent = name; this.docNameEl.textContent = name;
this.docNameEl.title = name; this.docNameEl.title = name;
} }
document.title = `${name} - Void:Form`; document.title = `${name} | Void:Form`;
}, },
buildOpenDialog() { buildOpenDialog() {

View file

@ -859,6 +859,11 @@ details[open] summary::after {
z-index: 1000; z-index: 1000;
} }
.dark #curtain {
color: white;
background-color: black;
}
#tracker { #tracker {
top: 0; top: 0;
left: 0; left: 0;

View file

@ -17,6 +17,12 @@
<meta http-equiv="origin-trial" content="AvttY0bfMDdg4vBwjn5k4Yv/+OmqjNj4bTRvCgBpP7hkI6base2DxEViSebcOyglERiFV7g0DaVmI+yv79ftDw8AAAB0eyJvcmlnaW4iOiJodHRwczovL2dyaWQuc3BhY2U6NDQzIiwiZmVhdHVyZSI6IlVucmVzdHJpY3RlZFNoYXJlZEFycmF5QnVmZmVyIiwiZXhwaXJ5IjoxNzc5MTQ4ODAwLCJpc1N1YmRvbWFpbiI6dHJ1ZX0="> <meta http-equiv="origin-trial" content="AvttY0bfMDdg4vBwjn5k4Yv/+OmqjNj4bTRvCgBpP7hkI6base2DxEViSebcOyglERiFV7g0DaVmI+yv79ftDw8AAAB0eyJvcmlnaW4iOiJodHRwczovL2dyaWQuc3BhY2U6NDQzIiwiZmVhdHVyZSI6IlVucmVzdHJpY3RlZFNoYXJlZEFycmF5QnVmZmVyIiwiZXhwaXJ5IjoxNzc5MTQ4ODAwLCJpc1N1YmRvbWFpbiI6dHJ1ZX0=">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Kiri:Moto</title> <title>Kiri:Moto</title>
<script>
if (localStorage['kiri-dark'] === 'true') {
document.documentElement.classList.add('dark');
document.documentElement.style.background = '#000000';
}
</script>
<link rel="icon" href="/icon/kirimoto.png"> <link rel="icon" href="/icon/kirimoto.png">
<link rel="apple-touch-icon" href="/icon/kirimoto.png"> <link rel="apple-touch-icon" href="/icon/kirimoto.png">
<link rel="stylesheet" type="text/css" href="index.css"> <link rel="stylesheet" type="text/css" href="index.css">

View file

@ -1,11 +1,10 @@
:root { :root {
--accent: #5a9fd4;
--menu-blue: #0079ff; --menu-blue: #0079ff;
--menu-back: rgba(255,255,255,0.55); --menu-back: rgba(60,60,60,0.75);
--dark-menu-back: rgba(80,80,80,0.75); --border: #444;
--border: #888; --selected: rgba(90,159,212,0.5);
--dark-border: #888; --selected-hover: rgba(90,159,212,1);
--selected: rgba(0,255,0,0.5);
--selected-hover: rgba(0,255,0,0.8);
} }
@font-face { @font-face {
@ -54,12 +53,18 @@ input {
font-family: sans-serif; font-family: sans-serif;
font-weight: normal; font-weight: normal;
font-size: larger; font-size: larger;
background: #0f1116;
bottom: 0; bottom: 0;
right: 0; right: 0;
left: 0; left: 0;
top: 0; top: 0;
} }
#app.booting #top,
#app.booting #app-body {
pointer-events: none;
}
#app-body { #app-body {
flex-grow: 1; flex-grow: 1;
} }
@ -75,8 +80,13 @@ input {
right: 0; right: 0;
bottom: 0; bottom: 0;
position: fixed; position: fixed;
background-color: #fff; background: radial-gradient(circle at 50% 40%, #1a2436 0%, #0f1116 60%, #0a0c11 100%);
color: #d7deea;
font-family: 'Russo One', sans-serif; font-family: 'Russo One', sans-serif;
font-size: 16px;
letter-spacing: 0.08em;
text-transform: uppercase;
text-shadow: 0 1px 2px rgba(0,0,0,0.35);
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
@ -84,6 +94,7 @@ input {
#container { #container {
z-index: 1; z-index: 1;
position: fixed; position: fixed;
background: #0f1116;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
@ -94,14 +105,9 @@ input {
overflow: hidden; overflow: hidden;
} }
.dark #top {
color: #eee;
background-color: var(--dark-menu-back);
}
#top { #top {
z-index: 50; z-index: 50;
color: #000; color: #eee;
font-size: 14px; font-size: 14px;
flex-direction: row; flex-direction: row;
background-color: var(--menu-back); background-color: var(--menu-back);
@ -126,6 +132,10 @@ input {
margin-bottom: -14px !important; margin-bottom: -14px !important;
} }
#modal button {
border-radius: 3px;
}
button { button {
padding: 2px 3px 2px 3px; padding: 2px 3px 2px 3px;
border: 1px solid rgba(150,150,150,0.5); border: 1px solid rgba(150,150,150,0.5);
@ -134,12 +144,8 @@ button {
outline: none; outline: none;
} }
.dark button:hover {
background-color: rgba(220,220,220,1);
}
button:hover { button:hover {
background-color: rgba(210,210,210,1); background-color: rgba(220,220,220,1);
} }
/** misc ui **/ /** misc ui **/
@ -168,47 +174,85 @@ button:hover {
padding: 0 !important; padding: 0 !important;
} }
.menu, #top-right > div { .title {
cursor: default;
align-self: stretch;
align-items: center;
margin: 4px 0 4px 0;
padding: 4px 12px 4px 12px;
border-radius: 0;
user-select: none;
font-weight: bold;
color: var(--accent);
}
.menubar-separator {
align-self: stretch;
align-items: center;
background: rgba(255,255,255,0.2);
margin: 5px;
width: 1px;
}
.toolbar-separator {
align-self: stretch;
align-items: center;
background: rgba(255,255,255,0.2);
margin: 2px;
height: 1px;
}
.menu, #top-doc-name, #top-mode-label {
cursor: default; cursor: default;
align-self: stretch; align-self: stretch;
align-items: center; align-items: center;
padding: 8px 12px 8px 12px; padding: 8px 12px 8px 12px;
border: 1px solid transparent;
border-radius: 6px; border-radius: 6px;
user-select: none; user-select: none;
margin: 2px;
} }
.dark .menu hr { .menu hr {
border-top: 0.5px solid rgba(255,255,255,0.5); border-top: 0.5px solid rgba(255,255,255,0.5);
} }
.menu:hover, .menu-items > div:hover, .tools i:hover { .menu:hover, .menu-items > div:hover, .tools i:hover {
background-color: var(--menu-blue); border: 1px solid var(--accent);
background-color: rgba(0,0,0,0.5);
} }
.menu:hover .menu-items { .menu:hover .menu-items {
display: flex; display: flex;
} }
.dark .menu-items {
background: #666;
border: 1px solid #999;
}
.menu-items { .menu-items {
display: none; display: none;
position: absolute; position: absolute;
flex-direction: column; flex-direction: column;
border: 1px solid #bbb; border: 1px solid #999;
border-radius: 6px; border-radius: 6px;
background: #eee; background: #333;
padding: 4px; padding: 4px;
top: 100%; top: calc(100% + 3px);
left: 0; left: 0;
gap: 6px; gap: 6px;
} }
/* invisible bridge between menu label and pop menu */
.menu-items::before {
content: "";
position: absolute;
top: -20px;
left: -10px;
right: -10px;
height: calc(100% + 20px);
background: transparent;
}
.menu-items > div { .menu-items > div {
gap: 15px; gap: 15px;
border: 1px solid transparent;
border-radius: 6px; border-radius: 6px;
padding: 4px 8px 4px 8px; padding: 4px 8px 4px 8px;
} }
@ -221,39 +265,6 @@ button:hover {
display: flex; display: flex;
} }
#mode-label {
display: none;
position: absolute;
text-transform: capitalize;
padding: 2px 4px 2px 4px;
background-color: rgba(0,200,0,0.35);
border: 1px solid rgba(0,0,0,0.25);
border-radius: 3px;
transform: translateX(-50%);
top: calc(100% + 2px);
left: 50%;
}
#mode-label:before {
content: '';
position: absolute;
top: -10px; /* Adjust to position the caret above the menu */
left: 50%;
transform: translateX(-50%);
border-width: 5px;
border-style: solid;
border-color: transparent transparent #777 transparent;
}
.dark #mode-label:before {
border-color: transparent transparent #ddd transparent;
}
.dark #mode-label {
background-color: rgba(0,200,0,0.65);
border-color: rgba(255,255,255,0.25);
}
#top-mid { #top-mid {
flex-grow: 1; flex-grow: 1;
} }
@ -268,6 +279,30 @@ button:hover {
gap: 8px; gap: 8px;
} }
#top-mode-label {
color: var(--accent);
white-space: nowrap;
justify-content: flex-end;
text-transform: capitalize;
}
#top-doc-name {
cursor: pointer;
max-width: 320px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
justify-content: flex-end;
border-color: #404040;
background-color: #202020;
transition: border-color 0.15s, color 0.15s;
}
#top-doc-name:hover {
border-color: var(--accent);
background-color: rgba(0,0,0,0.5);
}
/** specific modal dialogs */ /** specific modal dialogs */
.export { .export {
@ -313,6 +348,52 @@ button:hover {
text-align: center; text-align: center;
} }
.doc-open-list {
display: flex;
flex-direction: column;
min-width: 420px;
max-height: 60vh;
overflow: auto;
gap: 6px;
}
.doc-open-row {
display: flex;
align-items: center;
gap: 8px;
border: 1px solid var(--menu-border, #bbb);
border-radius: 6px;
padding: 6px 8px;
}
.doc-open-row:hover {
background-color: var(--menu-blue);
}
.doc-open-name {
flex: 1;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.doc-open-empty {
padding: 8px;
text-align: center;
opacity: 0.7;
}
.doc-open-new {
width: 100%;
}
.doc-open-del {
min-width: 28px;
padding: 2px 8px;
font-weight: 700;
}
.image-import { .image-import {
flex-direction: column; flex-direction: column;
gap: 3px; gap: 3px;
@ -348,12 +429,18 @@ button:hover {
} }
#modal_frame { #modal_frame {
border-radius: 4px !important; background-color: rgba(30,30,30,0.9);
background-color: #fff !important; border: 1px solid rgba(200,200,200,0.6);
border-radius: 4px;
color: white;
flex-direction: column; flex-direction: column;
padding: 0 !important; /* override common below */
/* z-index: 51; */
gap: 2px; gap: 2px;
margin: 0 0 2px 0;
padding: 3px;
}
#modal_frame a {
color: var(--accent);
} }
#modal_frame > div { #modal_frame > div {
@ -362,10 +449,10 @@ button:hover {
} }
#modal_title { #modal_title {
border-top-left-radius: 4px; color: white;
border-top-right-radius: 4px; border-radius: 2px;
border-bottom: 1px solid gray; border-bottom: 1px solid rgba(0,0,0,0.2);
background-color: rgba(0,0,0,0.2); background-color: rgba(0,0,0,0.7);
font-family: 'Russo One', monospace; font-family: 'Russo One', monospace;
font-size: smaller; font-size: smaller;
justify-content: center; justify-content: center;
@ -374,13 +461,13 @@ button:hover {
#modal_title_close { #modal_title_close {
position: absolute; position: absolute;
color: #555; color: #999;
right: 3px; right: 3px;
top: 2px; top: 2px;
} }
#modal_title_close:hover { #modal_title_close:hover {
color: black; color: #eee;
} }
/** welcome dialog **/ /** welcome dialog **/
@ -394,13 +481,12 @@ button:hover {
} }
.welcome a { .welcome a {
color: #038;
border-radius: 3px; border-radius: 3px;
padding: 0 10px 0 10px; padding: 2px 10px 2px 10px;
} }
.welcome a:hover { .welcome a:hover {
background-color: #ddd; background-color: #111;
} }
.welcome .choice { .welcome .choice {
@ -412,18 +498,18 @@ button:hover {
.settings { .settings {
gap: 5px; gap: 5px;
display: grid; display: grid;
background-color: #fff; /* background-color: #fff; */
grid: min-content 1fr / min-content 1fr; grid: min-content 1fr / min-content 1fr;
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
} }
.settings > div { .settings > div {
display: grid; display: grid;
background-color: #f5f5f5; background-color: rgba(0,0,0,0.1);
grid: min-content 1fr / min-content 1fr; grid: min-content 1fr / min-content 1fr;
white-space: nowrap; white-space: nowrap;
align-items: center; align-items: center;
border: 1px solid #ddd; border: 1px solid rgba(100,100,100,0.7);
border-radius: 3px; border-radius: 3px;
padding: 3px; padding: 3px;
} }
@ -433,7 +519,7 @@ button:hover {
text-align: center; text-align: center;
margin-bottom: 5px; margin-bottom: 5px;
border-radius: 3px; border-radius: 3px;
background-color: #ccc; background-color: rgba(100,100,100,0.8);
padding: 0; padding: 0;
} }
@ -477,12 +563,8 @@ button:hover {
/** common look & feel */ /** common look & feel */
.dark #modal_frame, .dark #actions > div, .dark #grouplist > div { #actions > div, #grouplist > div {
border: 1px solid #999; border: 1px solid #555;
}
#modal_frame, #actions > div, #grouplist > div {
border: 1px solid gray;
border-radius: 3px; border-radius: 3px;
margin: 0 0 2px 0; margin: 0 0 2px 0;
padding: 3px; padding: 3px;
@ -502,24 +584,18 @@ button:hover {
background-color: var(--selected-hover); background-color: var(--selected-hover);
} }
/* .dark .head { /* .head {
color: #000 !important; color: #000 !important;
} */ } */
/** slide in/out logging window **/ /** slide in/out logging window **/
.dark #logger {
border-color: var(--dark-border);
background-color: var(--dark-menu-back);
color: #ddd;
}
#logger { #logger {
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
border-right: 1px solid var(--border); border-right: 1px solid var(--border);
background-color: var(--menu-back); background-color: var(--menu-back);
display: none; display: none;
color: #555; color: #ddd;
min-width: 300px; min-width: 300px;
flex-direction: column; flex-direction: column;
position: absolute; position: absolute;
@ -561,28 +637,24 @@ button:hover {
border-right: 1px solid var(--border); border-right: 1px solid var(--border);
} }
.dark .tools {
border-color: var(--dark-border);
background-color: var(--dark-menu-back);
}
.dark .tools > div {
color: #fff;
}
.tools { .tools {
align-items: flex-start; align-items: flex-start;
background-color: var(--menu-back); background-color: var(--menu-back);
padding-top: 2px;
} }
.tools > div { .tools > div {
color: #fff;
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
margin: 2px;
} }
.tools i { .tools i {
padding: 9px; padding: 9px;
border-bottom: 1px solid var(--border); border: 1px solid transparent;
border-radius: 6px;
/* border-bottom: 1px solid var(--border); */
} }
.tool > div { .tool > div {
@ -597,11 +669,6 @@ button:hover {
width: 100%; width: 100%;
} }
.dark .tool label {
background-color: var(--dark-menu-back);
border: 1px solid var(--dark-border);
}
.tool label:before { .tool label:before {
content: ''; content: '';
position: absolute; position: absolute;
@ -610,10 +677,6 @@ button:hover {
transform: translateY(-50%); transform: translateY(-50%);
border-width: 6px; border-width: 6px;
border-style: solid; border-style: solid;
border-color: transparent #777 transparent transparent;
}
.dark .tool label:before {
border-color: transparent #ddd transparent transparent; border-color: transparent #ddd transparent transparent;
} }
@ -715,14 +778,10 @@ button:hover {
margin: 0; margin: 0;
} }
.dark #grouplist .models .square > svg {
color: #333;
}
#grouplist .models .square > svg { #grouplist .models .square > svg {
aspect-ratio: 1; aspect-ratio: 1;
max-height: 20px; max-height: 20px;
color: #666; color: #333;
} }
#grouplist .models > div { #grouplist .models > div {
@ -851,21 +910,15 @@ button:hover {
transform: translate(-50%, 50%); transform: translate(-50%, 50%);
} }
.dark #pinner {
background-color: rgba(255,255,255,0.15);
border-color: rgba(255,255,255,0.25);
border-top-color: rgba(255,255,255,0.6);
}
#pinner { #pinner {
z-index: 5000; z-index: 5000;
display: none; display: none;
width: 50px; width: 50px;
height: 50px; height: 50px;
background-color: rgba(0,0,0,0.1); background-color: rgba(255,255,255,0.15);
border: 12px solid rgba(0,0,0,0.15); border: 12px solid rgba(255,255,255,0.25);
border-radius: 50%; border-radius: 50%;
border-top-color: rgba(0,0,0,0.6); border-top-color: rgba(255,255,255,0.6);
animation: spin 1.5s linear infinite; animation: spin 1.5s linear infinite;
} }

View file

@ -88,7 +88,6 @@ html, body {
.toolbar-title { .toolbar-title {
font-weight: 600; font-weight: 600;
font-size: 16px; font-size: 16px;
margin-right: 12px;
color: var(--accent); color: var(--accent);
} }