deep clone + args, rework/cleanup ui binding, preserve groups on poly re-nest

This commit is contained in:
Stewart Allen 2024-07-30 15:12:02 -04:00
commit 18b6b8567a
8 changed files with 238 additions and 222 deletions

View file

@ -90,11 +90,11 @@ AP.addOnce = function(val) {
/** /**
* shallow cloning with clone(arg) call on each new element * shallow cloning with clone(arg) call on each new element
*/ */
AP.clone = function(arg) { AP.clone = function() {
let na = this.slice(), let na = this.slice(),
ln = na.length, ln = na.length,
i = 0; i = 0;
while (i < ln) na[i] = na[i++].clone(arg); while (i < ln) na[i] = na[i++].clone(...arguments);
return na; return na;
}; };

View file

@ -913,32 +913,33 @@ class Polygon {
* @param {Boolean} deep * @param {Boolean} deep
* @returns {Polygon} * @returns {Polygon}
*/ */
clone(deep) { clone(deep, fields) {
let np = newPolygon().copyZ(this.getZ()), let np = newPolygon().copyZ(this.getZ()),
ln = this.length, ln = this.length,
i = 0; i = 0;
while (i < ln) np.push(this.points[i++]); while (i < ln) np.push(this.points[i++]);
if (this.fillang) np.fillang = this.fillang; fields && fields.forEach(field => np[field] = this[field]);
this.fillang && (np.fillang = this.fillang);
np.depth = this.depth; np.depth = this.depth;
np.open = this.open; np.open = this.open;
if (deep && this.inner) { if (deep && this.inner) {
np.inner = this.inner.clone(); np.inner = this.inner.clone(false, fields);
} }
return np; return np;
} }
// special shallow for-render-or-read-only cloning // special shallow for-render-or-read-only cloning
cloneZ(z, stop) { cloneZ(z, deep = true) {
let p = newPolygon(); let p = newPolygon();
p.z = z; p.z = z;
p.open = this.open; p.open = this.open;
p.points = this.points; p.points = this.points;
if (this.inner) { if (deep && this.inner) {
p.inner = this.inner.map(p => p.cloneZ(z, true)); p.inner = this.inner.map(p => p.cloneZ(z, false));
} }
return p; return p;
} }

View file

@ -822,7 +822,7 @@ function platformLoadFiles(files, group) {
let wid = platformLoadWidget(group, vertices, name); let wid = platformLoadWidget(group, vertices, name);
if (api.mode.is_cam()) { if (api.mode.is_cam()) {
// attach raw illustration // attach raw illustration
api.event.emit('cam.parse.gerber', { data: text, mesh: wid.mesh }); // api.event.emit('cam.parse.gerber', { data: text, mesh: wid.mesh });
} }
}); });
} }

View file

@ -251,7 +251,9 @@ function space_init(data) {
case 'KeyW': case 'KeyW':
return api.wireframe(); return api.wireframe();
case 'KeyG': case 'KeyG':
return shiftKey ? api.tool.regroup() : api.grid(); return shiftKey ?
(api.mode.is([ api.modes.sketch ]) ? api.sketch.arrange.group() : api.tool.regroup()) :
api.grid();
case 'KeyL': case 'KeyL':
return api.log.toggle({ spinner: false }); return api.log.toggle({ spinner: false });
case 'KeyS': case 'KeyS':

View file

@ -212,6 +212,11 @@ const selection = {
return selection; return selection;
}, },
update_defer() {
util.defer(selection.update);
return selection;
},
drag(opt = {}) { drag(opt = {}) {
selected.forEach(s => s.drag(opt)); selected.forEach(s => s.drag(opt));
return selection; return selection;

View file

@ -11,6 +11,8 @@ const { broker } = gapp;
const { mesh, moto } = root; const { mesh, moto } = root;
const { api, util } = mesh; const { api, util } = mesh;
const { space } = moto; const { space } = moto;
const { bind, div, label, input, hr } = h;
const devel = api.isDebug; const devel = api.isDebug;
let call = broker.send; let call = broker.send;
@ -36,7 +38,7 @@ const modal = api.modal = {
if (this.info.showing) { if (this.info.showing) {
throw `modal conflict showing "${title}"`; throw `modal conflict showing "${title}"`;
} }
let bound = h.bind($('modal'), contents); let bound = bind($('modal'), contents);
$('modal_title_text').innerText = title; $('modal_title_text').innerText = title;
$('modal_page').style.display = 'flex'; $('modal_page').style.display = 'flex';
$('modal_frame').style.display = 'flex'; $('modal_frame').style.display = 'flex';
@ -99,7 +101,7 @@ const modal = api.modal = {
let { title, body } = opt; let { title, body } = opt;
let contents = []; let contents = [];
if (typeof body === 'string') { if (typeof body === 'string') {
contents.push(h.div(body)) contents.push(div(body))
} else if (Array.isArray(body)) { } else if (Array.isArray(body)) {
contents.appendAll(body); contents.appendAll(body);
} else { } else {
@ -181,7 +183,7 @@ const log = api.log = {
// re-render and show current log messages // re-render and show current log messages
render() { render() {
let area = $('logtext'); let area = $('logtext');
h.bind(area, log.data.map(rec => h.div({ _: rec.text }))); bind(area, log.data.map(rec => div({ _: rec.text })));
area.scrollTop = area.scrollHeight; area.scrollTop = area.scrollHeight;
return log.show(); return log.show();
}, },
@ -196,7 +198,7 @@ gapp.broker.subscribe("mesh.log", msg => {
api.welcome = function(version = "unknown") { api.welcome = function(version = "unknown") {
const { prefs } = api; const { prefs } = api;
const { map } = prefs; const { map } = prefs;
modal.show('About Mesh:Tool', h.div({ class:"welcome" }, [ modal.show('About Mesh:Tool', div({ class:"welcome" }, [
h.a({ h.a({
target: "docs", target: "docs",
_: "Guide & Documentation", _: "Guide & Documentation",
@ -207,10 +209,10 @@ api.welcome = function(version = "unknown") {
_: "Grid.Space", _: "Grid.Space",
href: "https://grid.space/" href: "https://grid.space/"
}), }),
h.div(`Version: ${version}`), div(`Version: ${version}`),
h.hr({ width: "100%" }), hr({ width: "100%" }),
h.div({ class: "choice" }, [ div({ class: "choice" }, [
h.input({ input({
type: "checkbox", type: "checkbox",
[ map.info.welcome !== false ? 'checked' : 'unchecked' ] : 1, [ map.info.welcome !== false ? 'checked' : 'unchecked' ] : 1,
onchange: (ev) => { onchange: (ev) => {
@ -218,7 +220,7 @@ api.welcome = function(version = "unknown") {
prefs.save(); prefs.save();
} }
}), }),
h.div("show at startup") div("show at startup")
]), ]),
])); ]));
}; };
@ -230,83 +232,83 @@ api.settings = function() {
const { surface, normals, space, sketch, wireframe } = prefs.map; const { surface, normals, space, sketch, wireframe } = prefs.map;
const { dark } = space; const { dark } = space;
const set1 = h.div([ const set1 = div([
h.label('dark mode'), label('dark mode'),
h.input({ type: "checkbox", input({ type: "checkbox",
onchange: ev => call.set_darkmode(ev.target.checked), onchange: ev => call.set_darkmode(ev.target.checked),
[ dark ? 'checked' : 'unchecked' ] : 1 [ dark ? 'checked' : 'unchecked' ] : 1
}), }),
h.label('auto floor'), label('auto floor'),
h.input({ type: "checkbox", input({ type: "checkbox",
onchange: ev => prefs.save( space.floor = !space.floor ), onchange: ev => prefs.save( space.floor = !space.floor ),
[ space.floor !== false ? 'checked' : 'unchecked' ] : 1 [ space.floor !== false ? 'checked' : 'unchecked' ] : 1
}), }),
h.label('auto center'), label('auto center'),
h.input({ type: "checkbox", input({ type: "checkbox",
onchange: ev => prefs.save( space.center = !space.center ), onchange: ev => prefs.save( space.center = !space.center ),
[ space.center !== false ? 'checked' : 'unchecked' ] : 1 [ space.center !== false ? 'checked' : 'unchecked' ] : 1
}), }),
]); ]);
const set2 = h.div([ const set2 = div([
h.label({ class: "header", _: 'wire'}), label({ class: "header", _: 'wire'}),
h.label('opacity'), label('opacity'),
h.input({ type: "text", size: 5, value: parseFloat(wireframe.opacity), input({ type: "text", size: 5, value: parseFloat(wireframe.opacity),
onchange: ev => call.set_wireframe_opacity(ev.target.value) onchange: ev => call.set_wireframe_opacity(ev.target.value)
}), }),
h.label('fog x'), label('fog x'),
h.input({ type: "text", size: 5, value: parseFloat(wireframe.fog), input({ type: "text", size: 5, value: parseFloat(wireframe.fog),
onchange: ev => call.set_wireframe_fog(ev.target.value) onchange: ev => call.set_wireframe_fog(ev.target.value)
}), }),
]); ]);
const set3 = h.div([ const set3 = div([
h.label({ class: "header", _: 'normal'}), label({ class: "header", _: 'normal'}),
h.label('length'), label('length'),
h.input({ type: "text", size: 6, value: parseFloat(normals.length), input({ type: "text", size: 6, value: parseFloat(normals.length),
onchange: ev => call.set_normals_length(ev.target.value) onchange: ev => call.set_normals_length(ev.target.value)
}), }),
h.label('color'), label('color'),
h.input({ type: "text", size: 6, input({ type: "text", size: 6,
value: util.toHexRGB(dark ? normals.color_dark : normals.color_lite), value: util.toHexRGB(dark ? normals.color_dark : normals.color_lite),
onchange: ev => call.set_normals_color(util.fromHexRGB(ev.target.value)) onchange: ev => call.set_normals_color(util.fromHexRGB(ev.target.value))
}), }),
]); ]);
const set4 = h.div([ const set4 = div([
h.label({ class: "header", _: 'surface'}), label({ class: "header", _: 'surface'}),
h.label('radians'), label('radians'),
h.input({ type: "text", size: 5, value: parseFloat(surface.radians), input({ type: "text", size: 5, value: parseFloat(surface.radians),
onchange: ev => call.set_surface_radians(ev.target.value) onchange: ev => call.set_surface_radians(ev.target.value)
}), }),
h.label('radius'), label('radius'),
h.input({ type: "text", size: 5, value: parseFloat(surface.radius || 0.2), input({ type: "text", size: 5, value: parseFloat(surface.radius || 0.2),
onchange: ev => call.set_surface_radius(ev.target.value) onchange: ev => call.set_surface_radius(ev.target.value)
}), }),
]); ]);
const set5 = h.div([ const set5 = div([
h.label({ class: "header", _: 'snap'}), label({ class: "header", _: 'snap'}),
h.label('value'), label('value'),
h.input({ type: "text", size: 5, value: parseFloat(space.snap || 1), input({ type: "text", size: 5, value: parseFloat(space.snap || 1),
onchange: ev => call.set_snap_value(ev.target.value) onchange: ev => call.set_snap_value(ev.target.value)
}), }),
h.label('enabled'), label('enabled'),
h.input({ type: "checkbox", input({ type: "checkbox",
onchange: ev => prefs.save( space.snapon = !space.snapon ), onchange: ev => prefs.save( space.snapon = !space.snapon ),
[ space.snapon === true ? 'checked' : 'unchecked' ] : 1 [ space.snapon === true ? 'checked' : 'unchecked' ] : 1
}), }),
]); ]);
const set6 = h.div([ const set6 = div([
h.label({ class: "header", _: 'duplicate'}), label({ class: "header", _: 'duplicate'}),
h.label('select'), label('select'),
h.input({ type: "checkbox", input({ type: "checkbox",
onchange: ev => prefs.save( space.dup_sel = !space.dup_sel ), onchange: ev => prefs.save( space.dup_sel = !space.dup_sel ),
[ space.dup_sel === true ? 'checked' : 'unchecked' ] : 1 [ space.dup_sel === true ? 'checked' : 'unchecked' ] : 1
}), }),
h.label('shift'), label('shift'),
h.input({ type: "checkbox", input({ type: "checkbox",
onchange: ev => prefs.save( space.dup_shift = !space.dup_shift ), onchange: ev => prefs.save( space.dup_shift = !space.dup_shift ),
[ space.dup_shift === true ? 'checked' : 'unchecked' ] : 1 [ space.dup_shift === true ? 'checked' : 'unchecked' ] : 1
}), }),
@ -314,18 +316,18 @@ api.settings = function() {
let ot = sketch.open_type; let ot = sketch.open_type;
let srr = () => { api.sketch.list().forEach(s => s.render()) }; let srr = () => { api.sketch.list().forEach(s => s.render()) };
const set7 = h.div([ const set7 = div([
h.label({ class: "header", _: 'open poly'}), label({ class: "header", _: 'open poly'}),
h.label('auto close'), label('auto close'),
h.input({ type: "checkbox", input({ type: "checkbox",
onchange: ev => prefs.save( ((sketch.open_close = !sketch.open_close) || true) && srr() ), onchange: ev => prefs.save( ((sketch.open_close = !sketch.open_close) || true) && srr() ),
[ sketch.open_close === true ? 'checked' : 'unchecked' ] : 1 [ sketch.open_close === true ? 'checked' : 'unchecked' ] : 1
}), }),
h.label('width'), label('width'),
h.input({ type: "text", size: 5, value: parseFloat(sketch.open_width || 1), input({ type: "text", size: 5, value: parseFloat(sketch.open_width || 1),
onchange: ev => prefs.save( (sketch.open_width = parseFloat(ev.target.value)) && srr() ) onchange: ev => prefs.save( (sketch.open_width = parseFloat(ev.target.value)) && srr() )
}), }),
h.label('type'), label('type'),
h.select({ type: "text", value: sketch.open_type, h.select({ type: "text", value: sketch.open_type,
onchange: ev => prefs.save( (sketch.open_type = ev.target.value) && srr() ) onchange: ev => prefs.save( (sketch.open_type = ev.target.value) && srr() )
}, [ }, [
@ -335,7 +337,7 @@ api.settings = function() {
]), ]),
]); ]);
modal.show('settings', h.div({ class: "settings" }, [ modal.show('settings', div({ class: "settings" }, [
set1, set2, set3, set4, set5, set6, set7 set1, set2, set3, set4, set5, set6, set7
] )); ] ));
} }
@ -353,12 +355,12 @@ function facon(name, sub) {
function menu_item(text, fn, short) { function menu_item(text, fn, short) {
if (short) { if (short) {
short = Array.isArray(short) ? short : [ short ]; short = Array.isArray(short) ? short : [ short ];
short = h.div({ class: "short" }, short.map(s => s.startsWith('bi-') ? short = div({ class: "short" }, short.map(s => s.startsWith('bi-') ?
bicon(s) : h.div(s) bicon(s) : div(s)
)); ));
} }
return h.div({ onclick: fn }, [ return div({ onclick: fn }, [
h.div({ _: text, class: "grow" }), div({ _: text, class: "grow" }),
short short
].filter(o => o)); ].filter(o => o));
} }
@ -371,194 +373,195 @@ function ui_build() {
let eye_closed = FontAwesome.icon({ prefix: "fas", iconName: "eye-slash" }).html[0]; let eye_closed = FontAwesome.icon({ prefix: "fas", iconName: "eye-slash" }).html[0];
// top left drop menus // top left drop menus
h.bind($('top-left'), [ bind($('top-left'), [
h.div({ class: "menu" }, [ div({ class: "menu" }, [
h.div('File'), div('File'),
h.div({ class: "menu-items" }, [ div({ class: "menu-items" }, [
h.input({ input({
id: "import", type: "file", class: ["hide"], multiple: true, id: "import", type: "file", class: ["hide"], multiple: true,
onchange(evt) { broker.send.load_files(evt.target.files) } onchange(evt) { broker.send.load_files(evt.target.files) }
}), }),
menu_item('Import', file.import, 'I'), menu_item('Import', file.import, 'I'),
menu_item('Export', file.export, 'X'), menu_item('Export', file.export, 'X'),
h.hr(), hr(),
menu_item('Slicer', api.kirimoto), menu_item('Slicer', api.kirimoto),
h.hr(), hr(),
menu_item('Close', window.close), menu_item('Close', window.close),
]) ])
]), ]),
h.div({ class: "menu sketch-on" }, [ div({ class: "menu sketch-on" }, [
h.div('Edit'), div('Edit'),
h.div({ class: "menu-items" }, [ div({ class: "menu-items" }, [
menu_item('Add Circle', () => { sketch.selected.one?.add.circle() }), menu_item('Add Circle', () => { sketch.selected.one?.add.circle() }),
menu_item('Add Rectangle', () => { sketch.selected.one?.add.rectangle() }), menu_item('Add Rectangle', () => { sketch.selected.one?.add.rectangle() }),
h.hr(), hr(),
menu_item('Add Sketch', add.sketch), menu_item('Add Sketch', add.sketch),
h.hr(), hr(),
menu_item('Delete', api.selection.delete, 'bi-backspace'), menu_item('Delete', api.selection.delete, 'bi-backspace'),
]) ])
]), ]),
h.div({ class: "menu sketch-off" }, [ div({ class: "menu sketch-off" }, [
h.div('Edit'), div('Edit'),
h.div({ class: "menu-items" }, [ div({ class: "menu-items" }, [
menu_item('Add Cylinder', add.cylinder), menu_item('Add Cylinder', add.cylinder),
menu_item('Add Cube', add.cube), menu_item('Add Cube', add.cube),
menu_item('Add Gear', add.gear), menu_item('Add Gear', add.gear),
h.hr(), hr(),
menu_item('Add Sketch', add.sketch), menu_item('Add Sketch', add.sketch),
devel ? menu_item('Add Vertices', add.input) : undefined, devel ? menu_item('Add Vertices', add.input) : undefined,
h.hr(), hr(),
menu_item('Delete', api.selection.delete, 'bi-backspace'), menu_item('Delete', api.selection.delete, 'bi-backspace'),
]) ])
]), ]),
h.div({ class: "menu" }, [ div({ class: "menu" }, [
h.div('View'), div('View'),
h.div({ class: "menu-items" }, [ div({ class: "menu-items" }, [
menu_item('Bounds', () => { selection.boundsBox({ toggle: true }) }, 'B'), menu_item('Bounds', () => { selection.boundsBox({ toggle: true }) }, 'B'),
menu_item('Normals', () => { api.normals() }, 'N'), menu_item('Normals', () => { api.normals() }, 'N'),
menu_item('Wireframes', () => { api.wireframe() }, 'W'), menu_item('Wireframes', () => { api.wireframe() }, 'W'),
h.hr(), hr(),
menu_item('Gridlines', () => { api.grid() }, 'G'), menu_item('Gridlines', () => { api.grid() }, 'G'),
menu_item('Messages', () => { api.log.toggle({ spinner: false }) }, 'L'), menu_item('Messages', () => { api.log.toggle({ spinner: false }) }, 'L'),
h.hr(), hr(),
menu_item('Home', space.view.home, 'H'), menu_item('Home', space.view.home, 'H'),
menu_item('Top', space.view.top, 'T'), menu_item('Top', space.view.top, 'T'),
menu_item('Reset', space.view.reset, 'Z'),
]) ])
]), ]),
h.div({ class: "menu" }, [ div({ class: "menu" }, [
h.div('Mode'), div('Mode'),
h.div({ class: "menu-items" }, [ div({ class: "menu-items" }, [
h.div({ _: 'Sketch', id: "mode-sketch", onclick: mode.sketch }), div({ _: 'Sketch', id: "mode-sketch", onclick: mode.sketch }),
h.hr(), hr(),
h.div({ _: 'Object', id: "mode-object", onclick: mode.object }), div({ _: 'Object', id: "mode-object", onclick: mode.object }),
h.div({ _: 'Tool', id: "mode-tool", onclick: mode.tool }), div({ _: 'Tool', id: "mode-tool", onclick: mode.tool }),
h.hr(), hr(),
h.div({ _: 'Surface', id: "mode-surface", onclick: mode.surface }), div({ _: 'Surface', id: "mode-surface", onclick: mode.surface }),
h.div({ _: 'Face', id: "mode-face", onclick: mode.face }), div({ _: 'Face', id: "mode-face", onclick: mode.face }),
h.div({ _: 'Edge', id: "mode-edge", onclick: mode.edge }), div({ _: 'Edge', id: "mode-edge", onclick: mode.edge }),
]) ])
]), ]),
h.div({ class: "menu sketch-on" }, [ div({ class: "menu sketch-on" }, [
h.div('Object'), div('Object'),
h.div({ class: "menu-items" }, [ div({ class: "menu-items" }, [
menu_item('Group', sketch.arrange.group), menu_item('Group', sketch.arrange.group),
menu_item('Ungroup', sketch.arrange.ungroup), menu_item('Ungroup', sketch.arrange.ungroup),
h.hr(), hr(),
menu_item('Center', sketch.arrange.center), menu_item('Center', sketch.arrange.center),
h.hr(), hr(),
menu_item('Flip Horizontal', sketch.arrange.fliph), menu_item('Flip Horizontal', sketch.arrange.fliph),
menu_item('Flip Vertical', sketch.arrange.flipv), menu_item('Flip Vertical', sketch.arrange.flipv),
h.hr(), hr(),
menu_item('Raise', sketch.arrange.up), menu_item('Raise', sketch.arrange.up),
menu_item('Lower', sketch.arrange.down), menu_item('Lower', sketch.arrange.down),
menu_item('To Top', sketch.arrange.top), menu_item('To Top', sketch.arrange.top),
menu_item('To Bottom', sketch.arrange.bottom), menu_item('To Bottom', sketch.arrange.bottom),
]) ])
]), ]),
h.div({ class: "menu sketch-off" }, [ div({ class: "menu sketch-off" }, [
h.div('Object'), div('Object'),
h.div({ class: "menu-items" }, [ div({ class: "menu-items" }, [
menu_item('Regroup', tool.regroup), menu_item('Regroup', tool.regroup),
menu_item('Isolate', tool.isolate), menu_item('Isolate', tool.isolate),
h.hr(), hr(),
menu_item('Duplicate', tool.duplicate, ['bi-shift','D']), menu_item('Duplicate', tool.duplicate, ['bi-shift','D']),
menu_item('Mirror', tool.mirror, 'M'), menu_item('Mirror', tool.mirror, 'M'),
menu_item('Merge', tool.merge), menu_item('Merge', tool.merge),
menu_item('Split', mesh.split.start, 'S'), menu_item('Split', mesh.split.start, 'S'),
h.hr(), hr(),
menu_item('Union', tool.union), menu_item('Union', tool.union),
menu_item('Subtract', tool.subtract), menu_item('Subtract', tool.subtract),
menu_item('Intersect', tool.intersect), menu_item('Intersect', tool.intersect),
menu_item('Difference', tool.difference), menu_item('Difference', tool.difference),
h.hr(), hr(),
menu_item('Copy Circle', api.pattern.circle), menu_item('Copy Circle', api.pattern.circle),
menu_item('Copy Grid', api.pattern.grid), menu_item('Copy Grid', api.pattern.grid),
]) ])
]), ]),
h.div({ class: "menu sketch-off" }, [ div({ class: "menu sketch-off" }, [
h.div('Faces'), div('Faces'),
h.div({ class: "menu-items" }, [ div({ class: "menu-items" }, [
menu_item('Flip Normals', tool.invert), menu_item('Flip Normals', tool.invert),
menu_item('Analyze', tool.analyze), menu_item('Analyze', tool.analyze),
// h.div({ _: 'Patch', onclick: tool.repair }), // div({ _: 'Patch', onclick: tool.repair }),
// h.div({ _: 'rebuild', onclick: tool.rebuild }), // div({ _: 'rebuild', onclick: tool.rebuild }),
menu_item('Clean', tool.clean), menu_item('Clean', tool.clean),
]) ])
]), ]),
h.div({ class: "menu sketch-on" }, [ div({ class: "menu sketch-on" }, [
h.div('Shape'), div('Shape'),
h.div({ class: "menu-items" }, [ div({ class: "menu-items" }, [
menu_item('Duplicate', tool.duplicate, ['bi-shift','D']), menu_item('Duplicate', tool.duplicate, ['bi-shift','D']),
menu_item('Extrude', sketch.extrude, ['bi-shift','E']), menu_item('Extrude', () => sketch.extrude(), ['bi-shift','E']),
h.hr(), hr(),
menu_item('Union', sketch.boolean.union), menu_item('Union', sketch.boolean.union),
menu_item('Intersect', sketch.boolean.intersect), menu_item('Intersect', sketch.boolean.intersect),
menu_item('Difference', sketch.boolean.difference), menu_item('Difference', sketch.boolean.difference),
h.hr(), hr(),
menu_item('Nest', sketch.boolean.nest), menu_item('Nest', sketch.boolean.nest),
menu_item('Flatten', sketch.boolean.flatten), menu_item('Flatten', sketch.boolean.flatten),
menu_item('Even Odd', sketch.boolean.evenodd), menu_item('Even Odd', sketch.boolean.evenodd),
]) ])
]), ]),
h.div({ class: "menu" }, [ div({ class: "menu" }, [
h.div('Help'), div('Help'),
h.div({ class: "menu-items" }, [ div({ class: "menu-items" }, [
menu_item('About', () => { api.welcome(mesh.version) }), menu_item('About', () => { api.welcome(mesh.version) }),
h.hr(), hr(),
menu_item('Documentation', api.help), menu_item('Documentation', api.help),
menu_item('Official Site', api.gridspace), menu_item('Official Site', api.gridspace),
menu_item('Donate', api.donate), menu_item('Donate', api.donate),
h.hr(), hr(),
menu_item('Versions', api.version), menu_item('Versions', api.version),
]) ])
]), ]),
]); ]);
// add help buttons // add help buttons
h.bind($('top-right'), [ bind($('top-right'), [
h.div({ id: "top-settings", onclick: api.settings }, [ div({ id: "top-settings", onclick: api.settings }, [
h.div({ class: "fas fa-gear" }), div({ class: "fas fa-gear" }),
h.div('Settings') div('Settings')
]), ]),
]); ]);
// modal dialog and page blocker // modal dialog and page blocker
h.bind($('modal_page'), [ bind($('modal_page'), [
h.div({ id: 'modal_frame' }, [ div({ id: 'modal_frame' }, [
h.div({ id: 'modal_title'}, [ div({ id: 'modal_title'}, [
h.div({ id: 'modal_title_text', _: 'title' }), div({ id: 'modal_title_text', _: 'title' }),
h.div({ id: 'modal_title_close', onclick: modal.cancel }, [ div({ id: 'modal_title_close', onclick: modal.cancel }, [
h.i({ class: "far fa-window-close" }) h.i({ class: "far fa-window-close" })
]) ])
]), ]),
h.div({ id: 'modal' }, [ div({ id: 'modal' }, [
h.div({ _: 'this is a modal test' }) div({ _: 'this is a modal test' })
]) ])
]), ]),
h.div({ id: 'spinner', class: 'spin' }) div({ id: 'spinner', class: 'spin' })
]); ]);
// create top level app areas // create top level app areas
let bound = h.bind($('app-body'), [ let bound = bind($('app-body'), [
// display and action areas // display and action areas
h.div({ id: 'tools' }, [ div({ id: 'tools' }, [
h.div({ id: 'sketchtools', class: "tools sketch-on" }), div({ id: 'sketchtools', class: "tools sketch-on" }),
h.div({ id: 'objecttools', class: "tools sketch-off" }), div({ id: 'objecttools', class: "tools sketch-off" }),
h.div({ id: 'logger', onmouseover() { log.show() } }), div({ id: 'logger', onmouseover() { log.show() } }),
]), ]),
h.div({ id: 'grouplist' }), div({ id: 'grouplist' }),
h.div({ id: 'selectlist' }), div({ id: 'selectlist' }),
h.div({ id: 'download', class: "hide" }) div({ id: 'download', class: "hide" })
]); ]);
let { sketchtools, grouplist, selectlist, logger } = bound; let { sketchtools, grouplist, selectlist, logger } = bound;
function tool_item(icon, help, fn) { function tool_item(icon, help, fn) {
return h.div({ onclick: fn, class: "tool" }, [ bicon(icon), h.div([ h.label(help) ]) ]); return div({ onclick: fn, class: "tool" }, [ bicon(icon), div([ label(help) ]) ]);
} }
// bind sketch chiclets // bind sketch chiclets
h.bind(sketchtools, h.div([ bind(sketchtools, div([
tool_item('bi-plus', 'New Sketch', add.sketch), tool_item('bi-plus', 'New Sketch', add.sketch),
tool_item('bi-circle', 'Add Circle', () => { sketch.selected.one?.add.circle() }), tool_item('bi-circle', 'Add Circle', () => { sketch.selected.one?.add.circle() }),
tool_item('bi-square', 'Add Rectangle', () => { sketch.selected.one?.add.rectangle() }), tool_item('bi-square', 'Add Rectangle', () => { sketch.selected.one?.add.rectangle() }),
@ -572,7 +575,7 @@ function ui_build() {
])); ]));
// bind object chiclets // bind object chiclets
h.bind(objecttools, h.div([ bind(objecttools, div([
tool_item('bi-pencil', 'New Sketch', add.sketch), tool_item('bi-pencil', 'New Sketch', add.sketch),
tool_item('bi-box', 'New Cube', add.cube), tool_item('bi-box', 'New Cube', add.cube),
tool_item('bi-database', 'New Cylinder', add.cylinder), tool_item('bi-database', 'New Cylinder', add.cylinder),
@ -584,26 +587,26 @@ function ui_build() {
])); ]));
// create slid in/out logging window // create slid in/out logging window
h.bind(logger, [ h.div({ id: 'logtext' }) ]); bind(logger, [ div({ id: 'logtext' }) ]);
// for group/model/box/area/mesh dashboard grids // for group/model/box/area/mesh dashboard grids
function grid(v1, v2, side = [ "pos", "rot"], top = [ "X", "Y", "Z" ], root) { function grid(v1, v2, side = [ "pos", "rot"], top = [ "X", "Y", "Z" ], root) {
let v = 'val', l = 'lbl'; let v = 'val', l = 'lbl';
let [ X, Y, Z ] = top; let [ X, Y, Z ] = top;
let [ pos, rot ] = side; let [ pos, rot ] = side;
return h.div({ class: "grid"}, [ return div({ class: "grid"}, [
h.div({ _: "" }), div({ _: "" }),
h.div({ _: X, class: "top noselect", id: root ? [ root, l, X ] : und }), div({ _: X, class: "top noselect", id: root ? [ root, l, X ] : und }),
h.div({ _: Y, class: "top noselect", id: root ? [ root, l, Y ] : und }), div({ _: Y, class: "top noselect", id: root ? [ root, l, Y ] : und }),
h.div({ _: Z, class: "top noselect", id: root ? [ root, l, Z ] : und }), div({ _: Z, class: "top noselect", id: root ? [ root, l, Z ] : und }),
h.div({ _: side[0], class: "side noselect" }), div({ _: side[0], class: "side noselect" }),
h.label({ _: v1[0], id: root ? [ root, v, X, pos ] : und }), label({ _: v1[0], id: root ? [ root, v, X, pos ] : und }),
h.label({ _: v1[1], id: root ? [ root, v, Y, pos ] : und }), label({ _: v1[1], id: root ? [ root, v, Y, pos ] : und }),
h.label({ _: v1[2], id: root ? [ root, v, Z, pos ] : und }), label({ _: v1[2], id: root ? [ root, v, Z, pos ] : und }),
h.div({ _: side[1], class: "side noselect" }), div({ _: side[1], class: "side noselect" }),
h.label({ _: v2[0], id: root ? [ root, v, X, rot ] : und }), label({ _: v2[0], id: root ? [ root, v, X, rot ] : und }),
h.label({ _: v2[1], id: root ? [ root, v, Y, rot ] : und }), label({ _: v2[1], id: root ? [ root, v, Y, rot ] : und }),
h.label({ _: v2[2], id: root ? [ root, v, Z, rot ] : und }), label({ _: v2[2], id: root ? [ root, v, Z, rot ] : und }),
]); ]);
} }
@ -612,16 +615,16 @@ function ui_build() {
let v = 'val', l = 'lbl'; let v = 'val', l = 'lbl';
let [ X, Y ] = top; let [ X, Y ] = top;
let [ pos, rot ] = side; let [ pos, rot ] = side;
return h.div({ class: "grid grid2"}, [ return div({ class: "grid grid2"}, [
h.div({ _: "" }), div({ _: "" }),
h.div({ _: X, class: "top noselect", id: root ? [ root, l, X ] : und }), div({ _: X, class: "top noselect", id: root ? [ root, l, X ] : und }),
h.div({ _: Y, class: "top noselect", id: root ? [ root, l, Y ] : und }), div({ _: Y, class: "top noselect", id: root ? [ root, l, Y ] : und }),
h.div({ _: side[0], class: "side noselect" }), div({ _: side[0], class: "side noselect" }),
h.label({ _: v1[0], id: root ? [ root, v, X, pos ] : und }), label({ _: v1[0], id: root ? [ root, v, X, pos ] : und }),
h.label({ _: v1[1], id: root ? [ root, v, Y, pos ] : und }), label({ _: v1[1], id: root ? [ root, v, Y, pos ] : und }),
h.div({ _: side[1], class: "side noselect" }), div({ _: side[1], class: "side noselect" }),
h.label({ _: v2[0], id: root ? [ root, v, X, rot ] : und }), label({ _: v2[0], id: root ? [ root, v, X, rot ] : und }),
h.label({ _: v2[1], id: root ? [ root, v, Y, rot ] : und }), label({ _: v2[1], id: root ? [ root, v, Y, rot ] : und }),
]); ]);
} }
@ -629,13 +632,13 @@ function ui_build() {
function grid1(vals, side = [ "X", "Y"], top = "center", root, post) { function grid1(vals, side = [ "X", "Y"], top = "center", root, post) {
let v = 'val', l = 'lbl'; let v = 'val', l = 'lbl';
let [ X, Y ] = side; let [ X, Y ] = side;
return h.div({ class: "grid grid1"}, [ return div({ class: "grid grid1"}, [
h.div({ _: "" }), div({ _: "" }),
h.div({ _: top, class: "top noselect", id: root ? [ root, l ] : und }), div({ _: top, class: "top noselect", id: root ? [ root, l ] : und }),
h.div({ _: side[0], class: "side noselect" }), div({ _: side[0], class: "side noselect" }),
h.label({ _: vals[0], id: root ? [ root, v, X, post ] : und }), label({ _: vals[0], id: root ? [ root, v, X, post ] : und }),
h.div({ _: side[1], class: "side noselect" }), div({ _: side[1], class: "side noselect" }),
h.label({ _: vals[1], id: root ? [ root, v, Y, post ] : und }), label({ _: vals[1], id: root ? [ root, v, Y, post ] : und }),
]); ]);
} }
@ -649,7 +652,7 @@ function ui_build() {
let selHas = selection.contains; let selHas = selection.contains;
// map groups to divs // map groups to divs
let groups = api.group.list() let groups = api.group.list()
.map(g => h.div([ .map(g => div([
h.button({ _: g.name || `group`, title: g.id, h.button({ _: g.name || `group`, title: g.id,
class: [ "group", selHas(g) ? 'selected' : undefined ], class: [ "group", selHas(g) ? 'selected' : undefined ],
onclick(e) { onclick(e) {
@ -657,7 +660,7 @@ function ui_build() {
selection.toggle(g); selection.toggle(g);
} }
}), }),
h.div({ class: "models"}, div({ class: "models"},
// map models to buttons // map models to buttons
g.models.map(m => [ g.models.map(m => [
h.button({ _: m.file || m.id, h.button({ _: m.file || m.id,
@ -691,11 +694,11 @@ function ui_build() {
])); ]));
let sketches = api.sketch.list(); let sketches = api.sketch.list();
if (sketches.length) { if (sketches.length) {
let sg = h.div([ let sg = div([
h.button({ _: 'sketch', h.button({ _: 'sketch',
class: [ "group" ], class: [ "group" ],
}), }),
sketches.map(sketch => h.div({ class: "models"}, [ sketches.map(sketch => div({ class: "models"}, [
h.button({ _: sketch.file || sketch.id, h.button({ _: sketch.file || sketch.id,
class: [ selHas(sketch) ? 'selected' : undefined ], class: [ selHas(sketch) ? 'selected' : undefined ],
onclick(e) { onclick(e) {
@ -725,7 +728,7 @@ function ui_build() {
]); ]);
groups.push(sg); groups.push(sg);
} }
h.bind(grouplist, groups); bind(grouplist, groups);
} }
// return function bound to field editing clicks // return function bound to field editing clicks
@ -758,8 +761,8 @@ function ui_build() {
onclick(); onclick();
} }
}; };
let { tempval } = api.modal.show(title, h.div({ class: "tempedit" }, [ let { tempval } = api.modal.show(title, div({ class: "tempedit" }, [
h.input({ id: 'tempval', value, size: 10, onkeydown }), input({ id: 'tempval', value, size: 10, onkeydown }),
h.button({ _: 'set', onclick }) h.button({ _: 'set', onclick })
])); ]));
tempval.setSelectionRange(0, tempval.value.length); tempval.setSelectionRange(0, tempval.value.length);
@ -780,8 +783,8 @@ function ui_build() {
data: `${label}_data` data: `${label}_data`
}; };
return [ return [
h.button({ id: map.show, class: "side" }, [ h.div(label)] ), h.button({ id: map.show, class: "side" }, [ div(label)] ),
h.div({ id: map.data }, [ div({ id: map.data }, [
h.button({ id: map.hide, _: label, title }), h.button({ id: map.hide, _: label, title }),
grid grid
]) ])
@ -794,7 +797,7 @@ function ui_build() {
let s_mdl = selection.models(true); let s_mdl = selection.models(true);
if (!(sketch || s_grp.length)) { if (!(sketch || s_grp.length)) {
return h.bind(selectlist, []); return bind(selectlist, []);
} }
// render sketch options // render sketch options
@ -828,8 +831,8 @@ function ui_build() {
} else if (item.type === 'circle') { } else if (item.type === 'circle') {
return [ return [
sblock('circle', '', grid1( sblock('circle', '', grid1(
[ item.points, item.rotation || 0 ], [ item.points || sel_sobj.poly.length, item.rotation || 0 ],
[ "points", "rotation" ], "value", 'circle', 'other' [ "points", "rotation" ], "Value", 'circle', 'other'
)), )),
sblock('circle', '', grid2( sblock('circle', '', grid2(
util.extract(item.center, map), util.extract(item.center, map),
@ -839,7 +842,7 @@ function ui_build() {
]; ];
} }
}); });
let bound = h.bind(selectlist, [ let bound = bind(selectlist, [
...si_vars, ...si_vars,
...sk_vars ...sk_vars
]); ]);
@ -916,36 +919,36 @@ function ui_build() {
let t_vert = s_mdl.map(m => m.vertices).reduce((a,v) => a+v); let t_vert = s_mdl.map(m => m.vertices).reduce((a,v) => a+v);
let t_face = s_mdl.map(m => m.faces).reduce((a,v) => a+v); let t_face = s_mdl.map(m => m.faces).reduce((a,v) => a+v);
let h_msh = [h.div([ let h_msh = [div([
h.button({ _: `mesh` }), h.button({ _: `mesh` }),
h.div({ class: ["grid","grid1"]}, [ div({ class: ["grid","grid1"]}, [
h.div({ _: "" }), div({ _: "" }),
h.div({ _: "count", class: "top" }), div({ _: "count", class: "top" }),
h.div({ _: "vertex", class: "side" }), div({ _: "vertex", class: "side" }),
h.label({ _: util.comma(t_vert) }), label({ _: util.comma(t_vert) }),
h.div({ _: "face", class: "side" }), div({ _: "face", class: "side" }),
h.label({ _: util.comma(t_face) }), label({ _: util.comma(t_face) }),
]) ])
])]; ])];
let m_viz = !s_mdl.map(m => !m.visible()).reduce((v,b) => v || b); let m_viz = !s_mdl.map(m => !m.visible()).reduce((v,b) => v || b);
let h_ops = [h.div([ let h_ops = [div([
h.button({ _: `ops` }), h.button({ _: `ops` }),
h.div({ class: ["grid","grid0"]}, [ div({ class: ["grid","grid0"]}, [
h.div({ class: [ 'square' ], div({ class: [ 'square' ],
onclick(e) { onclick(e) {
api.selection.visible(!m_viz); api.selection.visible(!m_viz);
update_selector(); update_selector();
} }
}, [ h.raw(m_viz ? eye_open : eye_closed) ]), }, [ h.raw(m_viz ? eye_open : eye_closed) ]),
h.div({ class: [ 'square' ], div({ class: [ 'square' ],
onclick(e) { api.selection.delete() } onclick(e) { api.selection.delete() }
}, [ h.raw(trash) ]) }, [ h.raw(trash) ])
]) ])
])]; ])];
// bind elements to selectlist div // bind elements to selectlist div
let bound = h.bind(selectlist, [ let bound = bind(selectlist, [
...h_grp, ...h_grp,
...h_ara, ...h_ara,
...h_msh, ...h_msh,

View file

@ -149,7 +149,7 @@ mesh.sketch = class MeshSketch extends mesh.object {
}, },
items() { items() {
return sketch.selection.mesh_items().map(i => c.sketch_item.item); return sketch.selection.mesh_items().map(c => c.sketch_item);
}, },
count() { count() {
@ -232,10 +232,11 @@ mesh.sketch = class MeshSketch extends mesh.object {
}); });
}, },
item(item, opt = {}) { item(item) {
item.group = item.group || mesh.util.uuid(); item.group = item.group || mesh.util.uuid();
sketch.items.push(item); sketch.items.push(item);
sketch.render_defer(); sketch.render_defer();
api.selection.update_defer();
return item; return item;
} }
} }
@ -277,31 +278,30 @@ mesh.sketch = class MeshSketch extends mesh.object {
}, },
nest() { nest() {
let items = sketch.selection.mesh_items(); let items = sketch.selection.items();
if (items.length < 2) return log('operation requires at least 2 items'); if (items.length < 2) return log('operation requires at least 2 items');
let polys = items.map(i => i.sketch_item.poly).clone(true); let polys = items.map(si => si.poly.annotate({group:si.group}).clone(true, ['group']));
let union = POLYS.nest(POLYS.flatten(polys, [], true)); let union = POLYS.nest(POLYS.flatten(polys, [], true));
sketch.selection.delete(); sketch.selection.delete();
for (let poly of union) { for (let poly of union) {
sketch.add.polygon({ poly }); sketch.add.polygon({ poly, group:poly.group });
} }
}, },
evenodd() { evenodd() {
let items = sketch.selection.mesh_items(); let items = sketch.selection.items();
// if (items.length < 2) return log('operation requires at least 2 items'); let polys = items.map(si => si.poly.annotate({group:si.group}));
let polys = items.map(i => i.sketch_item.poly); let even = POLYS.nest(POLYS.flatten(polys.clone(true,['group']), [], true));
let even = POLYS.nest(POLYS.flatten(polys.clone(true), [], true)); let odd = POLYS.nest(POLYS.flatten(even.clone(true,['group']), [], true).filter(p => p.depth > 0));
let odd = POLYS.nest(POLYS.flatten(even.clone(true), [], true).filter(p => p.depth > 0));
sketch.selection.delete(); sketch.selection.delete();
for (let poly of [...even, ...odd]) { for (let poly of [...even, ...odd]) {
sketch.add.polygon({ poly }); sketch.add.polygon({ poly, group:poly.group });
} }
}, },
flatten() { flatten() {
let items = sketch.selection.mesh_items(); let items = sketch.selection.items();
let polys = items.map(i => i.sketch_item.poly).clone(true); let polys = items.map(si => si.poly.annotate({group:si.group}).clone(true, ['group']));
let flat = POLYS.flatten(polys, [], true); let flat = POLYS.flatten(polys, [], true);
sketch.selection.delete(); sketch.selection.delete();
for (let poly of flat) { for (let poly of flat) {
@ -566,6 +566,10 @@ class SketchItem {
return "sketch_item"; return "sketch_item";
} }
get group() {
return this.item.group;
}
get selected() { get selected() {
return this.item.selected; return this.item.selected;
} }

View file

@ -699,6 +699,7 @@ button:hover {
border-radius: 3px; border-radius: 3px;
padding: 3px; padding: 3px;
display: grid; display: grid;
flex-grow: 1;
gap: 2px 2px; gap: 2px 2px;
grid: min-content 1fr 1fr / min-content 1fr 1fr 1fr; grid: min-content 1fr 1fr / min-content 1fr 1fr 1fr;
} }