diff --git a/src/main/mesh.js b/src/main/mesh.js index 5f30c24c..8b1b9e58 100644 --- a/src/main/mesh.js +++ b/src/main/mesh.js @@ -114,13 +114,15 @@ function restore_space() { // restore preferences after models are restored api.prefs.load().then(() => { let { map } = api.prefs; - let { space } = map; + 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)); api.selection.set([...smodel, ...sgroup]); + // restore edit mode + api.mode.set(mode); }); }); // hide loading curtain @@ -264,11 +266,7 @@ function space_init(data) { if (model) { let group = model.group; let { ctrlKey, metaKey, shiftKey } = event; - if (shiftKey && metaKey) { - // find faces adjacent to point/line clicked - model.find(int.point, int.face); - // console.log(int); - } else if (metaKey) { + if (metaKey) { // set focus on intersected face let { x, y, z } = int.point; // y,z swap due to world rotation for orbit controls @@ -277,7 +275,18 @@ function space_init(data) { // rotate selected face towawrd z "floor" group.faceDown(int.face.normal); } else { - selection.toggle(shiftKey ? model : model.group); + let modes = api.mode.modes; + switch(api.mode.get()) { + case modes.object: + selection.toggle(shiftKey ? model : model.group); + break; + case modes.face: + case modes.line: + case modes.vertex: + // find faces adjacent to point/line clicked + model.find(int.point, int.face); + break; + } } } } else { diff --git a/src/mesh/api.js b/src/mesh/api.js index bb45bcb6..fa3fd465 100644 --- a/src/mesh/api.js +++ b/src/mesh/api.js @@ -121,12 +121,6 @@ let selection = { util.defer(selection.update); }, - duplicate() { - for (let m of selection.models()) { - m.duplicate(); - } - }, - // update selection and wireframe for all objects update() { for (let group of groups) { @@ -309,6 +303,12 @@ let tool = { }); }, + duplicate() { + for (let m of selection.models()) { + m.duplicate(); + } + }, + analyze(models) { models = fallback(models); api.log.emit('analyzing mesh(es)...').pin(); @@ -377,10 +377,46 @@ let tool = { } }; +let modes = { object: "object", face: "face", line: "line", vertex: "vertex" }; + +// edit mode +let mode = { + set(mode) { + prefs.save(prefs.map.mode = mode); + for (let key of Object.values(modes)) { + $(`mode-${key}`).classList.remove('selected'); + } + $(`mode-${mode}`).classList.add('selected'); + }, + + get() { + return prefs.map.mode; + }, + + object() { + mode.set(modes.object); + }, + + face() { + mode.set(modes.face); + }, + + line() { + mode.set(modes.line); + }, + + vertex() { + mode.set(modes.vertex); + }, + + modes +}; + // persisted preference map let prefs = { map: { info: {}, + mode: modes.object, space: { wire: false, grid: true, @@ -460,6 +496,8 @@ let api = mesh.api = { prefs.save( prefs.map.space.wire = wire ); }, + mode, + selection, group, diff --git a/src/mesh/build.js b/src/mesh/build.js index e1a3db65..29b67934 100644 --- a/src/mesh/build.js +++ b/src/mesh/build.js @@ -210,29 +210,37 @@ function ui_build() { // create slid in/out logging window h.bind(logger, [ h.div({ id: 'logtext' }) ]); + // a few shortcuts to api calls + let { file, selection, mode, tool, prefs } = api; + // create hotkey/action menu (top/left) h.bind(actions, [ h.div([ // create and bind file loading elements - h.button({ _: 'import', onclick: api.file.import }, [ + h.button({ _: 'import', onclick: file.import }, [ h.input({ id: "import", type: "file", class: ["hide"], multiple: true, onchange(evt) { broker.send.load_files(evt.target.files) } }) ]), - h.button({ _: 'export', onclick: api.file.export }), + h.button({ _: 'export', onclick: file.export }), h.div({ class: "vsep" }), - h.button({ _: 'visible', onclick() { api.selection.visible({ toggle: true }) } }), - h.button({ _: 'bounds', onclick() { api.selection.boundsBox({ toggle: true }) } }), + h.button({ _: 'visible', onclick() { selection.visible({ toggle: true }) } }), + h.button({ _: 'bounds', onclick() { selection.boundsBox({ toggle: true }) } }), h.button({ _: 'gridlines', onclick() { api.grid() } }), h.button({ _: 'wireframe', onclick() { api.wireframe() } }), h.div({ class: "vsep" }), - h.button({ _: 'duplicate', onclick: api.selection.duplicate }), - h.button({ _: 'merge', onclick: api.tool.merge }), + h.button({ _: 'duplicate', onclick: tool.duplicate }), + h.button({ _: 'merge', onclick: tool.merge }), h.div({ class: "vsep" }), - h.button({ _: 'analyze', onclick: api.tool.analyze }), - h.button({ _: 'repair', onclick: api.tool.repair }), - h.button({ _: 'clean', onclick: api.tool.clean }), + h.button({ _: 'analyze', onclick: tool.analyze }), + h.button({ _: 'repair', onclick: tool.repair }), + h.button({ _: 'clean', onclick: tool.clean }), + h.div({ class: "vsep" }), + h.button({ _: 'object', id: "mode-object", onclick() { mode.object() } }), + h.button({ _: 'face', id: "mode-face", onclick() { mode.face() } }), + h.button({ _: 'line', id: "mode-line", onclick() { mode.line() } }), + h.button({ _: 'vertex', id: "mode-vertex", onclick() { mode.vertex() } }), ]), ]); @@ -261,14 +269,14 @@ function ui_build() { // update model selector list (top/right) function update_selector() { - let selHas = api.selection.contains; + let selHas = selection.contains; // map groups to divs let groups = api.group.list() .map(g => h.div([ h.button({ _: g.name || `group`, title: g.id, class: [ "group", selHas(g) ? 'selected' : undefined ], onclick(e) { - e.shiftKey ? api.selection.toggle(g) : api.selection.set([g]) + e.shiftKey ? selection.toggle(g) : selection.set([g]) } }), h.div({ class: "vsep" }), @@ -280,10 +288,10 @@ function ui_build() { m.visible() ? undefined : 'hidden' ], onclick(e) { - let sel = api.selection.list(); + let sel = selection.list(); e.shiftKey || (sel.length === 1 && m === sel[0]) ? - api.selection.toggle(m) : - api.selection.set([m]) + selection.toggle(m) : + selection.set([m]) } })) ) @@ -294,8 +302,8 @@ function ui_build() { // update model information dashboard (bottom) function update_selection() { let map = { fixed: 2 }; - let s_grp = api.selection.groups(); - let s_mdl = api.selection.models(); + let s_grp = selection.groups(); + let s_mdl = selection.models(); if (s_mdl.length === 0) { return h.bind(selectlist, []); } @@ -359,20 +367,20 @@ function ui_build() { ...h_ara, ...h_msh ]); - let prefs = api.prefs.map.info; + let pmap = prefs.map.info; // map buttons to show/hide selection info function toggle(label, h, s, d) { s.onclick = () => { s.style.display = 'none'; d.style.display = 'flex'; - api.prefs.save( prefs[label] = 'show' ); // <- :) + prefs.save( pmap[label] = 'show' ); // <- :) }; h.onclick = () => { d.style.display = 'none'; s.style.display = 'flex'; - api.prefs.save( prefs[label] = 'hide' ); + prefs.save( pmap[label] = 'hide' ); }; - if (prefs[label] === 'show') { + if (pmap[label] === 'show') { s.onclick(); } else { h.onclick(); diff --git a/web/mesh/index.css b/web/mesh/index.css index ede583e0..64403dec 100644 --- a/web/mesh/index.css +++ b/web/mesh/index.css @@ -280,6 +280,16 @@ button:hover { border-bottom: 1px dashed gray; } +/** actions menu top/left **/ + +#actions button.selected { + background-color: rgba(0,255,0,0.5); +} + +#actions button.selected:hover { + background-color: rgba(0,255,0,1); +} + /** slide in/out logging window **/ #logger {