add sketch item dialogs and editing, sketch extrusion dialog and options

This commit is contained in:
Stewart Allen 2024-07-25 23:30:58 -04:00
commit 47e47df0a5
6 changed files with 279 additions and 53 deletions

View file

@ -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;

View file

@ -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();

View file

@ -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() {

View file

@ -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
});
}

View file

@ -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));
// 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 = this.outs = new THREE.Line(lge, material.wireline);
let out = new THREE.Line(lge, material.wireline);
out.renderOrder = -1;
out.sketch_line = this;
out.position.z += bump + (order * bump);
outs.push(out);
}
}
}

View file

@ -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;
}