add mesh edge selection to allow creating faces
This commit is contained in:
parent
3541c76ccb
commit
4261d2eea9
8 changed files with 92 additions and 38 deletions
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -596,8 +596,8 @@ base.util = {
|
|||
ptimer,
|
||||
flatten,
|
||||
rotate,
|
||||
dist2D,
|
||||
distSq,
|
||||
dist2D,
|
||||
distSqv2,
|
||||
center2d,
|
||||
center2pr,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue