grid-apps-cmms/src/mesh/api.js

1721 lines
51 KiB
JavaScript
Raw Normal View History

/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { broker } from '../moto/broker.js';
import { history } from './history.js';
import { client as motoClient } from '../moto/client.js';
import { space as motoSpace } from '../moto/space.js';
import { base } from '../geo/base.js';
import { handles } from './handles.js';
import { util as meshUtil } from './util.js';
import { edges as meshEdges } from './edges.js';
import { group as meshGroup } from './group.js';
import { model as meshModel } from './model.js';
import { sketch as meshSketch } from './sketch.js';
2025-10-24 23:56:06 -04:00
import { script } from './script.js';
import { newPolygon } from '../geo/polygon.js';
import { THREE } from '../ext/three.js';
import { $, $C, h } from '../moto/webui.js';
2022-01-12 11:46:10 -05:00
2022-02-20 00:15:59 -05:00
const { Matrix4, Vector3 } = THREE;
const worker = motoClient.fn;
const groups = [];
2024-07-23 22:42:20 -04:00
const sketches = [];
2021-12-31 14:12:42 -05:00
let selected = [];
let tools = [];
2022-02-20 00:15:59 -05:00
const selection = {
count() {
return selected.length;
},
// @returns {MeshObject[]} or all groups if not strict and no selection
2024-08-24 23:24:03 -04:00
list(strict = false) {
return selected.length || strict ? selected.slice() : groups.slice();
2021-12-31 14:12:42 -05:00
},
2022-01-12 23:02:08 -05:00
// return selected groups + groups from selected models
2024-08-24 23:24:03 -04:00
groups(strict = true) {
let all = selection.list(strict);
let grp = all.filter(s => s instanceof meshGroup);
let mdl = all.filter(s => s instanceof meshModel);
for (let m of mdl) {
grp.addOnce(m.group);
}
return grp;
},
2022-01-12 23:02:08 -05:00
// return selected models + models from selected groups
2024-08-24 23:24:03 -04:00
models(strict = true) {
let all = selection.list(strict);
let grp = all.filter(s => s instanceof meshGroup);
let mdl = all.filter(s => s instanceof meshModel);
for (let g of grp) {
for (let m of g.models) {
mdl.addOnce(m);
}
}
return mdl;
},
2024-07-25 14:05:44 -04:00
sketches() {
return selection.list(true).filter(s => s.type === 'sketch');
},
2024-08-24 23:24:03 -04:00
sketch() {
return selection.sketches()[0];
},
2024-07-25 14:05:44 -04:00
sketch_items() {
return selection.list(true).filter(s => s.type === 'sketch_item')
2024-07-24 19:30:44 -04:00
},
2022-01-02 16:30:05 -05:00
contains(object) {
return selected.contains(object);
},
// @param group {MeshObject[]}
set(objects, toolist) {
2022-04-21 10:14:12 -04:00
// flatten groups into model lists when present
selected = objects.map(o => o.models ? o.models : o).flat();
tools = toolist || [];
meshUtil.defer(selection.update);
2025-10-25 22:13:54 -04:00
return api.selection;
2021-12-31 14:12:42 -05:00
},
// @param group {MeshObject}
2022-04-21 10:14:12 -04:00
add(object, tool = mode.is([ modes.tool ])) {
// eject incompatible types from selection
switch (object.type) {
case 'sketch':
2024-07-24 16:57:42 -04:00
mode.sketch();
tools.length = 0;
2024-07-24 16:57:42 -04:00
selected.length = 0;
meshEdges?.end();
break;
case 'group':
case 'model':
selected = selected.filter(s => s.type !== 'sketch');
break;
}
2022-04-21 10:14:12 -04:00
// when group added, add group models instead
if (object.models) {
for (let m of object.models) {
2022-04-21 10:14:12 -04:00
selected.addOnce(m);
if (tool) {
tools.addOnce(m);
}
}
} else {
2022-04-21 10:14:12 -04:00
selected.addOnce(object);
if (tool) {
tools.addOnce(object);
}
}
meshUtil.defer(selection.update);
2021-12-31 14:12:42 -05:00
},
// @param group {MeshObject}
2021-12-31 14:12:42 -05:00
remove(object) {
2022-04-21 10:14:12 -04:00
if (object.models) {
for (let m of object.models) {
selected.remove(m);
tools.remove(m);
}
} else {
selected.remove(object);
tools.remove(object);
}
meshUtil.defer(selection.update);
2021-12-31 14:12:42 -05:00
},
2022-04-19 23:03:58 -04:00
// remove all
delete() {
2024-07-27 20:01:13 -04:00
if (sketch.selected.delete()) {
return;
}
2024-07-25 15:52:17 -04:00
for (let s of selection.list()) {
2022-04-19 23:03:58 -04:00
selection.remove(s);
tools.remove(s);
2022-04-19 23:03:58 -04:00
s.remove();
}
},
// @param group {MeshObject}
2022-04-21 10:14:12 -04:00
toggle(object, tool = mode.is([ modes.tool ])) {
if (object.models) {
for (let m of object.models) {
if (selected.contains(m)) {
return selection.remove(object);
}
}
return selection.add(object, tool);
}
if (selected.contains(object)) {
selection.remove(object);
} else {
2022-04-21 10:14:12 -04:00
selection.add(object, tool);
}
},
2021-12-31 14:12:42 -05:00
clear() {
2024-07-27 20:01:13 -04:00
if (sketch.selected.clear()) {
2024-07-25 15:52:17 -04:00
return;
}
for (let s of selection.list()) {
selection.remove(s);
tools.remove(s);
}
2022-01-13 21:17:35 -05:00
for (let m of api.model.list()) {
m.clearSelections();
2022-01-13 21:17:35 -05:00
}
return true;
},
2022-01-12 00:28:33 -05:00
visible() {
for (let m of selection.models()) {
m.visible(...arguments);
}
meshUtil.defer(selection.update);
2022-01-12 00:28:33 -05:00
},
// update selection and wireframe for all objects
update() {
2024-07-25 14:05:44 -04:00
// de-select all
for (let group of groups) {
group.select(false);
2022-01-14 14:14:13 -05:00
group.normals(prefs.map.space.norm || false);
2022-11-15 22:15:14 -05:00
group.wireframe(prefs.map.space.wire || false, {
opacity: prefs.map.wireframe.opacity,
edges: prefs.map.space.wire_edge
});
}
2024-07-23 22:42:20 -04:00
for (let sketch of sketches) {
sketch.select(false);
}
api.updateFog();
2022-01-02 16:30:05 -05:00
// prevent selection of model and its group
let mgsel = selected.filter(s => s instanceof meshModel).map(m => m.group);
selected = selected.filter(sel => !mgsel.contains(sel)).filter(v => v);
// highlight selected
for (let object of selected) {
object.select(true, tools.contains(object));
}
// update saved selection id list
prefs.save( prefs.map.space.select = selected.map(s => s.id) );
prefs.save( prefs.map.space.tools = tools.map(s => s.id) );
// force repaint in case of idle
motoSpace.update();
return selection;
2021-12-31 14:12:42 -05:00
},
update_defer() {
meshUtil.defer(selection.update);
return selection;
},
2024-07-24 19:30:44 -04:00
drag(opt = {}) {
if (opt.start) {
broker.publish('drag_start', selected);
} else if (opt.end) {
broker.publish('drag_end', selected);
} else {
broker.publish('drag', opt);
}
2024-07-26 16:28:52 -04:00
selected.forEach(s => s.drag(opt));
2024-07-24 19:30:44 -04:00
return selection;
},
2021-12-31 14:12:42 -05:00
move(dx = 0, dy = 0, dz = 0) {
let set = [...selection.groups(), ...selection.sketches()];
for (let s of set) {
2021-12-31 14:12:42 -05:00
s.move(dx, dy, dz);
2021-12-30 22:59:46 -05:00
}
broker.publish('move', { set, dx, dy, dz });
2021-12-31 14:12:42 -05:00
return selection;
2021-12-30 22:59:46 -05:00
},
2024-08-10 21:59:02 -04:00
move_models(dx = 0, dy = 0, dz = 0) {
for (let s of selection.models()) {
s.move(dx, dy, dz);
}
broker.publish('move', { set: selection.models(), dx, dy, dz });
2024-08-10 21:59:02 -04:00
return selection;
},
2021-12-31 14:12:42 -05:00
rotate(dx = 0, dy = 0, dz = 0) {
for (let s of selection.groups()) {
2021-12-31 16:12:37 -05:00
s.rotate(dx, dy, dz);
2021-12-31 14:12:42 -05:00
}
broker.publish('rotate', { set: selection.models(), dx, dy, dz });
2021-12-31 14:12:42 -05:00
return selection;
},
2022-01-01 10:44:06 -05:00
qrotate(q) {
for (let s of selection.groups()) {
2022-01-01 10:44:06 -05:00
s.qrotate(q);
}
broker.publish('qrotate', { set: selection.groups(), q });
2022-01-01 10:44:06 -05:00
return selection;
},
2021-12-31 14:12:42 -05:00
scale(dx = 0, dy = 0, dz = 0) {
for (let s of selection.groups()) {
s.scale(dx, dy, dz);
2021-12-31 14:12:42 -05:00
}
broker.publish('scale', { set: selection.groups(), dx, dy, dz });
2021-12-31 14:12:42 -05:00
return selection;
},
floor() {
for (let s of selection.groups()) {
2024-08-09 21:53:33 -04:00
s.floor();
2021-12-31 14:12:42 -05:00
}
2024-09-02 13:57:47 -04:00
call.selection_update();
2021-12-31 14:12:42 -05:00
return selection;
},
centerXY() {
2024-07-26 00:35:36 -04:00
for (let s of [...selection.groups(), ...selection.sketches()]) {
2024-08-09 21:53:33 -04:00
s.centerXY();
2021-12-31 14:12:42 -05:00
}
2024-09-02 13:57:47 -04:00
call.selection_update();
2021-12-31 14:12:42 -05:00
return selection;
},
boundsBox() {
handles.toggleBounds();
return selection;
},
2021-12-31 14:12:42 -05:00
home() {
return selection.centerXY().floor();
2021-12-31 16:59:38 -05:00
},
focus() {
selection.count() && api.focus(selection.list());
2021-12-31 16:59:38 -05:00
},
bounds() {
return meshUtil.bounds(selected.map(s => s.object));
2024-07-20 18:47:47 -04:00
},
show() {
for (let m of selection.models()) {
m.visible(true);
}
},
hide() {
for (let m of selection.models()) {
m.visible(false);
}
2021-12-31 14:12:42 -05:00
}
};
2024-07-20 02:24:40 -04:00
const pattern = {
/**
* @param {int} count number of entities
* @param {float[]} center [x,y,z]
*/
circle() {
async function doit(count = 3, center = [0,0], zdelta = 0) {
2024-07-20 02:24:40 -04:00
api.modal.hide();
let models = selection.models();
let [ cx, cy ] = center;
2024-07-20 02:24:40 -04:00
for (let model of models) {
let pos = model.bounds.mid;
2024-07-20 02:24:40 -04:00
let stepr = (Math.PI * 2) / count;
let modnu = [ model ];
for (let i=1; i<count; i++) {
2024-08-09 23:58:36 -04:00
let clone = model.duplicate().centerTo({ x:cx, y:cy, z:pos.z });
clone.rotate(0, 0, stepr * i);
clone.reCenter();
2024-07-20 02:24:40 -04:00
modnu.push(clone);
}
await api.tool.regroup(modnu);
}
}
api.modal.dialog({
title: "circle pattern",
2024-08-24 23:24:03 -04:00
body: [ h.div({ class: "additem" }, [
h.label('total count'),
h.input({ value: 3, size: 5, id: "_count" }),
h.label('center x,y'),
h.input({ value: "0,0", size: 5, id: "_center" }),
// h.label('z delta'),
// h.input({ value: "0", size: 5, id: "_zdelta" }),
h.button({ _: "create", onclick() {
let { _count, _center, _zdelta } = api.modal.bound;
doit(
parseInt(_count.value),
_center.value.split(',').map(v => parseFloat(v)),
// parseFloat(_zdelta.value)
);
} })
]) ]
});
api.modal.bound._count.focus();
2024-07-20 02:24:40 -04:00
},
grid() {
2024-07-20 14:53:47 -04:00
async function doit(x = 3, y = 3, xs = 10, ys = 10) {
api.modal.hide();
let models = selection.models();
for (let model of models) {
let modnu = [ model ];
for (let i=0; i<x; i++) {
for (let j=0; j<y; j++) {
if (i === 0 && j === 0) continue;
2024-08-09 23:58:36 -04:00
let clone = await model.duplicate().reCenter();
clone.move(i * xs, -j * ys, 0);
2024-07-20 14:53:47 -04:00
modnu.push(clone);
}
}
await api.tool.regroup(modnu);
}
}
api.modal.dialog({
title: "grid pattern",
2024-08-24 23:24:03 -04:00
body: [ h.div({ class: "additem" }, [
h.label('x count'),
h.input({ value: 3, size: 5, id: "_x" }),
h.label('y count'),
h.input({ value: 3, size: 5, id: "_y" }),
h.label('x spacing'),
h.input({ value: 20, size: 5, id: "_xs" }),
h.label('y spacing'),
h.input({ value: 20, size: 5, id: "_ys" }),
h.button({ _: "create", onclick() {
doit(
parseInt(api.modal.bound._x.value) || 3,
parseInt(api.modal.bound._y.value) || 3,
parseFloat(api.modal.bound._xs.value) || 3,
parseFloat(api.modal.bound._ys.value) || 3,
);
} })
]) ]
});
api.modal.bound._x.focus();
2024-07-20 02:24:40 -04:00
}
};
2022-02-20 00:15:59 -05:00
const group = {
2021-12-31 14:12:42 -05:00
// @returns {MeshGroup[]}
list() {
return groups.slice();
},
// @param group {MeshModel[]}
2022-01-11 21:04:24 -05:00
new(models, id, name) {
return group.add(new meshGroup(models, id, name));
2021-12-31 14:12:42 -05:00
},
// @param group {MeshGroup}
2022-01-01 23:48:36 -05:00
add(group) {
2021-12-31 14:12:42 -05:00
groups.addOnce(group);
motoSpace.world.add(group.object);
meshUtil.defer(selection.update);
motoSpace.update();
2021-12-31 14:12:42 -05:00
return group;
},
// @param group {MeshGroup}
2022-01-01 23:48:36 -05:00
remove(group) {
2021-12-31 14:12:42 -05:00
groups.remove(group);
motoSpace.world.remove(group.object);
motoSpace.update();
2021-12-31 14:12:42 -05:00
}
};
2022-01-01 22:39:49 -05:00
let model = {
// @returns {MeshModel[]}
list() {
return groups.map(g => g.models).flat();
}
};
2024-07-23 22:42:20 -04:00
let sketch = {
add(sk) {
sketches.addOnce(sk);
motoSpace.world.add(sk.object);
meshUtil.defer(selection.update);
2024-07-23 22:42:20 -04:00
return sk;
},
remove(sk) {
sketches.remove(sk);
motoSpace.world.remove(sk.object);
motoSpace.update();
2024-07-23 22:42:20 -04:00
},
list() {
return sketches;
2024-07-25 00:05:24 -04:00
},
extrude(opt = {}) {
let sketch = selection.sketch();
if (!sketch) {
return log('select a sketch to extrude');
}
api.modal.dialog({
title: "extrude",
2024-08-24 23:24:03 -04:00
body: [ h.div({ class: "additem" }, [
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;
sketch.extrude({
selection: sketch.selection.count() > 0,
...opt,
height: parseFloat(_height.value),
chamfer_top: parseFloat(_chamtop.value),
chamfer_bottom: parseFloat(_chambot.value),
});
api.modal.hide();
} })
]) ]
2024-07-25 00:05:24 -04:00
});
api.modal.bound._height.focus();
2024-07-25 15:52:17 -04:00
},
// related to items selections in a selected sketch
arrange: {
group() {
2024-07-27 20:01:13 -04:00
sketch.selection.ifcan((sketch, items) => {
sketch.arrange.group(items);
2024-07-27 20:01:13 -04:00
}, 2);
},
ungroup() {
sketch.selection.ifcan((sketch, items) => {
sketch.arrange.ungroup(items);
2024-07-27 20:01:13 -04:00
});
},
center() {
2024-07-27 20:01:13 -04:00
sketch.selection.bounded_op((sketch, bounds, recs) => {
let center = bounds.center(0);
for (let rec of recs) {
2024-07-30 16:56:34 -04:00
let { item } = rec;
let poly = newPolygon().fromObject(item);
poly.move({ x: -center.x, y: -center.y, z: 0 });
2024-07-30 16:56:34 -04:00
Object.assign(rec.item, poly.toObject());
}
sketch.render();
2024-07-30 16:56:34 -04:00
selection.centerXY();
});
},
fliph() {
2024-07-27 20:01:13 -04:00
sketch.selection.ifcan((sketch, items) => {
items.filter(i => i.type === 'polygon').forEach(item => {
Object.assign(item, newPolygon().fromObject(item).flip('x').toObject());
});
sketch.render();
});
},
flipv() {
2024-07-27 20:01:13 -04:00
sketch.selection.ifcan((sketch, items) => {
items.filter(i => i.type === 'polygon').forEach(item => {
Object.assign(item, newPolygon().fromObject(item).flip('y').toObject());
});
sketch.render();
});
},
2024-07-31 09:43:06 -04:00
rotate() {
function doit(sketch, items, angle) {
items.filter(i => i.type === 'polygon').forEach(item => {
Object.assign(item, newPolygon().fromObject(item).rotate(-angle).toObject());
});
sketch.render();
}
sketch.selection.ifcan((sketch, items) => {
api.modal.dialog({
title: "rotate",
2024-08-24 23:24:03 -04:00
body: [ h.div({ class: "additem" }, [
2024-07-31 09:43:06 -04:00
h.label('angle'),
h.input({ value: 90, size: 5, id: "_angle" }),
h.button({ _: "rotate", onclick() {
const { _angle } = api.modal.bound;
doit(sketch, items, parseFloat(_angle.value));
api.modal.hide();
} })
]) ]
});
api.modal.bound._angle.focus();
});
},
down() {
sketch.selection.ifcan((sketch, items) => {
items.forEach(item => sketch.arrange.down(item));
sketch.render();
});
},
up() {
sketch.selection.ifcan((sketch, items) => {
items.forEach(item => sketch.arrange.up(item));
sketch.render();
});
},
top() {
sketch.selection.ifcan((sketch, items) => {
items.forEach(item => sketch.arrange.top(item));
sketch.render();
});
},
bottom() {
sketch.selection.ifcan((sketch, items) => {
items.forEach(item => sketch.arrange.bottom(item));
sketch.render();
});
},
},
2024-07-27 20:01:13 -04:00
// related to a selected sketch
selected: {
2024-07-25 15:52:17 -04:00
get one() {
return selection.sketch();
},
list() {
return selection.sketches();
},
clear() {
let list = selection.list(true);
return list[0]?.type === 'sketch' && list[0].selection.clear();
},
delete() {
let list = selection.list(true);
return list[0]?.type === 'sketch' && list[0].selection.delete();
2024-07-27 20:01:13 -04:00
},
},
// related to items selections in a selected sketch
selection: {
ifcan(fn, count = 1) {
let one = sketch.selected.one;
let items = one?.items.filter(i => i.selected);
if (items?.length >= count) {
return fn(one, items) || true;
} else {
return false;
}
},
bounded_op(fn) {
return sketch.selection.ifcan((sketch, items) => {
let bounds;
let recs = [];
items.filter(i => i.type === 'polygon').forEach(item => {
let poly = newPolygon().fromObject(item);
let bnds = poly.bounds;
bounds = (bounds ? bounds.merge(bnds) : bnds);
recs.push({ item, poly });
});
return (bounds && (fn(sketch, bounds, recs) || true)) || false;
}) || false;
},
duplicate(opt = {}) {
return sketch.selection.bounded_op((sketch, bounds, recs) => {
let { shift, select } = opt;
let x = bounds.width(), y = 0, z = 0;
for (let rec of recs) {
let { item, poly } = rec;
let item2 = structuredClone(item);
item.selected = !select;
item2.group += 'D';
shift && Object.assign(item2, poly.move({ x, y, z }).toObject());
sketch.add.item(item2, { select });
2024-07-27 20:01:13 -04:00
}
});
},
2024-07-25 15:52:17 -04:00
},
// related to items selections in a selected sketch
2024-07-25 15:52:17 -04:00
boolean: {
2024-07-26 00:06:17 -04:00
nest() {
2024-07-27 20:01:13 -04:00
sketch.selected.one?.boolean.nest();
2024-07-25 15:52:17 -04:00
},
2024-07-26 00:06:17 -04:00
flatten() {
2024-07-27 20:01:13 -04:00
sketch.selected.one?.boolean.flatten();
2024-07-25 15:52:17 -04:00
},
evenodd() {
2024-07-27 20:01:13 -04:00
sketch.selected.one?.boolean.evenodd();
},
2024-07-25 15:52:17 -04:00
union() {
2024-07-27 20:01:13 -04:00
sketch.selected.one?.boolean.union();
2024-07-25 15:52:17 -04:00
},
intersect() {
2024-07-27 20:01:13 -04:00
sketch.selected.one?.boolean.intersect();
2024-07-25 15:52:17 -04:00
},
difference() {
2024-07-27 20:01:13 -04:00
sketch.selected.one?.boolean.difference();
2024-07-25 15:52:17 -04:00
}
},
// related to items selections in a selected sketch
2024-07-25 15:52:17 -04:00
pattern: {
circle() {
2024-07-27 20:01:13 -04:00
sketch.selected.one?.pattern.circle();
2024-07-25 15:52:17 -04:00
},
grid() {
2024-07-27 20:01:13 -04:00
sketch.selected.one?.pattern.grid();
2024-07-25 15:52:17 -04:00
}
2024-07-23 22:42:20 -04:00
}
};
2022-04-19 16:51:55 -04:00
let add = {
2024-07-24 12:29:55 -04:00
sketch(opt) {
2024-08-15 14:38:52 -04:00
mode.sketch();
let nu = new meshSketch(opt);
2024-08-15 13:46:12 -04:00
selection.set([ sketch.add(nu) ]);
return nu;
2024-07-23 22:42:20 -04:00
},
2024-07-24 19:30:44 -04:00
/** add sketch components */
2024-07-31 09:26:44 -04:00
circle() {
let sketch = selection.sketch();
if (!sketch) {
2024-07-24 19:30:44 -04:00
return log('select a sketch');
}
api.modal.dialog({
title: "add circle",
2024-08-24 23:24:03 -04:00
body: [ h.div({ class: "additem" }, [
h.label('radius in mm'),
2024-07-31 09:26:44 -04:00
h.input({ value: 10, size: 5, id: "_radius" }),
h.hr(),
h.code("optional"),
h.hr(),
h.label('point spacing'),
2024-07-31 09:26:44 -04:00
h.input({ value: 0, size: 5, id: "_spacing" }),
h.label('point count'),
2024-07-31 09:26:44 -04:00
h.input({ value: 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();
2024-07-24 19:30:44 -04:00
},
rectangle() {
2024-07-31 09:26:44 -04:00
let sketch = selection.sketch();
if (!sketch) {
2024-07-24 19:30:44 -04:00
return log('select a sketch');
}
2024-07-31 09:26:44 -04:00
api.modal.dialog({
title: "add rectangle",
2024-08-24 23:24:03 -04:00
body: [ h.div({ class: "additem" }, [
2024-07-31 09:26:44 -04:00
h.label('width'),
h.input({ value: 20, size: 5, id: "_width" }),
h.label('height'),
h.input({ value: 20, size: 5, id: "_height" }),
h.button({ _: "create", onclick() {
const { _width, _height } = api.modal.bound;
sketch.add.rectangle({
width: parseFloat(_width.value),
height: parseFloat(_height.value),
});
api.modal.hide();
} })
]) ]
});
api.modal.bound._width.focus();
2024-07-24 19:30:44 -04:00
},
/** add models */
input() {
api.modal.dialog({
title: "vertices",
body: [ h.div({ class: "addact" }, [
h.textarea({ value: '', cols:30, rows:4, id: "genvrt" }),
h.button({ _: "create", onclick() {
const vert = (api.modal.bound.genvrt.value)
.split(',').map(v => parseFloat(v)).toFloat32();
const nmdl = new meshModel({ file: "input", mesh: vert });
const ngrp = api.group.new([ nmdl ]);
api.modal.hide();
} })
]) ]
});
api.modal.bound.genvrt.focus();
},
2025-10-25 18:25:21 -04:00
plane() {
const [ x,y ] = [ ...arguments ];
const box = new THREE.PlaneGeometry(1,1).toNonIndexed();
const vert = box.attributes.position.array;
const nmdl = new meshModel({ file: "plane", mesh: vert });
nmdl._loft = function(plane) {
function uv(v) {
let m = new Set();
let g = [...v].group(3).filter(a => {
let k = a.map(v => v.round(5)).join('-');
if (m.has(k)) return false;
m.add(k); return true;
});
return [ g[0], g[1], g[3], g[2] ];
};
let s0 = uv(vert);
let s1 = uv(plane.attributes.position.array);
let v = [];
for (let i=0; i<4; i++) {
let p0 = [ s0[i], s0[(i+1)%4] ];
let p1 = [ s1[i], s1[(i+1)%4] ];
v.push(...p1[1]);
v.push(...p0[1]);
v.push(...p0[0]);
v.push(...p1[0]);
v.push(...p1[1]);
v.push(...p0[0]);
}
let pa = [...plane.attributes.position.array];
v.push(...pa.group(3).reverse().flat());
v.push(...vert);
const nmdl = new meshModel({ file: "loft", mesh: v.toFloat32() });
const ngrp = api.group.new([ nmdl ]);
selection.set([ nmdl ]);
return ngrp;
};
const ngrp = api.group.new([ nmdl ]);
ngrp.scale(x ?? 10, y ?? 10, 1).floor();
selection.set([ nmdl ]);
return ngrp;
},
2022-04-19 16:51:55 -04:00
cube() {
const [ x,y,z ] = [ ...arguments ].map(v => parseFloat(v) || undefined);
2022-04-19 16:51:55 -04:00
const box = new THREE.BoxGeometry(1,1,1).toNonIndexed();
const vert = box.attributes.position.array;
const nmdl = new meshModel({ file: "box", mesh: vert });
const ngrp = api.group.new([ nmdl ]);
2025-10-24 23:56:06 -04:00
ngrp.scale(x ?? 10, y ?? 10, z ?? 10).floor();
selection.set([ nmdl ]);
2024-07-24 19:30:44 -04:00
return ngrp;
2022-04-19 16:51:55 -04:00
},
cylinder() {
function gencyl(opt = {}) {
let { diameter, height, facets, chamfer, bore } = opt;
2022-04-19 16:51:55 -04:00
api.modal.hide();
if (diameter && height) {
const center = { x:0, y:0, z:0 };
2024-07-24 19:30:44 -04:00
log(`add cylinder faces=${facets} height=${height} diameter=${diameter}`);
facets = Math.max(3, facets || diameter * 3)
const cyl = newPolygon().centerCircle(center, diameter/2, facets);
if (bore && bore < diameter) {
cyl.addInner(newPolygon().centerCircle(center, bore/2, facets));
}
const vert = cyl.extrude(height, { chamfer }).toFloat32();
const nmdl = new meshModel({ file: "cylinder", mesh: vert });
const ngrp = api.group.new([ nmdl ]);
selection.set([ nmdl ]);
2022-04-19 16:51:55 -04:00
ngrp.floor();
}
}
api.modal.dialog({
title: "cylinder",
2024-08-24 23:24:03 -04:00
body: [ h.div({ class: "additem" }, [
h.label('diameter'),
h.input({ value: 20, size: 5, id: "_diameter" }),
h.label('height'),
h.input({ value: 10, size: 5, id: "_height" }),
h.hr(),
h.label('bore'),
h.input({ value: 0, size: 5, id: "_bore" }),
h.label('facets'),
h.input({ value: 0, size: 5, id: "_facets" }),
h.label('chamfer'),
h.input({ value: 0, size: 5, id: "_chamfer" }),
h.button({ _: "create", onclick() {
const { _diameter, _facets, _height, _chamfer, _bore } = api.modal.bound;
gencyl({
diameter: parseFloat(_diameter.value),
height: parseFloat(_height.value),
facets: parseInt(_facets.value),
bore: parseFloat(_bore.value),
chamfer: parseFloat(_chamfer.value),
});
} })
]) ]
});
api.modal.bound._diameter.focus();
2024-07-18 10:53:27 -06:00
},
gear() {
function gengear(bound) {
2024-07-19 18:05:41 -04:00
const { _teeth, _module, _angle, _twist, _shaft, _offset, _height, _chamfer } = bound;
2024-07-18 10:53:27 -06:00
api.modal.hide();
let params;
worker.model_gen_gear(params = {
teeth: parseInt(_teeth.value),
module: parseFloat(_module.value),
angle: parseFloat(_angle.value),
twist: parseFloat(_twist.value),
shaft: parseFloat(_shaft.value),
offset: parseFloat(_offset.value),
height: parseFloat(_height.value),
2024-07-19 18:05:41 -04:00
chamfer: parseFloat(_chamfer.value),
2024-07-18 10:53:27 -06:00
}).then(gear => {
const nmdl = new meshModel({ file: "gear", mesh: gear.toFloat32() });
2024-07-18 10:53:27 -06:00
const ngrp = group.new([ nmdl ]);
selection.set([ nmdl ]);
2024-07-18 10:53:27 -06:00
ngrp.floor();
Object.assign(last, params);
api.prefs.save();
2024-07-18 10:53:27 -06:00
});
}
let last = api.prefs.map.gear = (api.prefs.map.gear || {});
2024-07-18 10:53:27 -06:00
api.modal.dialog({
title: "gear generator",
2024-08-24 23:24:03 -04:00
body: [ h.div({ class: "additem" }, [
2024-07-18 10:53:27 -06:00
h.label('number of teeth'),
h.input({ value: last.teeth || 20, size: 5, id: "_teeth" }),
h.label('shaft diameter'),
h.input({ value: last.shaft || 5, size: 5, id: "_shaft" }),
h.label('z height'),
h.input({ value: last.height || 15, size: 5, id: "_height" }),
h.label({ _:'chamfer', title:"negative values apply chamfer to the bottom only" }),
h.input({ value: last.chamfer || 0, size: 5, id: "_chamfer" }),
h.hr(),
h.code("all other settings must"),
h.code("match for gears to mesh"),
h.hr(),
2024-07-18 10:53:27 -06:00
h.label('module (diam / teeth)'),
h.input({ value: last.module || 3, size: 5, id: "_module" }),
2024-07-18 10:53:27 -06:00
h.label('pressure angle'),
h.input({ value: last.angle || 20, size: 5, id: "_angle" }),
h.label('twist angle (helix)'),
h.input({ value: last.twist || 0, size: 5, id: "_twist" }),
h.label('tooth offset (play)'),
h.input({ value: last.offset || 0.1, size: 5, id: "_offset" }),
h.button({ _: "create", onclick() { gengear(api.modal.bound) } })
2024-07-18 10:53:27 -06:00
]) ]
});
api.modal.bound._teeth.focus();
2024-08-24 23:24:03 -04:00
},
threads() {
2024-08-25 12:44:15 -04:00
function genthreads(bound) {
const { _height, _radius, _turns, _depth, _steps, _taper } = bound;
api.modal.hide();
let params;
worker.model_gen_threads(params = {
height: parseFloat(_height.value),
radius: parseFloat(_radius.value),
turns: parseFloat(_turns.value),
depth: parseFloat(_depth.value),
steps: parseFloat(_steps.value),
taper: _taper.checked ? true : false,
}).then(verts => {
const nmdl = new meshModel({ file: "threads", mesh: verts.toFloat32() });
2024-08-25 12:44:15 -04:00
const ngrp = group.new([ nmdl ]);
selection.set([ nmdl ]);
ngrp.floor();
Object.assign(last, params);
api.prefs.save();
});
}
let last = api.prefs.map.threads = (api.prefs.map.threads || {});
api.modal.dialog({
2024-08-29 14:12:02 -04:00
title: "threads generator",
2024-08-25 12:44:15 -04:00
body: [ h.div({ class: "additem" }, [
h.label('height'),
h.input({ value: last.height || 25, size: 5, id: "_height" }),
h.label('radius'),
h.input({ value: last.radius || 10, size: 5, id: "_radius" }),
h.label('turns'),
h.input({ value: last.turns || 10, size: 5, id: "_turns" }),
h.label('depth'),
h.input({ value: last.depth || 1, size: 5, id: "_depth" }),
h.label('steps'),
h.input({ value: last.steps || 100, size: 5, id: "_steps" }),
h.label('taper'),
h.input({ id: "_taper", type: "checkbox" }),
h.button({ _: "create", onclick() { genthreads(api.modal.bound) } })
]) ]
2024-08-24 23:24:03 -04:00
});
2024-08-25 12:44:15 -04:00
api.modal.bound._height.focus();
if (last.taper) api.modal.bound._taper.checked = true;
2022-04-19 16:51:55 -04:00
}
};
2022-01-06 20:22:50 -05:00
let file = {
import() {
// binding created in mesh.build
$('import').click();
// allow importing the same file twice
setTimeout(() => { $('import').value = ''; }, 100);
2022-01-06 20:22:50 -05:00
},
export() {
2022-01-08 21:00:19 -05:00
let recs = selection.models().map(m => { return {
id: m.id, matrix: m.matrix, file: m.file
} });
2022-01-08 22:06:50 -05:00
if (recs.length === 0) {
2024-07-24 19:30:44 -04:00
return log(`no models to export`);
2022-01-08 22:06:50 -05:00
}
function doit(ext = 'obj') {
2024-07-24 19:30:44 -04:00
log(`exporting ${recs.length} model(s)`);
2022-01-09 14:43:20 -05:00
let file = api.modal.bound.filename.value || "export.obj";
if (file.toLowerCase().indexOf(`.${ext}`) < 0) {
file = `${file}.${ext}`;
2022-01-08 22:06:50 -05:00
}
2022-01-09 14:43:20 -05:00
worker.file_export({
recs, format: ext
2022-01-09 14:43:20 -05:00
}).then(data => {
if (data.length) {
meshUtil.download(data, file);
2022-01-09 14:43:20 -05:00
}
}).finally( api.modal.hide );
}
api.modal.dialog({
title: `export ${recs.length} model(s)`,
body: [ h.div({ class: "export" }, [
h.div([
h.div("filename"),
h.input({ id: "filename", value: "mesh_export" })
]),
h.div([
h.button({ _: "download OBJ", onclick() { doit('obj') } }),
2025-10-25 23:37:40 -04:00
h.button({ _: "download STL", onclick() { doit('stl') } }),
h.button({ _: "download STEP", onclick() { doit('step') } }),
2022-01-09 14:43:20 -05:00
])
]) ]
2022-01-08 21:00:19 -05:00
});
2022-01-06 20:22:50 -05:00
},
};
2022-01-12 23:02:08 -05:00
// return selection if models are invalid. can happen when called from
// even bound to a button which delivers a PointEvent argument
function fallback(models, strict) {
return Array.isArray(models) ? models : selection.models(strict);
2022-01-12 23:02:08 -05:00
}
2022-02-20 00:15:59 -05:00
const tool = {
2022-01-11 16:29:54 -05:00
merge(models) {
2022-01-12 23:02:08 -05:00
models = fallback(models);
2022-10-24 11:43:43 -04:00
if (models.length < 2) {
2024-07-24 19:30:44 -04:00
return log('nothing to merge');
}
2024-07-24 19:30:44 -04:00
log(`merging ${models.length} models`).pin();
2025-10-25 22:13:54 -04:00
return worker.model_merge(models.map(m => {
2022-01-11 16:29:54 -05:00
return { id: m.id, matrix: m.matrix }
}))
.then(data => {
let group = api.group.new([new meshModel({
2022-01-11 16:29:54 -05:00
file: `merged`,
mesh: data
})]).promote();
2024-08-10 16:50:12 -04:00
api.selection.visible(false);
2022-01-11 16:29:54 -05:00
api.selection.set([group]);
2024-07-24 19:30:44 -04:00
log('merge complete').unpin();
2025-10-25 22:13:54 -04:00
return group;
2022-01-11 16:29:54 -05:00
});
},
2022-01-30 21:56:53 -05:00
union(models) {
models = fallback(models);
2022-10-24 11:43:43 -04:00
if (models.length < 2) {
2024-07-24 19:30:44 -04:00
return log('nothing to union');
2022-10-24 11:43:43 -04:00
}
2024-07-24 19:30:44 -04:00
log(`union ${models.length} models`).pin();
2024-08-10 13:24:01 -04:00
worker
.model_union(models.map(m => m.id))
.then(data => {
let group = api.group.new([new meshModel({
2024-08-10 13:24:01 -04:00
file: `union`,
mesh: data
})]).promote();
api.selection.delete();
2024-08-10 13:24:01 -04:00
api.selection.set([ group ]);
})
.catch(error => {
log(`union error: ${error}`);
})
.finally(() => {
log('union complete').unpin();
});
2022-01-30 21:56:53 -05:00
},
difference(models) {
models = fallback(models);
if (models.length < 2) {
2024-07-24 19:30:44 -04:00
return log('nothing to diff');
}
2024-07-24 19:30:44 -04:00
log(`diff ${models.length} models`).pin();
2024-08-10 13:24:01 -04:00
worker.model_difference(models.map(m => m.id))
.then(data => {
let group = api.group.new([new meshModel({
file: `diff`,
mesh: data
})]).promote();
api.selection.delete();
api.selection.set([group]);
})
.catch(error => {
2024-07-24 19:30:44 -04:00
log(`diff error: ${error}`);
})
.finally(() => {
2024-07-24 19:30:44 -04:00
log('diff complete').unpin();
});
},
2022-04-20 19:35:57 -04:00
intersect(models) {
models = fallback(models);
2022-10-24 11:43:43 -04:00
if (models.length < 2) {
2024-07-24 19:30:44 -04:00
return log('nothing to intersect');
2022-10-24 11:43:43 -04:00
}
2024-07-24 19:30:44 -04:00
log(`intersect ${models.length} models`).pin();
2024-08-10 13:24:01 -04:00
worker.model_intersect(models.map(m => m.id))
2022-04-20 19:35:57 -04:00
.then(data => {
let group = api.group.new([new meshModel({
2022-04-20 19:35:57 -04:00
file: `intersect`,
mesh: data
})]).promote();
api.selection.delete();
2022-04-20 19:35:57 -04:00
api.selection.set([group]);
})
.catch(error => {
2024-07-24 19:30:44 -04:00
log(`intersect error: ${error}`);
})
.finally(() => {
2024-07-24 19:30:44 -04:00
log('intersect complete').unpin();
2022-04-20 19:35:57 -04:00
});
},
subtract(models) {
models = fallback(models);
2022-10-24 11:43:43 -04:00
if (models.length < 2) {
2024-07-24 19:30:44 -04:00
return log('nothing to subtract');
2022-10-24 11:43:43 -04:00
}
2024-07-24 19:30:44 -04:00
log(`subtract ${models.length} models`).pin();
2025-10-25 22:13:54 -04:00
return worker.model_subtract(models.map(m => {
2024-08-10 13:24:01 -04:00
return { id: m.id, tool: m.tool() }
}))
.then(data => {
let group = api.group.new([new meshModel({
2022-04-20 19:35:57 -04:00
file: `subtract`,
2022-04-20 17:28:07 -04:00
mesh: data
})]).promote();
api.selection.delete();
api.selection.set([group]);
2025-10-25 22:13:54 -04:00
return group;
})
.catch(error => {
2024-07-24 19:30:44 -04:00
log(`subtract error: ${error}`);
})
.finally(() => {
2024-07-24 19:30:44 -04:00
log('subtract complete').unpin();
});
},
2022-01-13 17:01:35 -05:00
duplicate() {
let { dup_shift, dup_sel } = api.prefs.map.space;
2024-07-27 20:01:13 -04:00
if (sketch.selection.duplicate({ select: dup_sel, shift: dup_shift })) {
return;
}
let models = selection.models();
if (dup_sel) {
selection.clear();
}
let nm = [];
for (let m of models) {
nm.push(m.duplicate({ select: dup_sel, shift: dup_shift }));
2022-01-13 17:01:35 -05:00
}
2025-10-25 22:13:54 -04:00
return tool.regroup(nm);
2022-01-13 17:01:35 -05:00
},
mirror() {
for (let m of selection.models()) {
m.mirror();
}
},
2022-01-14 14:30:07 -05:00
invert() {
2024-08-24 23:48:55 -04:00
for (let m of selection.models(false)) {
2022-01-14 14:30:07 -05:00
m.invert(api.mode.get());
}
},
triangulate() {
2024-08-24 23:48:55 -04:00
for (let m of selection.models(false)) {
m.triangulateSelections();
}
},
2024-08-15 13:46:12 -04:00
toSketch() {
for (let m of api.model.list()) {
2024-08-15 13:46:12 -04:00
m.selectionToSketch();
}
},
2024-07-23 22:42:20 -04:00
rename(models, type = 'model') {
2022-01-20 17:40:10 -05:00
models = fallback(models, true);
let model = models[0];
if (!model) {
return;
}
if (models.length > 1) {
2024-07-24 19:30:44 -04:00
return log('rename requires a single selection');
2022-01-20 17:40:10 -05:00
}
let onclick = onkeydown = (ev) => {
let el = $('tempedit');
if (!el || (ev.code && ev.code !== 'Enter')) {
2022-01-20 17:40:10 -05:00
return;
}
model.rename( el.value.trim() );
meshUtil.defer(selection.update);
2022-01-20 17:40:10 -05:00
api.modal.hide();
};
2024-07-23 22:42:20 -04:00
let { tempedit } = api.modal.show(`rename ${type}`, h.div({ class: "rename"}, [
2022-01-20 17:40:10 -05:00
h.input({ id: "tempedit", value: model.file, onkeydown }),
h.button({ _: 'ok', onclick })
]));
tempedit.setSelectionRange(0,1000);
tempedit.focus();
},
2022-01-20 19:21:26 -05:00
regroup(models) {
models = fallback(models, true);
2024-08-09 21:53:33 -04:00
if (models.length) {
log(`regrouping ${models.length} model(s)`);
let group = api.group.new(models.map(m => m.ungroup()));
let bounds = group.bounds;
models.forEach(m => m.centerTo(bounds.mid));
return group;
2022-01-20 19:21:26 -05:00
}
},
analyze(models, opt = { compound: true }) {
2022-01-12 23:02:08 -05:00
models = fallback(models);
2024-07-24 19:30:44 -04:00
log('analyzing mesh(es)').pin();
2022-01-11 21:04:24 -05:00
let promises = [];
2022-01-12 11:46:10 -05:00
let mcore = new Matrix4().makeRotationX(Math.PI / 2);
for (let m of models) {
let p = worker.model_analyze({ id: m.id, opt }).then(data => {
2022-01-11 21:04:24 -05:00
let { areas, edges } = data.mapped;
let nm = areas.map(area => new meshModel({
2022-01-11 21:04:24 -05:00
file: (area.length/3).toString(),
mesh: area.toFloat32()
2022-01-13 21:17:35 -05:00
})).map( nm => nm.applyMatrix4(mcore.clone().multiply(m.mesh.matrixWorld)) );
2022-01-30 23:17:54 -05:00
if (nm.length) {
api.group.new(nm, undefined, "patch");
2022-01-30 23:17:54 -05:00
}
2022-01-11 14:41:49 -05:00
});
2022-01-11 16:29:54 -05:00
promises.push(p);
2022-01-11 14:41:49 -05:00
}
2024-07-20 02:24:40 -04:00
return Promise.all(promises).then(() => {
2024-07-24 19:30:44 -04:00
log('analysis complete').unpin();
2022-01-11 16:29:54 -05:00
});
2022-01-06 20:22:50 -05:00
},
2022-04-05 09:40:25 -07:00
isolate(models) {
models = fallback(models);
2024-07-24 19:30:44 -04:00
log('isolating bodies').pin();
2022-04-05 09:40:25 -07:00
let promises = [];
let mcore = new Matrix4().makeRotationX(Math.PI / 2);
let mark = Date.now();
2022-04-05 09:40:25 -07:00
for (let m of models) {
let p = worker.model_isolate({ id: m.id }).then(bodies => {
bodies = bodies.map(vert => new meshModel({
2022-04-05 09:40:25 -07:00
file: m.file,
mesh: vert.toFloat32()
})).map( nm => nm.applyMatrix4(mcore.clone().multiply(m.mesh.matrixWorld)) );
api.group.new(bodies, undefined, "isolate");
2022-04-05 09:40:25 -07:00
});
promises.push(p);
}
2024-07-20 02:24:40 -04:00
return Promise.all(promises).then(() => {
2024-07-24 19:30:44 -04:00
log('isolation complete').unpin();
// log(`... isolate time = ${Date.now() - mark}`);
2022-04-05 09:40:25 -07:00
});
},
indexFaces(models, opt = {}) {
2024-08-24 23:24:03 -04:00
models = fallback(models, false);
2024-07-24 19:30:44 -04:00
log('mapping faces').pin();
let promises = [];
let mark = Date.now();
for (let m of models) {
let p = worker.model_indexFaces({ id: m.id, opt }).then(data => {
// console.log({map_info: data});
});
promises.push(p);
}
Promise.all(promises).then(() => {
2024-07-24 19:30:44 -04:00
log('mapping complete').unpin();
// log(`... index time = ${Date.now() - mark}`);
});
},
2022-01-11 22:42:43 -05:00
heal(models, opt = {}) {
2022-01-12 23:02:08 -05:00
models = fallback(models);
2022-01-11 21:04:24 -05:00
let promises = [];
for (let m of models) {
2022-01-11 22:42:43 -05:00
let p = worker.model_heal({
id: m.id, opt
}).then(data => {
2024-08-10 14:34:16 -04:00
data?.vertices && m.reload(data.vertices);
2024-09-10 21:15:19 -04:00
return data;
2022-01-06 23:51:35 -05:00
});
2022-01-11 16:29:54 -05:00
promises.push(p);
2022-01-06 20:22:50 -05:00
}
2022-01-11 22:42:43 -05:00
return new Promise((resolve, reject) => {
Promise.all(promises).then(resolve).catch(error => reject);
2022-01-11 22:42:43 -05:00
});
},
repair(models) {
2022-01-12 23:02:08 -05:00
models = fallback(models);
2024-07-24 19:30:44 -04:00
log('repairing mesh(es)').pin();
2022-01-11 22:42:43 -05:00
tool.heal(models, { merge: true }).then(() => {
2024-07-24 19:30:44 -04:00
log('repair commplete').unpin();
meshUtil.defer(selection.update);
2022-01-11 22:42:43 -05:00
});
},
clean(models) {
2022-01-12 23:02:08 -05:00
models = fallback(models);
2024-08-10 14:34:16 -04:00
tool.heal(models, {
2024-09-10 21:15:19 -04:00
round: 3,
precision: 3,
2024-08-10 14:34:16 -04:00
merge: false,
dedup: true,
clean: true
2024-09-10 21:15:19 -04:00
}).then((data) => {
for (let od of data) {
let { vertices, zlist } = od;
console.log({ zlist });
log(`${vertices.length/3} vertices, ${zlist.length} unique Z`);
}
2024-07-24 19:30:44 -04:00
log('cleaning complete').unpin();
meshUtil.defer(selection.update);
2022-01-11 16:29:54 -05:00
});
2024-09-10 21:15:19 -04:00
},
flatten(models) {
fallback(models, false).map(model => model.selectionFlatten());
2022-01-06 20:22:50 -05:00
}
};
const modes = {
object: "object", // mesh or group
tool: "tool", // mesh or group for subtraction
face: "face", // facet / triangle
surface: "surface", // normal aligned & adjacent faces
edge: "edge", // edge of a face
2024-07-24 16:57:42 -04:00
sketch: "sketch", // 2D sketching
};
2022-01-13 17:01:35 -05:00
// edit mode
2022-02-20 00:15:59 -05:00
const mode = {
2022-01-13 17:01:35 -05:00
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');
$('mode-label').innerText = mode;
api.mode.check();
meshEdges?.end();
2024-07-24 16:57:42 -04:00
if (mode === 'sketch') {
$C('sketch-on', null, 'hide');
$C('sketch-off', 'hide', null);
} else {
$C('sketch-off', null, 'hide');
$C('sketch-on', 'hide', null);
}
2022-01-13 17:01:35 -05:00
},
get() {
return prefs.map.mode;
},
is(modelist) {
for (let m of modelist) {
if (prefs.map.mode === m) return true;
}
return false;
},
check() {
if (prefs.map.mode === modes.surface) {
tool.indexFaces();
}
},
2022-01-13 17:01:35 -05:00
object() {
2024-07-23 22:42:20 -04:00
if (!mode.is([ modes.object, modes.tool ])) {
selection.clear();
}
2022-01-13 17:01:35 -05:00
mode.set(modes.object);
},
tool() {
2024-07-23 22:42:20 -04:00
if (!mode.is([ modes.object, modes.tool ])) {
selection.clear();
}
mode.set(modes.tool);
2022-01-13 17:01:35 -05:00
},
face() {
2024-07-23 22:42:20 -04:00
if (!mode.is([ modes.face, modes.surface ])) {
selection.clear();
}
mode.set(modes.face);
2022-01-13 17:01:35 -05:00
},
surface() {
2024-07-23 22:42:20 -04:00
if (!mode.is([ modes.face, modes.surface ])) {
selection.clear();
}
mode.set(modes.surface);
},
edge() {
if (!mode.is([ modes.edge ])) {
selection.clear();
}
mode.set(modes.edge);
meshEdges.start();
2024-07-24 16:57:42 -04:00
},
sketch() {
if (!mode.is([ modes.sketch ])) {
selection.clear();
let first = sketch.list()[0];
selection.set(first ? [ first ] : []);
2024-07-24 16:57:42 -04:00
}
mode.set(modes.sketch);
}
2022-01-13 17:01:35 -05:00
};
2022-01-18 16:34:42 -05:00
// cache pref signature so we know when it changes
let prefsig;
let prefsignew;
2022-01-10 23:02:34 -05:00
// persisted preference map
2022-02-20 00:15:59 -05:00
const prefs = {
map: {
2022-01-20 17:46:39 -05:00
info: {
group: "show",
span: "show"
},
2022-01-13 17:01:35 -05:00
mode: modes.object,
edit: {
scale_group_X: true,
scale_group_Y: true,
scale_group_Z: true
},
space: {
2025-09-03 16:50:35 -04:00
bounds: false,
center: false,
2025-09-03 16:50:35 -04:00
dark: false,
floor: false,
grid: true,
script: false,
2025-09-03 16:50:35 -04:00
select: [],
snap: 1,
snapon: false,
wire: false
},
sketch: {
open_close: false,
open_thick: 1,
open_type: 'miter'
},
2022-02-25 13:56:09 -05:00
normals: {
2022-10-24 11:43:43 -04:00
length: 0.25,
color_lite: 0xffff00,
color_dark: 0xffff00,
},
surface: {
radians: 0.1,
radius: 0.2
2022-04-18 15:49:52 -04:00
},
wireframe: {
opacity: 0.4,
fog: 3
2022-02-25 13:56:09 -05:00
}
},
2022-01-10 23:02:34 -05:00
2022-01-18 16:34:42 -05:00
// look for changes in pref signature
changed() {
prefsignew = JSON.stringify(prefs.map);
return prefsignew !== prefsig;
2022-01-10 23:02:34 -05:00
},
// persist to data store
save() {
2022-01-18 16:34:42 -05:00
if (prefs.changed()) {
api.db.admin.put("prefs", prefs.map);
2022-01-18 16:34:42 -05:00
prefsig = prefsignew;
}
2022-01-10 23:02:34 -05:00
},
// reload from data store
load() {
return api.db.admin.get("prefs").then(data => {
// handle/ignore incompatible older prefs
if (data) Object.assign(prefs.map, data);
2022-01-10 23:02:34 -05:00
});
}
};
let mark = Date.now();
let since = () => {
return ((Date.now() - mark) / 1000).toFixed(2).padStart(9,0);
};
let dbug = self.dbug = self.dbug || {
level: 1, // 0=debug, 1=normal, 2=warn, 3=error
set: (level) => {
if (level >= 0) dbug.level = Math.min(level, 3);
},
debug: function() {
if (dbug.level <= 0) return console.log(since(),'[DBG]', ...arguments);
},
log: function() {
if (dbug.level <= 1) return console.log(since(),'|',...arguments);
},
warn: function() {
if (dbug.level <= 2) return console.log(since(),'[WRN]', ...arguments);
},
error: function() {
if (dbug.level <= 3) return console.trace(since(),'[ERR]', ...arguments);
},
since
};
2022-01-08 21:00:19 -05:00
// api is augmented in mesh.build (log, modal, download)
const api = {
init() {
// publish messages when api functions are called
broker.wrapObject(selection, 'selection');
// broker.wrapObject(model, 'model');
// broker.wrapObject(group, 'group');
// optimize db writes by merging updates
prefs.save = meshUtil.deferWrap(prefs.save, 100);
},
2022-01-09 10:13:09 -05:00
help() {
window.open("https://docs.grid.space/projects/mesh-tool");
},
2024-08-12 19:40:14 -04:00
bugs() {
window.open("https://github.com/GridSpace/grid-apps/issues");
},
2022-02-25 14:06:28 -05:00
version() {
window.location = "/choose?back";
2022-02-25 14:06:28 -05:00
},
donate() {
window.open("https://www.paypal.com/paypalme/gridspace3d");
},
2024-07-28 22:39:04 -04:00
gridspace() {
window.open("https://grid.space");
},
kirimoto() {
window.location = "/kiri";
},
2022-01-01 23:48:36 -05:00
clear() {
2025-10-24 23:56:06 -04:00
for (let grp of group.list()) {
group.remove(grp);
2021-12-31 14:12:42 -05:00
}
},
history: {
undo() {
history.undo();
},
redo() {
history.redo();
}
},
// @param object {MeshObject | MeshObject[] | Object}
2022-01-01 23:48:36 -05:00
focus(object) {
let { center } = object.center ? object : meshUtil.bounds(object);
2021-12-31 17:20:41 -05:00
// when no valid objects supplied, set origin
2021-12-31 16:59:38 -05:00
if (isNaN(center.x * center.y * center.z)) {
2021-12-31 17:20:41 -05:00
center = { x: 0, y: 0, z: 0 };
2021-12-31 16:59:38 -05:00
}
let { normal } = object;
let left, up;
if (normal) {
let { x, y, z } = normal;
2024-08-09 21:53:33 -04:00
left = new Vector3(x,y,0).angleTo(new Vector3(0,-1,0));
up = new Vector3(0,y,z).angleTo(new Vector3(0,0,1));
if (x < 0) left = -left;
}
2021-12-31 17:20:41 -05:00
// sets "home" views (front, back, home, reset)
motoSpace.platform.setCenter(center.x, -center.y, center.z);
2021-12-31 17:20:41 -05:00
// sets camera focus
motoSpace.view.panTo(center.x, center.z, -center.y, left, up);
2021-12-31 16:59:38 -05:00
},
2022-04-18 15:49:52 -04:00
grid(state = { toggle:true }) {
let { platform } = motoSpace;
let { map, save } = prefs;
2022-01-07 21:32:53 -05:00
if (state.toggle) {
platform.showGrid(!platform.isGridVisible());
2022-01-07 21:32:53 -05:00
} else {
platform.showGrid(state);
2022-01-07 21:32:53 -05:00
}
map.space.grid = platform.isGridVisible();
save();
2022-01-07 21:32:53 -05:00
},
wireedges(state = { toggle:true }, opt = {
opacity: prefs.map.wireframe.opacity,
edges: base.util.toDegrees(api.prefs.map.surface.radians)
}) {
api.wireframe(state, opt);
},
wireframe(state = { toggle:true }, opt = {
opacity: prefs.map.wireframe.opacity,
edges: prefs.map.space.wire_edge
}) {
const mspace = prefs.map.space;
let { wire } = mspace;
if (state.toggle) {
wire = !wire;
} else {
wire = state;
}
2022-01-14 14:14:13 -05:00
for (let m of model.list()) {
m.wireframe(wire, opt);
2022-01-06 09:38:04 -05:00
}
mspace.wire = wire;
mspace.wire_edge = wire ? opt.edges : 0;
prefs.save();
api.updateFog();
},
updateFog() {
const mspace = prefs.map.space;
2022-04-18 15:49:52 -04:00
const mwire = prefs.map.wireframe;
if (mspace.wire) {
motoSpace.scene.setFog(mwire.fog, mspace.dark ? 0 : 0xffffff);
} else {
motoSpace.scene.setFog(false);
}
2022-01-06 09:38:04 -05:00
},
2022-01-14 14:14:13 -05:00
normals(state = {toggle:true}) {
let norm = prefs.map.space.norm;
if (state.toggle) {
norm = !norm;
} else {
norm = state;
}
for (let m of model.list()) {
m.normals(norm);
}
prefs.save( prefs.map.space.norm = norm );
},
objects(selected = false) {
// return model objects suitable for finding ray intersections
if (selected) {
return [
...selection.models().map(o => o.mesh),
...selection.sketches().map(s => s.meshes).flat()
];
} else {
return [
...group.list().map(o => o.models).flat().map(o => o.mesh),
...sketches.map(s => s.meshes).flat()
];
}
},
2022-01-13 17:01:35 -05:00
add,
dbug,
2021-12-31 14:12:42 -05:00
file,
2024-07-20 02:24:40 -04:00
2021-12-31 14:12:42 -05:00
group,
mode,
2024-07-23 22:42:20 -04:00
modes,
2022-04-19 16:51:55 -04:00
model,
2022-01-06 20:22:50 -05:00
pattern,
2022-01-06 17:28:57 -05:00
2022-01-10 23:02:34 -05:00
prefs,
2025-10-24 23:56:06 -04:00
script,
selection,
sketch,
tool,
isDebug: self.debug === true
};
2024-07-24 19:30:44 -04:00
function log() {
2024-07-25 00:05:24 -04:00
return api.log.emit(...arguments);
2024-07-24 19:30:44 -04:00
}
const call = broker.send;
self.mesh_api = api;
2022-01-10 23:02:34 -05:00
export {
sketches,
selection,
groups,
api
};