diff --git a/src/geo/polygon.js b/src/geo/polygon.js index 935e0827..b7e64a4d 100644 --- a/src/geo/polygon.js +++ b/src/geo/polygon.js @@ -850,6 +850,18 @@ class Polygon { return this; } + rotate(degrees) { + let rad = degrees * DEG2RAD; + if (rad) + this.points = this.points.map(p => { + let [ x, y ] = base.util.rotate(p.x, p.y, rad); + p.x = x; + p.y = y; + return p; + }); + return this; + } + /** * hint fill angle hinting from longest segment */ @@ -2232,10 +2244,11 @@ class Polygon { } // outside wall - obj.appendAll(this.ribbonZ(z_side_top, z_side_bottom)); + let rib = z - chamfer_top - chamfer_bottom; + obj.appendAll(this.ribbonZ(rib, z_side_bottom)); for (let inner of this.inner || []) { // inside wall(s) - obj.appendAll(inner.ribbonZ(z_side_top, z_side_bottom, true)); + obj.appendAll(inner.ribbonZ(rib, z_side_bottom, true)); } return obj; diff --git a/src/main/mesh.js b/src/main/mesh.js index 89b8bd6b..31a635d0 100644 --- a/src/main/mesh.js +++ b/src/main/mesh.js @@ -235,6 +235,7 @@ function space_init(data) { return shiftKey ? api.tool.rebuild() : api.tool.repair(); case 'KeyE': if (api.mode.is([ api.modes.sketch ])) { + estop(evt); return api.sketch.extrude(); } return api.tool.clean(); diff --git a/src/mesh/api.js b/src/mesh/api.js index 35e71b70..ba06893a 100644 --- a/src/mesh/api.js +++ b/src/mesh/api.js @@ -442,10 +442,35 @@ let sketch = { return sketches; }, - extrude() { - selection.sketches(true).forEach(sketch => { - sketch.extrude(...arguments); + extrude(opt = {}) { + if (!selection.sketch()) { + return log('select a sketch to extrude'); + } + api.modal.dialog({ + title: "gear generator", + body: [ h.div({ class: "addgear" }, [ + h.label('height'), + h.input({ value: opt.height || 10, size: 5, id: "_height" }), + h.hr(), + h.code("optional"), + h.hr(), + h.label('chamfer top'), + h.input({ value: opt.chamfer_top || 0, size: 5, id: "_chamtop" }), + h.label('chamfer bottom'), + h.input({ value: opt.chamfer_bottom || 0, size: 5, id: "_chambot" }), + h.button({ _: "create", onclick() { + const { _height, _chamtop, _chambot } = api.modal.bound; + selection.sketch()?.extrude({ + ...opt, + height: parseFloat(_height.value), + chamfer_top: parseFloat(_chamtop.value), + chamfer_bottom: parseFloat(_chambot.value), + }); + api.modal.hide(); + } }) + ]) ] }); + api.modal.bound._height.focus(); }, selection: { @@ -508,12 +533,35 @@ let add = { /** add sketch components */ - circle() { - let sel = selection.sketches(); - if (!sel.length) { + circle(opt = {}) { + let sketch = selection.sketch(); + if (!sketch) { return log('select a sketch'); } - return sel[0].add_circle(...arguments); + api.modal.dialog({ + title: "gear generator", + body: [ h.div({ class: "addgear" }, [ + h.label('radius in mm'), + h.input({ value: opt.radius || 10, size: 5, id: "_radius" }), + h.hr(), + h.code("optional"), + h.hr(), + h.label('point spacing'), + h.input({ value: opt.spacing || 0, size: 5, id: "_spacing" }), + h.label('point count'), + h.input({ value: opt.points || 0, size: 5, id: "_points" }), + h.button({ _: "create", onclick() { + const { _radius, _points, _spacing } = api.modal.bound; + sketch.add_circle({ + radius: parseFloat(_radius.value), + points: parseInt(_points.value), + spacing: parseFloat(_spacing.value), + }); + api.modal.hide(); + } }) + ]) ] + }); + api.modal.bound._radius.focus(); }, rectangle() { diff --git a/src/mesh/build.js b/src/mesh/build.js index 828cb2e9..7bb32aab 100644 --- a/src/mesh/build.js +++ b/src/mesh/build.js @@ -445,14 +445,14 @@ function ui_build() { h.button({ _: 'grid', onclick() { api.pattern.grid() } }), ]) ]), - h.div({ class: "sketch-on" }, [ - h.div({ _: "pattern", class: "head" }), - h.div({ class: "back"}), - h.div({ class: "pop"}, [ - h.button({ _: 'circular', onclick() { sketch.pattern.circle() } }), - h.button({ _: 'grid', onclick() { sketch.pattern.grid() } }), - ]) - ]), + // h.div({ class: "sketch-on" }, [ + // h.div({ _: "pattern", class: "head" }), + // h.div({ class: "back"}), + // h.div({ class: "pop"}, [ + // h.button({ _: 'circular', onclick() { sketch.pattern.circle() } }), + // h.button({ _: 'grid', onclick() { sketch.pattern.grid() } }), + // ]) + // ]), h.div({ class: "sketch-on" }, [ h.div({ _: "extrude", class: "head" }), h.div({ class: "back"}), @@ -518,6 +518,38 @@ function ui_build() { ]); } + // for dashboard sketch grids + function grid2(v1, v2, side = [ "pos", "rot"], top = [ "X", "Y" ], root) { + let v = 'val', l = 'lbl'; + let [ X, Y ] = top; + let [ pos, rot ] = side; + return h.div({ class: "grid grid2"}, [ + h.div({ _: "" }), + h.div({ _: X, class: "top noselect", id: root ? [ root, l, X ] : und }), + h.div({ _: Y, class: "top noselect", id: root ? [ root, l, Y ] : und }), + h.div({ _: side[0], class: "side noselect" }), + h.label({ _: v1[0], id: root ? [ root, v, X, pos ] : und }), + h.label({ _: v1[1], id: root ? [ root, v, Y, pos ] : und }), + h.div({ _: side[1], class: "side noselect" }), + h.label({ _: v2[0], id: root ? [ root, v, X, rot ] : und }), + h.label({ _: v2[1], id: root ? [ root, v, Y, rot ] : und }), + ]); + } + + // for other dashboard grids + function grid1(vals, side = [ "X", "Y"], top = "center", root, post) { + let v = 'val', l = 'lbl'; + let [ X, Y ] = side; + return h.div({ class: "grid grid1"}, [ + h.div({ _: "" }), + h.div({ _: top, class: "top noselect", id: root ? [ root, l ] : und }), + h.div({ _: side[0], class: "side noselect" }), + h.label({ _: vals[0], id: root ? [ root, v, X, post ] : und }), + h.div({ _: side[1], class: "side noselect" }), + h.label({ _: vals[1], id: root ? [ root, v, Y, post ] : und }), + ]); + } + function update_all() { update_selector(); update_selection(); @@ -619,13 +651,17 @@ function ui_build() { if (modal.info.cancelled) { return; } + if (opt.sketch) { + defer_selection(); + return set(tempval, value); + } for (let g of api.selection.groups()) { set(g, tempval, value); if (floor && opt.floor !== false) { g.floor(mesh.group); } - defer_selection(); // update ui } + defer_selection(); }; let onkeydown = (ev) => { if (ev.key === 'Enter') { @@ -642,14 +678,9 @@ function ui_build() { } } - // update model information dashboard (bottom) + // editable model/sketch information dashboard function update_selection() { let map = { fixed: 2 }; - let s_grp = selection.groups(true); - let s_mdl = selection.models(true); - if (s_mdl.length === 0) { - return h.bind(selectlist, []); - } // toggle-able stat block generator let sdata = {}; @@ -668,6 +699,108 @@ function ui_build() { ]; } + // possible selections + let sketch = selection.sketch(); + let s_grp = selection.groups(true); + let s_mdl = selection.models(true); + + if (!(sketch || s_grp.length)) { + return h.bind(selectlist, []); + } + + // render sketch options + if (sketch) { + let bounds = { + size: sketch.scale, + center: sketch.center + }; + let sel = sketch.selection.mesh_items(); + let sk_vars = sblock('sketch', sketch.file || sketch.id, grid( + util.extract(bounds.center, map), + util.extract(bounds.size, map), + [ "center", "size" ], und, 'sketch' + )); + let sel_item; + let si_vars = sel.slice(0,1).map(item => item.sketch_item.item).map(item => { + sel_item = item; + if (item.type === 'rectangle') { + return sblock('rectangle', '', grid2( + util.extract(item.center, map), + [ item.width.toFixed(2), item.height.toFixed(2) ], + [ "center", "size" ], und, 'item' + )); + } else if (item.type === 'circle') { + return [ + sblock('circle', '', grid1( + [ item.points, item.rotation || 0 ], + [ "points", "rotation" ], "value", 'circle', 'other' + )), + sblock('circle', '', grid2( + util.extract(item.center, map), + [ (item.radius*2).toFixed(2), (item.radius*2).toFixed(2) ], + [ "center", "size" ], und, 'item' + )), + ]; + } else if (item.type === 'polygon') { + return sblock('poly', '', grid1( + util.extract(item.center, map), + und, und, 'item', 'center' + )); + } + }); + let bound = h.bind(selectlist, [ + ...si_vars, + ...sk_vars + ]); + for (let axis of [ 'x', 'y', 'z' ]) { + let el = bound[`sketch_val_${axis.toUpperCase()}_center`]; + el.classList.add('editable'); + el.onclick = field_edit(`${axis} center`, (nval, oval) => { + sketch.center[axis] = nval; + sketch.render(); + }, { sketch: true }); + let el2 = bound[`sketch_val_${axis.toUpperCase()}_size`]; + el2.classList.add('editable'); + el2.onclick = field_edit(`${axis} size`, (nval, oval) => { + sketch.scale[axis] = nval; + sketch.render(); + }, { sketch: true }); + } + if (sel_item) + for (let axis of [ 'x', 'y' ]) { + let el = bound[`item_val_${axis.toUpperCase()}_center`]; + if (!el) continue; + el.classList.add('editable'); + el.onclick = field_edit(`${axis} center`, (nval, oval) => { + sel_item.center[axis] = nval; + sketch.render(); + }, { sketch: true }); + + let el2 = bound[`item_val_${axis.toUpperCase()}_size`]; + if (!el2) continue; + el2.classList.add('editable'); + el2.onclick = field_edit(`${axis} size`, (nval, oval) => { + if (sel_item.type === 'rectangle') { + sel_item[{ x:'width', y:'height' }[axis]] = nval; + } else if (sel_item.type === 'circle') { + sel_item.radius = nval / 2; + } + sketch.render(); + }, { sketch: true }); + + let lab = { x: 'points', y: 'rotation' }[axis]; + let el3 = bound[`circle_val_${lab}_other`]; + if (!el3) continue; + el3.classList.add('editable'); + el3.onclick = field_edit(`${axis} size`, (nval, oval) => { + sel_item[lab] = nval; + sketch.render(); + }, { sketch: true }); + } + + return; + } + // map selection to divs let g_pos = util.average(s_grp.map(g => g.object.position)); let g_rot = util.average(s_grp.map(g => g.object.rotation)); @@ -678,8 +811,8 @@ function ui_build() { und, und, 'group' )); - let m_pos = util.average(s_mdl.map(m => m.object.position)); - let m_rot = util.average(s_mdl.map(m => m.object.rotation)); + // let m_pos = util.average(s_mdl.map(m => m.object.position)); + // let m_rot = util.average(s_mdl.map(m => m.object.rotation)); let m_id = s_mdl.map(m => m.id).join(' '); let bounds = util.bounds(s_mdl); @@ -693,7 +826,7 @@ function ui_build() { let t_face = s_mdl.map(m => m.faces).reduce((a,v) => a+v); let h_msh = [h.div([ h.button({ _: `mesh` }), - h.div({ class: ["grid","grid2"]}, [ + h.div({ class: ["grid","grid1"]}, [ h.div({ _: "" }), h.div({ _: "count", class: "top" }), h.div({ _: "vertex", class: "side" }), @@ -706,7 +839,7 @@ function ui_build() { let m_viz = !s_mdl.map(m => !m.visible()).reduce((v,b) => v || b); let h_ops = [h.div([ h.button({ _: `ops` }), - h.div({ class: ["grid","grid1"]}, [ + h.div({ class: ["grid","grid0"]}, [ h.div({ class: [ 'square' ], onclick(e) { api.selection.visible(!m_viz); @@ -852,6 +985,7 @@ function ui_build() { selection_scale: defer_selection, selection_rotate: defer_selection, selection_qrotate: defer_selection, + sketch_selections: defer_selection }); } diff --git a/src/mesh/sketch.js b/src/mesh/sketch.js index beab06d9..8c25535a 100644 --- a/src/mesh/sketch.js +++ b/src/mesh/sketch.js @@ -18,7 +18,7 @@ const { BufferGeometry, BufferAttribute } = THREE; const { MeshBasicMaterial, LineBasicMaterial, LineSegments, DoubleSide } = THREE; const { PlaneGeometry, EdgesGeometry, SphereGeometry, Vector3, Mesh, Group } = THREE; const { base, mesh, moto } = root; -const { space } = moto; +const { space, broker } = moto; const { api, util } = mesh; const { newPolygon } = base; @@ -144,6 +144,7 @@ mesh.sketch = class MeshSketch extends mesh.object { all() { sketch.items.forEach(i => i.selected = true); sketch.render(); + broker.publish('sketch_selections'); return sketch.items.length; }, @@ -153,6 +154,7 @@ mesh.sketch = class MeshSketch extends mesh.object { if (sel.length) { sel.forEach(s => s.selected = false); sketch.render(); + broker.publish('sketch_selections'); } return sel.length; }, @@ -163,6 +165,7 @@ mesh.sketch = class MeshSketch extends mesh.object { if (sel.length) { sketch.items = sketch.items.filter(i => !i.selected); sketch.render(); + broker.publish('sketch_selections'); } return sel.length; } @@ -174,6 +177,7 @@ mesh.sketch = class MeshSketch extends mesh.object { return { union() { let items = sketch.selection.mesh_items(); + if (items.length < 2) return log('operation requires at least 2 items'); let polys = items.map(i => i.sketch_item.poly); let union = POLYS.union(polys, 0, true); sketch.selection.delete(); @@ -184,6 +188,7 @@ mesh.sketch = class MeshSketch extends mesh.object { intersect() { let items = sketch.selection.mesh_items(); + if (items.length !== 2) return log('operation requires 2 items'); let polys = items.map(i => i.sketch_item.poly); let trim = POLYS.trimTo([polys[0]], [polys[1]]); sketch.selection.delete(); @@ -194,6 +199,7 @@ mesh.sketch = class MeshSketch extends mesh.object { difference() { let items = sketch.selection.mesh_items(); + if (items.length !== 2) return log('operation requires 2 items'); let polys = items.map(i => i.sketch_item.poly); let diff1 = POLYS.diff([polys[0]], [polys[1]]); let diff2 = POLYS.diff([polys[1]], [polys[0]]); @@ -205,6 +211,7 @@ mesh.sketch = class MeshSketch extends mesh.object { group() { let items = sketch.selection.mesh_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 union = POLYS.nest(POLYS.flatten(polys, [], true)); sketch.selection.delete(); @@ -320,22 +327,31 @@ mesh.sketch = class MeshSketch extends mesh.object { add_circle(opt = {}) { log(this.file || this.id, '| add circle'); - Object.assign(opt, { center: {x:0, y:0, z:0}, radius:5 }, opt); - this.items.push({ type: "circle", ...opt }); + this.items.push({ + type: "circle", + ...Object.assign({}, { center: {x:0, y:0, z:0}, radius:5 }, opt) + }); this.render(); } add_rectangle(opt = {}) { log(this.file || this.id, '| add rectangle'); - Object.assign(opt, { center: {x:0, y:0, z:0}, width:15, height:10 }, opt); - this.items.push({ type: "rectangle", ...opt }); + this.items.push({ + type: "rectangle", + ...Object.assign({}, { center: {x:0, y:0, z:0}, width:15, height:10 }, opt) + }); this.render(); } add_polygon(opt = {}) { // log(this.file || this.id, '| add polygon'); - Object.assign(opt, { center: {x:0, y:0, z:0} }, opt); - this.items.push({ type: "polygon", ...opt, ...opt.poly.toObject() }); + let poly = opt.poly; + delete opt.poly; + this.items.push({ + type: "polygon", + ...Object.assign({}, { center: {x:0, y:0, z:0} }, opt), + ...poly.toObject() + }); this.render(); } @@ -347,19 +363,24 @@ mesh.sketch = class MeshSketch extends mesh.object { // mapy items into polys into meshes to add to group for (let si of this.items.map((i,o) => new SketchItem(this, i, o))) { group.add(si.mesh); - group.add(si.outs); + group.add(...si.outs); } this.update(); } extrude(opt = {}) { - let { z, selection } = opt; + console.log({ opt }); + let { selection, height, chamfer, chamfer_top, chamfer_bottom } = opt; let models = []; let items = this.group.children .filter(c => c.sketch_item) .filter(c => !selection || c.sketch_item.selected); for (let item of items) { - let vert = item.sketch_item.extrude(z || 10); + let vert = item.sketch_item.extrude(height, { + chamfer, + chamfer_top, + chamfer_bottom + }); let nmdl = new mesh.model({ file: "item", mesh: vert.toFloat32() }); models.push(nmdl); } @@ -393,21 +414,22 @@ class SketchItem { this.item.selected = !this.item.selected; this.update(); this.sketch.render(); + broker.publish('sketch_selections'); } - extrude(opt = {}) { - return this.poly.extrude(opt); + extrude(z, opt = {}) { + return this.poly.extrude(z, opt); } update() { let bump = 0.0025; let { item, sketch, order } = this; let { material } = mesh; - let { type, center, width, height, radius, spacing, poly, selected } = item; + let { type, center, width, height, radius, points, spacing, poly, selected } = item; if (type === 'circle') { let circumference = 2 * Math.PI * radius; - let points = Math.floor(circumference / (spacing || 1)); - poly = newPolygon().centerCircle(center, radius, points).annotate({ item } ); + points = points || Math.floor(circumference / (spacing || 1)); + poly = newPolygon().centerCircle(center, radius, points).annotate({ item } ).rotate(item.rotation || 0); } else if (type === 'rectangle') { poly = newPolygon().centerRectangle(center, width, height).annotate({ item }); } else if (type === 'polygon') { @@ -430,14 +452,18 @@ class SketchItem { meh.sketch_item = this; // bump z to avoid z order conflict and ensure item ray intersect priority meh.position.z += bump + (order * bump); - // create poly outline - let lpt = poly.points.map(p => new Vector3(p.x, p.y, p.z)); - lpt.push(lpt[0]); - let lge = new BufferGeometry().setFromPoints(lpt); - let out = this.outs = new THREE.Line(lge, material.wireline); - out.renderOrder = -1; - out.sketch_line = this; - out.position.z += bump + (order * bump); + // create poly outline(s) + let outs = this.outs = []; + for (let p of [poly, ...(poly.inner || [])]) { + let lpt = p.points.map(p => new Vector3(p.x, p.y, p.z)); + lpt.push(lpt[0]); + let lge = new BufferGeometry().setFromPoints(lpt); + let out = new THREE.Line(lge, material.wireline); + out.renderOrder = -1; + out.sketch_line = this; + out.position.z += bump + (order * bump); + outs.push(out); + } } } diff --git a/web/mesh/index.css b/web/mesh/index.css index 8e2ade60..18162462 100644 --- a/web/mesh/index.css +++ b/web/mesh/index.css @@ -756,7 +756,7 @@ button.selected:hover { grid: min-content 1fr 1fr / min-content 1fr 1fr 1fr; } -#selectlist .grid1 { +#selectlist .grid0 { display: flex; flex-grow: 1; flex-direction: column; @@ -765,10 +765,14 @@ button.selected:hover { gap: 5px; } -#selectlist .grid2 { +#selectlist .grid1 { grid: min-content 1fr / min-content 1fr; } +#selectlist .grid2 { + grid: min-content 1fr 1fr / min-content 1fr 1fr; +} + .grid .ungrouped { background-color: rgba(128,128,128,0.5) !important; }