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 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
gapp.register("add.three", [], (root, exports) => {
const { THREE, SVGLoader, BufferGeometryUtils } = ThreeBundle;
const { THREE, SVGLoader, BufferGeometryUtils, Line2, LineMaterial, LineGeometry, LineSegments2 } = ThreeBundle;
root.THREE = THREE;
THREE.BufferGeometryUtils = BufferGeometryUtils;
THREE.SVGLoader = SVGLoader.SVGLoader;
THREE.LineMaterial = LineMaterial;
THREE.LineGeometry = LineGeometry;
THREE.LineSegments2 = LineSegments2;
THREE.Line2 = Line2;
let MP = THREE.Mesh.prototype,
XP = THREE.Box3.prototype,

View file

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

View file

@ -134,6 +134,37 @@ class Point {
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
* 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));
api.selection.set([...smodel, ...sgroup], [...tmodel, ...tgroup]);
// restore edit mode
api.mode.set(mode);
api.mode[mode]();
// restore dark mode
set_darkmode(map.space.dark);
});
@ -207,7 +207,7 @@ function space_init(data) {
case 'KeyQ':
return api.settings();
case 'KeyI':
return api.file.import();
return shiftKey ? api.tool.invert() : api.file.import();
case 'KeyX':
return api.file.export();
case 'KeyD':
@ -221,6 +221,7 @@ function space_init(data) {
case 'KeyU':
return shiftKey && api.tool.union();
case 'KeyA':
if (api.mode.is([ api.modes.edge ])) return mesh.edges.add();
return shiftKey && api.tool.analyze();
case 'KeyR':
return shiftKey ? api.tool.rebuild() : api.tool.repair();
@ -237,7 +238,8 @@ function space_init(data) {
case 'KeyL':
return api.log.toggle({ spinner: false });
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':
return selection.boundsBox({toggle:true});
case 'KeyH':
@ -261,13 +263,15 @@ function space_init(data) {
switch (code) {
case 'KeyA':
if (metaKey || ctrlKey) {
api.mode.object();
selection.set(api.group.list());
estop(evt);
}
break;
case 'Escape':
selection.clear();
mesh.split.active() && mesh.split.end();
mesh.edges.clear();
mesh.split.end();
estop(evt);
break;
case 'Backspace':
@ -358,6 +362,9 @@ function space_init(data) {
shiftKey ? { clear: true } : { select: true },
opt);
break;
case modes.edge:
mesh.edges.select();
break;
}
}
}
@ -369,7 +376,7 @@ function space_init(data) {
space.mouse.onDrag((delta, offset, up = false) => {
const { mode, modes } = api;
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 ])) {
return api.objects().length > 0;
}

View file

@ -7,6 +7,7 @@
// dep: moto.broker
// dep: moto.space
// dep: mesh.util
// dep: mesh.edges
// dep: add.three
// use: add.array
gapp.register("mesh.api", [], (root, exports) => {
@ -24,6 +25,10 @@ let selected = [];
let tools = [];
const selection = {
count() {
return selected.length;
},
// @returns {MeshObject[]} or all groups if not strict and no selection
list(strict = false) {
return selected.length || strict ? selected.slice() : groups.slice();
@ -562,11 +567,11 @@ const tool = {
return { id: m.id, matrix: m.matrix }
}))
.then(data => {
api.selection.visible(false);
let group = api.group.new([new mesh.model({
file: `merged`,
mesh: data
})]).promote();
api.selection.visible(false);
api.selection.set([group]);
api.log.emit('merge complete').unpin();
});
@ -582,11 +587,11 @@ const tool = {
return { id: m.id, matrix: m.matrix }
}))
.then(data => {
api.selection.visible(false);
let group = api.group.new([new mesh.model({
file: `union`,
mesh: data
})]).promote();
api.selection.visible(false);
api.selection.set([group]);
})
.catch(error => {
@ -607,11 +612,11 @@ const tool = {
return { id: m.id, matrix: m.matrix }
}))
.then(data => {
api.selection.visible(false);
let group = api.group.new([new mesh.model({
file: `diff`,
mesh: data
})]).promote();
api.selection.visible(false);
api.selection.set([group]);
})
.catch(error => {
@ -857,6 +862,7 @@ const mode = {
}
$(`mode-${mode}`).classList.add('selected');
api.mode.check();
mesh.edges.end();
},
get() {
@ -909,6 +915,7 @@ const mode = {
selection.clear();
}
mode.set(modes.edge);
mesh.edges.start();
}
};
@ -1091,6 +1098,8 @@ const api = exports({
});
const { broker } = gapp;
const call = broker.send;
// publish messages when api functions are called
broker.wrapObject(selection, 'selection');
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)
// defaults to returning a new model in a new group
// options to mirror, re-use a group, or update model in-place
duplicate(opt = {}) {
duplicate(opt = { select: true }) {
return worker.model_duplicate({
matrix: this.matrix,
id: this.id,
opt: { mirror: opt.mirror}
}).then(data => {
if (opt.append) data = [...data, ...opt.append].toFloat32();
if (opt.reload) return this.reload(data);
let model = new mesh.model({ file: `${this.file}`, mesh: data });
let group = opt.group || mesh.api.group.new();
group.add(model);
group.setSelected();
if (opt.mirror) {
group.move(0, 0, group.bounds.dim.z);
}
model.wireframe(this.wireframe());
model.normals(this.normals());
if (opt.select) group.setSelected();
if (opt.mirror) group.move(0, 0, group.bounds.dim.z);
return model;
});
}
@ -209,7 +210,6 @@ mesh.model = class MeshModel extends mesh.object {
geo.setIndex(new BufferAttribute(indices, 1));
geo = geo.toNonIndexed();
}
let meh = this.mesh = new Mesh(geo, [
this.mats.normal,
this.mats.face

View file

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