updating mesh visuals
add document history
This commit is contained in:
parent
a17128ee87
commit
913977ec64
18 changed files with 1050 additions and 274 deletions
21
app.js
21
app.js
|
|
@ -26,6 +26,7 @@ const mods = {};
|
|||
const load = [];
|
||||
const api = {};
|
||||
|
||||
let lastTouchTime = {};
|
||||
let forceUseCache = false;
|
||||
let serviceWorker = true;
|
||||
let crossOrigin = false;
|
||||
|
|
@ -213,9 +214,19 @@ function init(mod) {
|
|||
}
|
||||
}
|
||||
|
||||
// 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`;
|
||||
|
|
@ -223,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);
|
||||
|
|
@ -239,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) {
|
||||
|
|
|
|||
|
|
@ -171,6 +171,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);
|
||||
|
|
|
|||
|
|
@ -172,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.classList.add('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.classList.remove('dark');
|
||||
}
|
||||
space.platform.setSize();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -387,7 +387,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 +395,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 +446,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;
|
||||
}
|
||||
|
|
|
|||
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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -109,10 +109,11 @@ function setupLeftPanelResize(db) {
|
|||
event.stopPropagation();
|
||||
};
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
const curr = left.getBoundingClientRect().width;
|
||||
applyWidth(curr);
|
||||
});
|
||||
const onResize = () => {
|
||||
applyWidth(left.getBoundingClientRect().width);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', onResize);
|
||||
}
|
||||
|
||||
// Main initialization function
|
||||
|
|
@ -289,15 +290,12 @@ async function init() {
|
|||
toolbar.updateDocumentTitle();
|
||||
tree.render();
|
||||
|
||||
// TEST: Add example overlay elements
|
||||
// These demonstrate the 2D overlay tracking 3D points
|
||||
if (true) { // Set to false to disable test overlays
|
||||
const { THREE } = window;
|
||||
|
||||
// Show overlay
|
||||
overlay.show();
|
||||
|
||||
// Add test points at origin and along axes
|
||||
// Add origin
|
||||
overlay.add('origin-point', 'point', {
|
||||
pos3d: new THREE.Vector3(0, 0, 0),
|
||||
radius: 4.8,
|
||||
|
|
@ -307,9 +305,6 @@ async function init() {
|
|||
});
|
||||
api.origin.syncOverlayPoint();
|
||||
|
||||
console.log({ test_overlays_added: 1 });
|
||||
}
|
||||
|
||||
// Hide loading curtain
|
||||
const curtain = $('curtain');
|
||||
if (curtain) {
|
||||
|
|
@ -320,6 +315,9 @@ async function init() {
|
|||
}, 300);
|
||||
}
|
||||
|
||||
// update canvas based on left panel size
|
||||
space.event.onResize();
|
||||
|
||||
console.log({ void_form_ready: true });
|
||||
}
|
||||
|
||||
|
|
|
|||
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,6 +390,8 @@ 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" }, [
|
||||
|
|
@ -398,12 +399,17 @@ function ui_build() {
|
|||
id: "import", type: "file", class: ["hide"], multiple: true, accept:".stl,.obj",
|
||||
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('Preferences', api.settings, 'Q'),
|
||||
hr(),
|
||||
menu_item('Close', () => window.close() || api.kirimoto()),
|
||||
])
|
||||
]),
|
||||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
@ -235,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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1560,17 +1560,39 @@ let Space = {
|
|||
front: (then) => { runPreset(0, PI2, then) },
|
||||
right: (then) => { runPreset(PI2, PI2, then) },
|
||||
left: (then) => { runPreset(-PI2, PI2, then) },
|
||||
reset: () => { viewControl.reset(); requestRefresh() },
|
||||
load: (cam) => { 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) },
|
||||
reset: () => {
|
||||
viewControl.reset();
|
||||
requestRefresh()
|
||||
},
|
||||
load: (cam) => {
|
||||
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
|
||||
const box = new THREE.Box3();
|
||||
let hasObjects = false;
|
||||
const visibleOnly = opts.visibleOnly !== undefined ? !!opts.visibleOnly : fitVisibleOnly;
|
||||
const targetObjects = Array.isArray(opts.objects) ? opts.objects.filter(Boolean) : null;
|
||||
|
||||
if (targetObjects && targetObjects.length) {
|
||||
// Fit only the supplied objects (selection-driven fit in app code)
|
||||
for (const obj of targetObjects) {
|
||||
if (!obj) continue;
|
||||
if (visibleOnly && !isEffectivelyVisible(obj)) continue;
|
||||
box.expandByObject(obj);
|
||||
hasObjects = true;
|
||||
}
|
||||
} else {
|
||||
// Recursively expand box for all visible objects with geometry
|
||||
WORLD.traverse(obj => {
|
||||
if (!obj.geometry) return;
|
||||
|
|
@ -1580,6 +1602,7 @@ let Space = {
|
|||
hasObjects = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If no objects, fall back to platform bounds
|
||||
if (!hasObjects) {
|
||||
|
|
@ -1690,7 +1713,7 @@ let Space = {
|
|||
const newPanX = center.x;
|
||||
const newPanY = center.y;
|
||||
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 fitPos = {
|
||||
|
|
|
|||
|
|
@ -693,7 +693,7 @@ async function applyChamferFeature(solids, meshCache, feature, makeBodyId, bodyS
|
|||
solids.splice(targetIndex, 1, nextSolid);
|
||||
meshCache.delete(solidId);
|
||||
meshCache.set(nextId, result.mesh);
|
||||
console.log('void.chamfer.applied', {
|
||||
if (false) console.log('void.chamfer.applied', {
|
||||
featureId: feature?.id,
|
||||
solidId,
|
||||
cutters: tools.length,
|
||||
|
|
|
|||
|
|
@ -675,7 +675,7 @@ const toolbar = {
|
|||
this.docNameEl.textContent = name;
|
||||
this.docNameEl.title = name;
|
||||
}
|
||||
document.title = `${name} - Void:Form`;
|
||||
document.title = `${name} | Void:Form`;
|
||||
},
|
||||
|
||||
buildOpenDialog() {
|
||||
|
|
|
|||
|
|
@ -859,6 +859,11 @@ details[open] summary::after {
|
|||
z-index: 1000;
|
||||
}
|
||||
|
||||
.dark #curtain {
|
||||
color: white;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
#tracker {
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,12 @@
|
|||
<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">
|
||||
<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="apple-touch-icon" href="/icon/kirimoto.png">
|
||||
<link rel="stylesheet" type="text/css" href="index.css">
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
:root {
|
||||
--accent: #5a9fd4;
|
||||
--menu-blue: #0079ff;
|
||||
--menu-back: rgba(255,255,255,0.55);
|
||||
--dark-menu-back: rgba(80,80,80,0.75);
|
||||
--border: #888;
|
||||
--dark-border: #888;
|
||||
--selected: rgba(0,255,0,0.5);
|
||||
--selected-hover: rgba(0,255,0,0.8);
|
||||
--menu-back: rgba(60,60,60,0.75);
|
||||
--border: #444;
|
||||
--selected: rgba(90,159,212,0.5);
|
||||
--selected-hover: rgba(90,159,212,1);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
|
|
@ -54,12 +53,18 @@ input {
|
|||
font-family: sans-serif;
|
||||
font-weight: normal;
|
||||
font-size: larger;
|
||||
background: #0f1116;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
#app.booting #top,
|
||||
#app.booting #app-body {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#app-body {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
|
@ -75,8 +80,13 @@ input {
|
|||
right: 0;
|
||||
bottom: 0;
|
||||
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-size: 16px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
text-shadow: 0 1px 2px rgba(0,0,0,0.35);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
|
@ -84,6 +94,7 @@ input {
|
|||
#container {
|
||||
z-index: 1;
|
||||
position: fixed;
|
||||
background: #0f1116;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
|
@ -94,14 +105,9 @@ input {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dark #top {
|
||||
color: #eee;
|
||||
background-color: var(--dark-menu-back);
|
||||
}
|
||||
|
||||
#top {
|
||||
z-index: 50;
|
||||
color: #000;
|
||||
color: #eee;
|
||||
font-size: 14px;
|
||||
flex-direction: row;
|
||||
background-color: var(--menu-back);
|
||||
|
|
@ -126,6 +132,10 @@ input {
|
|||
margin-bottom: -14px !important;
|
||||
}
|
||||
|
||||
#modal button {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 2px 3px 2px 3px;
|
||||
border: 1px solid rgba(150,150,150,0.5);
|
||||
|
|
@ -134,12 +144,8 @@ button {
|
|||
outline: none;
|
||||
}
|
||||
|
||||
.dark button:hover {
|
||||
background-color: rgba(220,220,220,1);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: rgba(210,210,210,1);
|
||||
background-color: rgba(220,220,220,1);
|
||||
}
|
||||
|
||||
/** misc ui **/
|
||||
|
|
@ -168,47 +174,85 @@ button:hover {
|
|||
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;
|
||||
align-self: stretch;
|
||||
align-items: center;
|
||||
padding: 8px 12px 8px 12px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
user-select: none;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.dark .menu hr {
|
||||
.menu hr {
|
||||
border-top: 0.5px solid rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.dark .menu-items {
|
||||
background: #666;
|
||||
border: 1px solid #999;
|
||||
}
|
||||
|
||||
.menu-items {
|
||||
display: none;
|
||||
position: absolute;
|
||||
flex-direction: column;
|
||||
border: 1px solid #bbb;
|
||||
border: 1px solid #999;
|
||||
border-radius: 6px;
|
||||
background: #eee;
|
||||
background: #333;
|
||||
padding: 4px;
|
||||
top: 100%;
|
||||
top: calc(100% + 3px);
|
||||
left: 0;
|
||||
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 {
|
||||
gap: 15px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
padding: 4px 8px 4px 8px;
|
||||
}
|
||||
|
|
@ -221,39 +265,6 @@ button:hover {
|
|||
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 {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
|
@ -268,6 +279,30 @@ button:hover {
|
|||
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 */
|
||||
|
||||
.export {
|
||||
|
|
@ -313,6 +348,52 @@ button:hover {
|
|||
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 {
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
|
|
@ -348,12 +429,18 @@ button:hover {
|
|||
}
|
||||
|
||||
#modal_frame {
|
||||
border-radius: 4px !important;
|
||||
background-color: #fff !important;
|
||||
background-color: rgba(30,30,30,0.9);
|
||||
border: 1px solid rgba(200,200,200,0.6);
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
flex-direction: column;
|
||||
padding: 0 !important; /* override common below */
|
||||
/* z-index: 51; */
|
||||
gap: 2px;
|
||||
margin: 0 0 2px 0;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
#modal_frame a {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
#modal_frame > div {
|
||||
|
|
@ -362,10 +449,10 @@ button:hover {
|
|||
}
|
||||
|
||||
#modal_title {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom: 1px solid gray;
|
||||
background-color: rgba(0,0,0,0.2);
|
||||
color: white;
|
||||
border-radius: 2px;
|
||||
border-bottom: 1px solid rgba(0,0,0,0.2);
|
||||
background-color: rgba(0,0,0,0.7);
|
||||
font-family: 'Russo One', monospace;
|
||||
font-size: smaller;
|
||||
justify-content: center;
|
||||
|
|
@ -374,13 +461,13 @@ button:hover {
|
|||
|
||||
#modal_title_close {
|
||||
position: absolute;
|
||||
color: #555;
|
||||
color: #999;
|
||||
right: 3px;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
#modal_title_close:hover {
|
||||
color: black;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
/** welcome dialog **/
|
||||
|
|
@ -394,13 +481,12 @@ button:hover {
|
|||
}
|
||||
|
||||
.welcome a {
|
||||
color: #038;
|
||||
border-radius: 3px;
|
||||
padding: 0 10px 0 10px;
|
||||
padding: 2px 10px 2px 10px;
|
||||
}
|
||||
|
||||
.welcome a:hover {
|
||||
background-color: #ddd;
|
||||
background-color: #111;
|
||||
}
|
||||
|
||||
.welcome .choice {
|
||||
|
|
@ -412,18 +498,18 @@ button:hover {
|
|||
.settings {
|
||||
gap: 5px;
|
||||
display: grid;
|
||||
background-color: #fff;
|
||||
/* background-color: #fff; */
|
||||
grid: min-content 1fr / min-content 1fr;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
|
||||
.settings > div {
|
||||
display: grid;
|
||||
background-color: #f5f5f5;
|
||||
background-color: rgba(0,0,0,0.1);
|
||||
grid: min-content 1fr / min-content 1fr;
|
||||
white-space: nowrap;
|
||||
align-items: center;
|
||||
border: 1px solid #ddd;
|
||||
border: 1px solid rgba(100,100,100,0.7);
|
||||
border-radius: 3px;
|
||||
padding: 3px;
|
||||
}
|
||||
|
|
@ -433,7 +519,7 @@ button:hover {
|
|||
text-align: center;
|
||||
margin-bottom: 5px;
|
||||
border-radius: 3px;
|
||||
background-color: #ccc;
|
||||
background-color: rgba(100,100,100,0.8);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
|
@ -477,12 +563,8 @@ button:hover {
|
|||
|
||||
/** common look & feel */
|
||||
|
||||
.dark #modal_frame, .dark #actions > div, .dark #grouplist > div {
|
||||
border: 1px solid #999;
|
||||
}
|
||||
|
||||
#modal_frame, #actions > div, #grouplist > div {
|
||||
border: 1px solid gray;
|
||||
#actions > div, #grouplist > div {
|
||||
border: 1px solid #555;
|
||||
border-radius: 3px;
|
||||
margin: 0 0 2px 0;
|
||||
padding: 3px;
|
||||
|
|
@ -502,24 +584,18 @@ button:hover {
|
|||
background-color: var(--selected-hover);
|
||||
}
|
||||
|
||||
/* .dark .head {
|
||||
/* .head {
|
||||
color: #000 !important;
|
||||
} */
|
||||
|
||||
/** slide in/out logging window **/
|
||||
|
||||
.dark #logger {
|
||||
border-color: var(--dark-border);
|
||||
background-color: var(--dark-menu-back);
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
#logger {
|
||||
border-top: 1px solid var(--border);
|
||||
border-right: 1px solid var(--border);
|
||||
background-color: var(--menu-back);
|
||||
display: none;
|
||||
color: #555;
|
||||
color: #ddd;
|
||||
min-width: 300px;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
|
|
@ -561,28 +637,24 @@ button:hover {
|
|||
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 {
|
||||
align-items: flex-start;
|
||||
background-color: var(--menu-back);
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.tools > div {
|
||||
color: #fff;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.tools i {
|
||||
padding: 9px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
/* border-bottom: 1px solid var(--border); */
|
||||
}
|
||||
|
||||
.tool > div {
|
||||
|
|
@ -597,11 +669,6 @@ button:hover {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.dark .tool label {
|
||||
background-color: var(--dark-menu-back);
|
||||
border: 1px solid var(--dark-border);
|
||||
}
|
||||
|
||||
.tool label:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
|
@ -610,10 +677,6 @@ button:hover {
|
|||
transform: translateY(-50%);
|
||||
border-width: 6px;
|
||||
border-style: solid;
|
||||
border-color: transparent #777 transparent transparent;
|
||||
}
|
||||
|
||||
.dark .tool label:before {
|
||||
border-color: transparent #ddd transparent transparent;
|
||||
}
|
||||
|
||||
|
|
@ -715,14 +778,10 @@ button:hover {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.dark #grouplist .models .square > svg {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#grouplist .models .square > svg {
|
||||
aspect-ratio: 1;
|
||||
max-height: 20px;
|
||||
color: #666;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#grouplist .models > div {
|
||||
|
|
@ -851,21 +910,15 @@ button:hover {
|
|||
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 {
|
||||
z-index: 5000;
|
||||
display: none;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background-color: rgba(0,0,0,0.1);
|
||||
border: 12px solid rgba(0,0,0,0.15);
|
||||
background-color: rgba(255,255,255,0.15);
|
||||
border: 12px solid rgba(255,255,255,0.25);
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -88,7 +88,6 @@ html, body {
|
|||
.toolbar-title {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
margin-right: 12px;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue