add mesh edge selection to allow creating faces

This commit is contained in:
Stewart Allen 2024-07-21 22:36:37 -04:00
commit 4261d2eea9
8 changed files with 92 additions and 38 deletions

View file

@ -2,4 +2,9 @@ import * as THREE from "../node_modules/three/build/three.module.js";
import * as SVGLoader from "../node_modules/three/examples/jsm/loaders/SVGLoader.js"; import * as SVGLoader from "../node_modules/three/examples/jsm/loaders/SVGLoader.js";
import * as BufferGeometryUtils from "../node_modules/three/examples/jsm/utils/BufferGeometryUtils.js"; import * as BufferGeometryUtils from "../node_modules/three/examples/jsm/utils/BufferGeometryUtils.js";
export { THREE, SVGLoader, BufferGeometryUtils }; import { Line2 } from '../node_modules/three/examples/jsm/lines/Line2.js';
import { LineMaterial } from '../node_modules/three/examples/jsm/lines/LineMaterial.js';
import { LineGeometry } from '../node_modules/three/examples/jsm/lines/LineGeometry.js';
import { LineSegments2 } from '../node_modules/three/examples/jsm/lines/LineSegments2.js';
export { THREE, SVGLoader, BufferGeometryUtils, Line2, LineMaterial, LineGeometry, LineSegments2 };

View file

@ -5,11 +5,15 @@
// dep: ext.three // dep: ext.three
gapp.register("add.three", [], (root, exports) => { gapp.register("add.three", [], (root, exports) => {
const { THREE, SVGLoader, BufferGeometryUtils } = ThreeBundle; const { THREE, SVGLoader, BufferGeometryUtils, Line2, LineMaterial, LineGeometry, LineSegments2 } = ThreeBundle;
root.THREE = THREE; root.THREE = THREE;
THREE.BufferGeometryUtils = BufferGeometryUtils; THREE.BufferGeometryUtils = BufferGeometryUtils;
THREE.SVGLoader = SVGLoader.SVGLoader; THREE.SVGLoader = SVGLoader.SVGLoader;
THREE.LineMaterial = LineMaterial;
THREE.LineGeometry = LineGeometry;
THREE.LineSegments2 = LineSegments2;
THREE.Line2 = Line2;
let MP = THREE.Mesh.prototype, let MP = THREE.Mesh.prototype,
XP = THREE.Box3.prototype, XP = THREE.Box3.prototype,

View file

@ -596,8 +596,8 @@ base.util = {
ptimer, ptimer,
flatten, flatten,
rotate, rotate,
dist2D,
distSq, distSq,
dist2D,
distSqv2, distSqv2,
center2d, center2d,
center2pr, center2pr,

View file

@ -134,6 +134,37 @@ class Point {
return Math.sqrt(this.distToLineSq(p1, p2)); return Math.sqrt(this.distToLineSq(p1, p2));
} }
distToLine3D(lp1, lp2) {
// Convert points to vectors
const p0 = [this.x, this.y, this.z];
const p1 = [lp1.x, lp1.y, lp1.z];
const p2 = [lp2.x, lp2.y, lp2.z];
// Calculate the direction vector of the line
const lineDir = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2]];
// Calculate the vector from p1 to the point
const p1ToP = [p0[0] - p1[0], p0[1] - p1[1], p0[2] - p1[2]];
// Calculate the cross product of lineDir and p1ToP
const crossProd = [
lineDir[1] * p1ToP[2] - lineDir[2] * p1ToP[1],
lineDir[2] * p1ToP[0] - lineDir[0] * p1ToP[2],
lineDir[0] * p1ToP[1] - lineDir[1] * p1ToP[0]
];
// Calculate the magnitude of the cross product
const crossProdMag = Math.sqrt(crossProd[0] ** 2 + crossProd[1] ** 2 + crossProd[2] ** 2);
// Calculate the magnitude of the direction vector of the line
const lineDirMag = Math.sqrt(lineDir[0] ** 2 + lineDir[1] ** 2 + lineDir[2] ** 2);
// Distance from point to line is the magnitude of the cross product divided by the magnitude of the direction vector
const distance = crossProdMag / lineDirMag;
return distance;
}
/** /**
* used exclusively in new fill code. output does not agree with * used exclusively in new fill code. output does not agree with
* old distToLine, but is the only method that seems to work for * old distToLine, but is the only method that seems to work for

View file

@ -158,7 +158,7 @@ async function restore_space() {
let tgroup = api.group.list().filter(m => tolist.contains(m.id)); let tgroup = api.group.list().filter(m => tolist.contains(m.id));
api.selection.set([...smodel, ...sgroup], [...tmodel, ...tgroup]); api.selection.set([...smodel, ...sgroup], [...tmodel, ...tgroup]);
// restore edit mode // restore edit mode
api.mode.set(mode); api.mode[mode]();
// restore dark mode // restore dark mode
set_darkmode(map.space.dark); set_darkmode(map.space.dark);
}); });
@ -207,7 +207,7 @@ function space_init(data) {
case 'KeyQ': case 'KeyQ':
return api.settings(); return api.settings();
case 'KeyI': case 'KeyI':
return api.file.import(); return shiftKey ? api.tool.invert() : api.file.import();
case 'KeyX': case 'KeyX':
return api.file.export(); return api.file.export();
case 'KeyD': case 'KeyD':
@ -221,6 +221,7 @@ function space_init(data) {
case 'KeyU': case 'KeyU':
return shiftKey && api.tool.union(); return shiftKey && api.tool.union();
case 'KeyA': case 'KeyA':
if (api.mode.is([ api.modes.edge ])) return mesh.edges.add();
return shiftKey && api.tool.analyze(); return shiftKey && api.tool.analyze();
case 'KeyR': case 'KeyR':
return shiftKey ? api.tool.rebuild() : api.tool.repair(); return shiftKey ? api.tool.rebuild() : api.tool.repair();
@ -237,7 +238,8 @@ function space_init(data) {
case 'KeyL': case 'KeyL':
return api.log.toggle({ spinner: false }); return api.log.toggle({ spinner: false });
case 'KeyS': case 'KeyS':
return shiftKey ? selection.visible({toggle:true}) : call.edit_split(); if (!api.mode.is([ api.modes.object ])) return;
return shiftKey ? selection.visible({toggle:true}) : mesh.split.start();
case 'KeyB': case 'KeyB':
return selection.boundsBox({toggle:true}); return selection.boundsBox({toggle:true});
case 'KeyH': case 'KeyH':
@ -261,13 +263,15 @@ function space_init(data) {
switch (code) { switch (code) {
case 'KeyA': case 'KeyA':
if (metaKey || ctrlKey) { if (metaKey || ctrlKey) {
api.mode.object();
selection.set(api.group.list()); selection.set(api.group.list());
estop(evt); estop(evt);
} }
break; break;
case 'Escape': case 'Escape':
selection.clear(); selection.clear();
mesh.split.active() && mesh.split.end(); mesh.edges.clear();
mesh.split.end();
estop(evt); estop(evt);
break; break;
case 'Backspace': case 'Backspace':
@ -358,6 +362,9 @@ function space_init(data) {
shiftKey ? { clear: true } : { select: true }, shiftKey ? { clear: true } : { select: true },
opt); opt);
break; break;
case modes.edge:
mesh.edges.select();
break;
} }
} }
} }
@ -369,7 +376,7 @@ function space_init(data) {
space.mouse.onDrag((delta, offset, up = false) => { space.mouse.onDrag((delta, offset, up = false) => {
const { mode, modes } = api; const { mode, modes } = api;
if (delta && delta.event.shiftKey) { if (delta && delta.event.shiftKey) {
selection.move(delta.x, delta.y, 0); selection.count() && selection.move(delta.x, delta.y, 0);
} else if (mode.is([ modes.object, modes.tool ])) { } else if (mode.is([ modes.object, modes.tool ])) {
return api.objects().length > 0; return api.objects().length > 0;
} }

View file

@ -7,6 +7,7 @@
// dep: moto.broker // dep: moto.broker
// dep: moto.space // dep: moto.space
// dep: mesh.util // dep: mesh.util
// dep: mesh.edges
// dep: add.three // dep: add.three
// use: add.array // use: add.array
gapp.register("mesh.api", [], (root, exports) => { gapp.register("mesh.api", [], (root, exports) => {
@ -24,6 +25,10 @@ let selected = [];
let tools = []; let tools = [];
const selection = { const selection = {
count() {
return selected.length;
},
// @returns {MeshObject[]} or all groups if not strict and no selection // @returns {MeshObject[]} or all groups if not strict and no selection
list(strict = false) { list(strict = false) {
return selected.length || strict ? selected.slice() : groups.slice(); return selected.length || strict ? selected.slice() : groups.slice();
@ -562,11 +567,11 @@ const tool = {
return { id: m.id, matrix: m.matrix } return { id: m.id, matrix: m.matrix }
})) }))
.then(data => { .then(data => {
api.selection.visible(false);
let group = api.group.new([new mesh.model({ let group = api.group.new([new mesh.model({
file: `merged`, file: `merged`,
mesh: data mesh: data
})]).promote(); })]).promote();
api.selection.visible(false);
api.selection.set([group]); api.selection.set([group]);
api.log.emit('merge complete').unpin(); api.log.emit('merge complete').unpin();
}); });
@ -582,11 +587,11 @@ const tool = {
return { id: m.id, matrix: m.matrix } return { id: m.id, matrix: m.matrix }
})) }))
.then(data => { .then(data => {
api.selection.visible(false);
let group = api.group.new([new mesh.model({ let group = api.group.new([new mesh.model({
file: `union`, file: `union`,
mesh: data mesh: data
})]).promote(); })]).promote();
api.selection.visible(false);
api.selection.set([group]); api.selection.set([group]);
}) })
.catch(error => { .catch(error => {
@ -607,11 +612,11 @@ const tool = {
return { id: m.id, matrix: m.matrix } return { id: m.id, matrix: m.matrix }
})) }))
.then(data => { .then(data => {
api.selection.visible(false);
let group = api.group.new([new mesh.model({ let group = api.group.new([new mesh.model({
file: `diff`, file: `diff`,
mesh: data mesh: data
})]).promote(); })]).promote();
api.selection.visible(false);
api.selection.set([group]); api.selection.set([group]);
}) })
.catch(error => { .catch(error => {
@ -857,6 +862,7 @@ const mode = {
} }
$(`mode-${mode}`).classList.add('selected'); $(`mode-${mode}`).classList.add('selected');
api.mode.check(); api.mode.check();
mesh.edges.end();
}, },
get() { get() {
@ -909,6 +915,7 @@ const mode = {
selection.clear(); selection.clear();
} }
mode.set(modes.edge); mode.set(modes.edge);
mesh.edges.start();
} }
}; };
@ -1091,6 +1098,8 @@ const api = exports({
}); });
const { broker } = gapp; const { broker } = gapp;
const call = broker.send;
// publish messages when api functions are called // publish messages when api functions are called
broker.wrapObject(selection, 'selection'); broker.wrapObject(selection, 'selection');
broker.wrapObject(model, 'model'); broker.wrapObject(model, 'model');

View file

@ -157,20 +157,21 @@ mesh.model = class MeshModel extends mesh.object {
// translated into world coordinates (rebuilt from rotation matrix) // translated into world coordinates (rebuilt from rotation matrix)
// defaults to returning a new model in a new group // defaults to returning a new model in a new group
// options to mirror, re-use a group, or update model in-place // options to mirror, re-use a group, or update model in-place
duplicate(opt = {}) { duplicate(opt = { select: true }) {
return worker.model_duplicate({ return worker.model_duplicate({
matrix: this.matrix, matrix: this.matrix,
id: this.id, id: this.id,
opt: { mirror: opt.mirror} opt: { mirror: opt.mirror}
}).then(data => { }).then(data => {
if (opt.append) data = [...data, ...opt.append].toFloat32();
if (opt.reload) return this.reload(data); if (opt.reload) return this.reload(data);
let model = new mesh.model({ file: `${this.file}`, mesh: data }); let model = new mesh.model({ file: `${this.file}`, mesh: data });
let group = opt.group || mesh.api.group.new(); let group = opt.group || mesh.api.group.new();
group.add(model); group.add(model);
group.setSelected(); model.wireframe(this.wireframe());
if (opt.mirror) { model.normals(this.normals());
group.move(0, 0, group.bounds.dim.z); if (opt.select) group.setSelected();
} if (opt.mirror) group.move(0, 0, group.bounds.dim.z);
return model; return model;
}); });
} }
@ -209,7 +210,6 @@ mesh.model = class MeshModel extends mesh.object {
geo.setIndex(new BufferAttribute(indices, 1)); geo.setIndex(new BufferAttribute(indices, 1));
geo = geo.toNonIndexed(); geo = geo.toNonIndexed();
} }
let meh = this.mesh = new Mesh(geo, [ let meh = this.mesh = new Mesh(geo, [
this.mats.normal, this.mats.normal,
this.mats.face this.mats.face

View file

@ -1,25 +1,32 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
/**
* toggle edit/split on Z tool
* presents a Z plane on hover
* click to split the selected mesh(es)
*/
"use strict"; "use strict";
// dep: moto.space // dep: moto.space
gapp.register("mesh.split", [], (root, exports) => { gapp.register("mesh.split", [], (root, exports) => {
const { Mesh, MeshPhongMaterial, PlaneGeometry, DoubleSide, Vector3 } = THREE; const { Mesh, MeshPhongMaterial, PlaneGeometry, DoubleSide, Vector3 } = THREE;
const { broker } = gapp;
const { moto } = root; const { moto } = root;
const { space } = moto; const { space } = moto;
// toggle edit/split temporary mode (present plane on hover) let isActive;
let temp_mode;
// split functions // split functions
let split = { let split = {
active() { active() {
return temp_mode ? true : false; return isActive ? true : false;
}, },
start() { start() {
if (isActive) {
return;
}
let { api, util } = mesh; let { api, util } = mesh;
// highlight button // highlight button
let button = event.target; let button = event.target;
@ -54,7 +61,7 @@ let split = {
let { dim, mid } = util.bounds(meshes); let { dim, mid } = util.bounds(meshes);
let { point, face, object } = int; let { point, face, object } = int;
let { x, y, z } = point; let { x, y, z } = point;
// set appearance of split plane
mat.color.set(0x5555aa); mat.color.set(0x5555aa);
obj.visible = true; obj.visible = true;
if (event.shiftKey) { if (event.shiftKey) {
@ -62,10 +69,11 @@ let split = {
} }
// y is z in model space for the purposes of a split // y is z in model space for the purposes of a split
state.plane = { z: y }; state.plane = { z: y };
// ensure split plane exceeds bounds of split target
obj.scale.set(dim.x + 2, dim.y + 2, 1); obj.scale.set(dim.x + 2, dim.y + 2, 1);
obj.position.set(mid.x, y, -mid.y); obj.position.set(mid.x, y, -mid.y);
}); });
temp_mode = split; isActive = true;
}, },
select() { select() {
@ -73,19 +81,22 @@ let split = {
let { models, plane } = split.state; let { models, plane } = split.state;
log.emit(`splitting ${models.length} model(s) at ${plane.z.round(3)}`).pin(); log.emit(`splitting ${models.length} model(s) at ${plane.z.round(3)}`).pin();
Promise.all(models.map(m => m.split(plane))).then(models => { Promise.all(models.map(m => m.split(plane))).then(models => {
mesh.api.selection.set(models); mesh.api.selection.set(models.filter(m => m));
log.emit('split complete').unpin(); log.emit('split complete').unpin();
split.end(); split.end();
}); });
}, },
end() { end() {
if (!isActive) {
return;
}
let space = moto.space; let space = moto.space;
let { button, obj } = split.state; let { button, obj } = split.state;
button.classList.remove('selected'); button.classList.remove('selected');
space.scene.remove(obj); space.scene.remove(obj);
space.mouse.onHover(undefined); space.mouse.onHover(undefined);
temp_mode = split.state = undefined; isActive = split.state = undefined;
mesh.api.selection.update(); mesh.api.selection.update();
}, },
@ -102,19 +113,6 @@ let split = {
} }
} }
function edit_split(event) {
if (temp_mode) {
temp_mode.end();
} else {
split.start();
}
}
exports(split); exports(split);
// bind functions to topics
broker.listeners({
edit_split,
});
}); });