Merge branch 'knife-tool' into next

This commit is contained in:
JannisX11 2024-04-05 18:36:30 +02:00
commit 010fd037bf
7 changed files with 596 additions and 8 deletions

View File

@ -431,7 +431,9 @@ class Tool extends Action {
if (this.condition == undefined && this.modes instanceof Array) {
this.condition = {modes: this.modes};
}
this.raycast_options = data.raycast_options;
this.onCanvasClick = data.onCanvasClick;
this.onCanvasMouseMove = data.onCanvasMouseMove;
this.onCanvasRightClick = data.onCanvasRightClick;
this.onTextureEditorClick = data.onTextureEditorClick;
this.onSelect = data.onSelect;
@ -2062,6 +2064,7 @@ const BARS = {
'pivot_tool',
'vertex_snap_tool',
'stretch_tool',
'knife_tool',
'seam_tool',
'pan_tool',
'brush_tool',

View File

@ -792,6 +792,12 @@ addEventListeners(document, 'keydown mousedown', function(e) {
} else if (Keybinds.extra.cancel.keybind.isTriggered(e) && (Transformer.dragging)) {
Transformer.cancelMovement(e, false);
updateSelection();
} else if (KnifeToolContext.current) {
if (Keybinds.extra.cancel.keybind.isTriggered(e)) {
KnifeToolContext.current.cancel();
} else if (Keybinds.extra.confirm.keybind.isTriggered(e)) {
KnifeToolContext.current.apply();
}
}
//Keybinds
if (!input_focus || !used_for_input_action) {

View File

@ -98,6 +98,528 @@ const ProportionalEdit = {
}
}
class KnifeToolContext {
/**
* Click
* Create point
* Snap point to face or edge
* Connect points with lines
* Press something to apply
*
* Iterate over faces
* Remove former face, refill section between old edges and new edges
*/
constructor(mesh) {
this.mesh = mesh;
this.points = [];
this.hover_point = null;
this.points_geo = new THREE.BufferGeometry();
let points_material = new THREE.PointsMaterial({size: 9, sizeAttenuation: false, vertexColors: true});
this.points_mesh = new THREE.Points(this.points_geo, points_material);
this.points_mesh.renderOrder = 100;
//points_material.depthTest = false
this.lines_mesh = new THREE.Line(this.points_geo, Canvas.outlineMaterial);
this.points_mesh.frustumCulled = false;
this.lines_mesh.frustumCulled = false;
this.mesh.mesh.add(this.points_mesh);
this.mesh.mesh.add(this.lines_mesh);
}
showToast() {
this.toast = Blockbench.showToastNotification({
text: tl('message.knife_tool.confirm', [Keybinds.extra.confirm.keybind.label]),
icon: BarItems.knife_tool.icon,
click: () => {
this.apply();
}
});
}
hover(data) {
if (data.element != this.mesh || !data) {
if (this.hover_point) {
this.hover_point = null;
this.updatePreviewGeometry();
}
return;
}
let point = {
position: new THREE.Vector3().copy(data.intersects[0].point),
type: data.type == 'element' ? 'face' : data.type,
attached_vertex: data.vertex,
attached_line: data.vertices,
snapped: false,
fkey: data.face
}
// Snapping
if (data.type == 'vertex') {
point.position.fromArray(this.mesh.vertices[data.vertex]);
point.snapped = true;
} else if (data.type == 'line') {
// https://gamedev.stackexchange.com/questions/72528/how-can-i-project-a-3d-point-onto-a-3d-line
let point_a = Reusable.vec1.fromArray(this.mesh.vertices[data.vertices[0]]);
let point_b = Reusable.vec2.fromArray(this.mesh.vertices[data.vertices[1]]);
let a_b = new THREE.Vector3().copy(point_b).sub(point_a);
let a_p = new THREE.Vector3().copy(point.position).sub(point_a);
point.position.copy(point_a).addScaledVector(a_b, a_p.dot(a_b) / a_b.dot(a_b));
point.snapped = true;
}
// Snap to existing points?
let pos = this.mesh.mesh.localToWorld(Reusable.vec1.copy(point.position));
let threshold = Preview.selected.calculateControlScale(pos) * 0.6;
let matching_point = this.points.find(other => {
return point.position.distanceTo(other.position) < threshold && !other.reuse_of;
})
if (matching_point) {
point.position.copy(matching_point.position);
point.reuse_of = matching_point;
} else if (data.event && (data.event.ctrlOrCmd || Pressing.overrides.shift) && point.fkey) {
let face = this.mesh.faces[point.fkey];
let uv = face.localToUV(point.position);
let factor = (data.event.shiftKey || Pressing.overrides.shift) ? 4 : 1;
uv[0] = Math.round(uv[0] * factor) / factor;
uv[1] = Math.round(uv[1] * factor) / factor;
let target = face.UVToLocal(uv);
point.position.copy(target);
}
if (this.points.length && point.position.distanceToSquared(this.points.last().position) < 0.001) return;
this.hover_point = point;
this.updatePreviewGeometry();
}
updatePreviewGeometry() {
let point_positions = [];
let point_colors = [];
let displayed_points = this.points.slice();
if (this.hover_point) displayed_points.push(this.hover_point);
for (let point of displayed_points) {
point_positions.push(point.position.x, point.position.y, point.position.z);
if (point.snapped) {
point_colors.push(0.1, 0.9, 0.12);
} else {
point_colors.push(0.2, 0.4, 0.98);
}
}
this.points_geo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(point_positions), 3));
this.points_geo.setAttribute('color', new THREE.BufferAttribute(new Float32Array(point_colors), 3));
return this;
}
addPoint(data) {
if (!this.hover_point) this.hover(data);
if (!this.hover_point) return;
let last_point = this.points.last();
if (last_point && this.hover_point) {
let this_point = this.hover_point;
let isSupported = (point_1, point_2) => {
if (point_1.type == 'face' && point_2.type == 'face') {
return point_1.fkey == point_2.fkey;
}
if (point_1.type == 'face' && point_2.type == 'line') {
let face = this.mesh.faces[point_1.fkey];
return (face && face.vertices.includes(point_2.attached_line[0]) && face.vertices.includes(point_2.attached_line[1]));
}
if (point_1.type == 'face' && point_2.type == 'vertex') {
let face = this.mesh.faces[point_1.fkey];
return (face && face.vertices.includes(point_2.attached_vertex));
}
if (point_1.type != 'face' && point_2.type != 'face' && (point_1.type != point_2.type || point_1 == last_point)) {
let pointInFace = (point, vertices) => {
if (point.type == 'line') {
return vertices.includes(point.attached_line[0]) && vertices.includes(point.attached_line[1]);
} else {
return vertices.includes(point.attached_vertex)
}
}
for (let fkey in this.mesh.faces) {
let vertices = this.mesh.faces[fkey]?.vertices;
if (pointInFace(point_1, vertices) && pointInFace(point_2, vertices)) {
return true;
}
}
}
}
if (!isSupported(last_point, this_point) && !isSupported(this_point, last_point)) {
Blockbench.showQuickMessage('message.knife_tool.skipped_face', 2200);
}
}
this.points.push(this.hover_point);
this.hover_point = null;
if (this.points.length == 1) this.showToast();
}
apply() {
if (!this.mesh || !this.points.length) {
this.cancel();
return;
}
function intersectLinesIgnoreTouching(p1, p2, p3, p4) {
let s1 = [ p2[0] - p1[0], p2[1] - p1[1] ];
let s2 = [ p4[0] - p3[0], p4[1] - p3[1] ];
let s = (-s1[1] * (p1[0] - p3[0]) + s1[0] * (p1[1] - p3[1])) / (-s2[0] * s1[1] + s1[0] * s2[1]);
let t = ( s2[0] * (p1[1] - p3[1]) - s2[1] * (p1[0] - p3[0])) / (-s2[0] * s1[1] + s1[0] * s2[1]);
return (s > 0.00001 && s < 0.99999 && t > 0.00001 && t < 0.99999);
}
function lineIntersectsTriangle(l1, l2, v1, v2, v3) {
if (l1.equals(l2)) return false;
let tri = [v1, v2, v3];
let l1_in_tri = tri.find(corner => corner.equals(l1));
let l2_in_tri = tri.find(corner => corner.equals(l2));
if (l1_in_tri && l2_in_tri) {
// Line is identical with tri edge
return false;
}/* else if (l1_in_tri) {
// Nudge away from triangle center
l1 = [
Math.lerp(l1[0], (v1[0] + v2[0] + v3[0]) / 3, -0.001),
Math.lerp(l1[1], (v1[1] + v2[1] + v3[1]) / 3, -0.001)
]
} else if (l2_in_tri) {
// Nudge away from triangle center
l2 = [
Math.lerp(l2[0], (v1[0] + v2[0] + v3[0]) / 3, -0.001),
Math.lerp(l2[1], (v1[1] + v2[1] + v3[1]) / 3, -0.001)
]
}*/
return intersectLinesIgnoreTouching(l1, l2, v1, v2)
|| intersectLinesIgnoreTouching(l1, l2, v2, v3)
|| intersectLinesIgnoreTouching(l1, l2, v3, v1)
|| pointInTriangle(l1.map((v, i) => Math.lerp(v, l2[i], 0.5)), v1, v2, v3)
}
Undo.initEdit({elements: [this.mesh]});
let {mesh} = this;
let all_new_fkeys = [];
let all_new_vkeys = [];
let all_new_edges = [];
let old_face_normal = new THREE.Vector3();
for (let fkey in mesh.faces) {
let face = mesh.faces[fkey];
let all_points = this.points.map(point => {
if (point.fkey == fkey) return point;
if (face.vertices.includes(point.attached_vertex)) return point;
if (point.attached_line && point.attached_line.allAre(vkey => face.vertices.includes(vkey))) return point;
})
let included_points = all_points.filter(point => point);
let new_vertex_points = included_points.filter(point => point.attached_vertex);
if (included_points.length == 0 || (new_vertex_points.length == 1 && included_points.length == 1)) {
continue;
}
let uv_data = {};
let face_sorted_vertices = face.getSortedVertices();
old_face_normal.fromArray(face.getNormal(true));
delete mesh.faces[fkey];
for (let vkey of face_sorted_vertices) {
uv_data[vkey] = face.uv[vkey];
}
// Add new points as vertices
included_points.forEach(point => {
if (!point.vkey) {
if (point.attached_vertex) {
point.vkey = point.attached_vertex;
} else if (point.reuse_of) {
point.vkey = point.reuse_of.vkey;
} else {
point.vkey = mesh.addVertices(point.position.toArray())[0];
all_new_vkeys.push(point.vkey);
}
}
if (!uv_data[point.vkey]) {
uv_data[point.vkey] = face.localToUV(point.position);
}
})
let all_planned_edges = [];
for (let i = 1; i < all_points.length; i++) {
let point_a = all_points[i-1];
let point_b = all_points[i];
if (point_a && point_b) {
all_planned_edges.push([point_a.vkey, point_b.vkey]);
}
}
all_new_edges.push(...all_planned_edges);
let mid_points = included_points.filter(point => point.type == 'face');
let perimeter_points = included_points.filter(point => point.type != 'face');
let mid_edges = all_planned_edges.filter(([vkey1, vkey2]) => {
return !perimeter_points.includes(vkey1) || !perimeter_points.includes(vkey2)
});
let generated_edges = [];
// Track how often each edge is connected, each edge should only be connected to 2 faces
let edge_face_connections = {};
let perimeter_vertices = [];
let perimeter_edges = [];
let covered_perimeter_edges = {};
let created_face_edgings = [];
// Get perimeter edges
for (let i = 0; i < face_sorted_vertices.length; i++) {
let vkey1 = face_sorted_vertices[i];
perimeter_vertices.push(vkey1);
let regular_next = face_sorted_vertices[i+1] || face_sorted_vertices[0];
let regular_edge = [vkey1, regular_next];
let on_edge_points = perimeter_points.filter(point => point.type == 'line' && sameMeshEdge(point.attached_line, regular_edge));
if (on_edge_points.length) {
let vkey1_vector = new THREE.Vector3().fromArray(mesh.vertices[vkey1]);
on_edge_points.sort((a, b) => b.position.distanceToSquared(vkey1_vector) - a.position.distanceToSquared(vkey1_vector));
perimeter_vertices.push(...on_edge_points.map(point => point.vkey));
}
}
for (let i = 0; i < perimeter_vertices.length; i++) {
perimeter_edges.push([perimeter_vertices[i], perimeter_vertices[i+1] || perimeter_vertices[0]]);
}
function getEdgeKey(edge) {
return edge.slice().sort().join('.');
}
// Utility to check for points in faces
let plane = new THREE.Plane().setFromNormalAndCoplanarPoint(
old_face_normal,
Reusable.vec3.fromArray(mesh.vertices[face.vertices[0]])
)
let projection_rot = cameraTargetToRotation([0, 0, 0], old_face_normal.toArray());
let projection_euler = new THREE.Euler(Math.degToRad(projection_rot[1] - 90), Math.degToRad(projection_rot[0] + 180), 0);
function getFlatPos(vkey) {
let coplanar_pos = plane.projectPoint(Reusable.vec4.fromArray(mesh.vertices[vkey]), Reusable.vec5);
coplanar_pos.applyEuler(projection_euler);
return [coplanar_pos.x, coplanar_pos.z];
}
function verticesToEdges(vertices) {
return vertices.map((a, i) => ([a, vertices[i+1] || vertices[0]]));
}
function thingsInTri(...vertices) {
let flat_positions = vertices.map(getFlatPos);
for (let point of mid_points) {
if (vertices.includes(point.vkey)) continue;
let flat_point = getFlatPos(point.vkey);
if (pointInTriangle(flat_point, ...flat_positions)) {
return true;
}
}
for (let edge of mid_edges.concat(generated_edges)) {
if (sameMeshEdge(edge, vertices.slice(0, 2)) || sameMeshEdge(edge, vertices.slice(1, 3)) || sameMeshEdge(edge, [vertices[2], vertices[0]])) continue;
let edge_a = getFlatPos(edge[0]);
let edge_b = getFlatPos(edge[1]);
if (lineIntersectsTriangle(edge_a, edge_b, ...flat_positions)) {
return true;
}
}
return false;
}
function getCornerAngle(vertices, index) {
let vkey_a = vertices[index - 1] || vertices.last();
let vkey_b = vertices[index];
let vkey_c = vertices[index+1] || vertices[0];
let vec_a = Reusable.vec1.fromArray(mesh.vertices[vkey_a]);
let vec_b = Reusable.vec2.fromArray(mesh.vertices[vkey_b]);
let vec_c = Reusable.vec3.fromArray(mesh.vertices[vkey_c]);
let angle = vec_a.sub(vec_b).angleTo(vec_c.sub(vec_b));
return Math.radToDeg(angle);
}
function tryMakeQuad(vkey1, vkey2, vkey3, vkey4) {
if (!vkey1 || !vkey2 || !vkey3 || !vkey4) return;
let vertices = [vkey1, vkey2, vkey3, vkey4];
let face = new MeshFace(mesh, {vertices});
if (face.isConcave()) return;
let sorted_vertices = face.getSortedVertices();
// Diagonals
let diagonal_1 = [sorted_vertices[0], sorted_vertices[2]];
let diagonal_2 = [sorted_vertices[1], sorted_vertices[3]];
if (mid_edges.find(edge => sameMeshEdge(edge, diagonal_1) || sameMeshEdge(edge, diagonal_2))) {
return;
}
// Occupied edges
let edges = verticesToEdges(sorted_vertices);
let occupied_edge = edges.find(edge => {
let edge_key = getEdgeKey(edge);
if (covered_perimeter_edges[edge_key]) return true;
if (edge_face_connections[edge_key] >= 2) return true;
})
if (occupied_edge) return;
// Face exists
if (created_face_edgings.find(edging => {
return edging.allAre(vkey => sorted_vertices.includes(vkey))
})) {return;}
// Conflicts
if (thingsInTri(sorted_vertices[0], sorted_vertices[1], sorted_vertices[2])) return;
if (thingsInTri(sorted_vertices[0], sorted_vertices[2], sorted_vertices[3])) return;
if (thingsInTri(sorted_vertices[0], sorted_vertices[1], sorted_vertices[3])) return;
if (thingsInTri(sorted_vertices[1], sorted_vertices[2], sorted_vertices[3])) return;
// Corner angles
for (let i = 0; i < sorted_vertices.length; i++) {
let angle = getCornerAngle(sorted_vertices, i);
if (angle < 1 || angle > 178) return;
}
return face;
}
function tryMakeTri(vkey1, vkey2, vkey3) {
if (!vkey1 || !vkey2 || !vkey3) return;
let vertices = [vkey1, vkey2, vkey3];
// Face exists
if (created_face_edgings.find(edging => {
return vertices.allAre(vkey => edging.includes(vkey))
})) {return;}
// Conflicts
if (thingsInTri(vkey1, vkey2, vkey3)) return;
// Occupied edges
let edges = verticesToEdges(vertices);
let occupied_edge = edges.find(edge => {
let edge_key = getEdgeKey(edge);
if (covered_perimeter_edges[edge_key]) return true;
if (edge_face_connections[edge_key] >= 2) return true;
})
if (occupied_edge) return;
// Corner angles
for (let i = 0; i < vertices.length; i++) {
let angle = getCornerAngle(vertices, i);
if (angle < 2 || angle > 178) return;
}
let face = new MeshFace(mesh, {vertices});
return face;
}
function initFace(new_face) {
if (face.getAngleTo(new_face) > 90) {
new_face.invert();
}
for (let vkey of new_face.vertices) {
new_face.uv[vkey] = uv_data[vkey] ? uv_data[vkey].slice() : [0, 0];
}
new_face.texture = face.texture;
created_face_edgings.push(new_face.vertices);
let edges = new_face.getEdges();
for (let edge of edges) {
if (
!mid_edges.find(e2 => sameMeshEdge(edge, e2)) &&
!perimeter_edges.find(e2 => sameMeshEdge(edge, e2)) &&
!generated_edges.find(e2 => sameMeshEdge(edge, e2))
) {
generated_edges.push(edge);
}
let edge_key = getEdgeKey(edge);
if (!edge_face_connections[edge_key]) edge_face_connections[edge_key] = 0;
edge_face_connections[edge_key] += 1;
}
let fkey = mesh.addFaces(new_face)[0];
all_new_fkeys.push(fkey);
return fkey;
}
// Add faces from perimeter inwards
for (let edge of perimeter_edges) {
let edge_center = Reusable.vec2.fromArray(mesh.vertices[edge[0]].slice().V3_add(mesh.vertices[edge[1]])).divideScalar(2);
let sortByDistance = (a, b) => {
let a_vector = Reusable.vec5.fromArray(mesh.vertices[typeof a == 'string' ? a : a.vkey]);
let b_vector = Reusable.vec6.fromArray(mesh.vertices[typeof b == 'string' ? b : a.vkey]);
return a_vector.distanceToSquared(edge_center) - b_vector.distanceToSquared(edge_center);
}
let nearest_points = [
...mid_points.map(point => point.vkey).sort(sortByDistance),
...perimeter_vertices.filter(v => !edge.includes(v)).sort(sortByDistance)
];
let new_face = tryMakeQuad(edge[0], edge[1], nearest_points[0], nearest_points[1])
|| tryMakeQuad(edge[0], edge[1], nearest_points[0], nearest_points[2])
|| tryMakeQuad(edge[0], edge[1], nearest_points[1], nearest_points[2])
|| tryMakeQuad(edge[0], edge[1], nearest_points[0], nearest_points[3])
|| tryMakeQuad(edge[0], edge[1], nearest_points[1], nearest_points[3])
|| tryMakeQuad(edge[0], edge[1], nearest_points[2], nearest_points[3]);
let i = 0;
while (!new_face && nearest_points[i]) {
new_face = tryMakeTri(edge[0], edge[1], nearest_points[i])
i++;
}
if (new_face) {
initFace(new_face);
// Mark edges as occupied
covered_perimeter_edges[getEdgeKey(edge)] = true;
// Count faces per mid edge
let sorted_vertices = new_face.getSortedVertices();
for (let i = 0; i < sorted_vertices.length; i++) {
let edge1 = [sorted_vertices[i], sorted_vertices[i+1] || sorted_vertices[0]];
if (sameMeshEdge(edge1, edge)) continue
for (let edge2 of perimeter_edges) {
if (sameMeshEdge(edge1, edge2)) {
covered_perimeter_edges[getEdgeKey(edge1)] = true;
}
}
}
}
}
// Add missing faces between inner edges
for (let edge of mid_edges) {
let edge_key = getEdgeKey(edge);
let limiter = 0;
while (edge_face_connections[edge_key] != 2 && limiter < 5) {
let edge_center = Reusable.vec2.fromArray(mesh.vertices[edge[0]].slice().V3_add(mesh.vertices[edge[1]])).divideScalar(2);
let sortByDistance = (a, b) => {
let a_vector = Reusable.vec5.fromArray(mesh.vertices[typeof a == 'string' ? a : a.vkey]);
let b_vector = Reusable.vec6.fromArray(mesh.vertices[typeof b == 'string' ? b : a.vkey]);
return a_vector.distanceToSquared(edge_center) - b_vector.distanceToSquared(edge_center);
}
let nearest_vertices = mid_points.map(point => point.vkey).filter(v => !edge.includes(v)).concat(perimeter_vertices);
nearest_vertices.sort(sortByDistance);
let new_face = tryMakeQuad(edge[0], edge[1], nearest_vertices[0], nearest_vertices[1])
|| tryMakeQuad(edge[0], edge[1], nearest_vertices[0], nearest_vertices[2])
|| tryMakeQuad(edge[0], edge[1], nearest_vertices[1], nearest_vertices[2])
|| tryMakeQuad(edge[0], edge[1], nearest_vertices[0], nearest_vertices[3])
|| tryMakeQuad(edge[0], edge[1], nearest_vertices[1], nearest_vertices[3])
|| tryMakeQuad(edge[0], edge[1], nearest_vertices[2], nearest_vertices[3]);
let i = 0;
while (!new_face && nearest_vertices[i]) {
new_face = tryMakeTri(edge[0], edge[1], nearest_vertices[i])
i++;
}
if (new_face) {
initFace(new_face);
let edges = new_face.getEdges();
for (let edge1 of edges) {
let edge1_key = getEdgeKey(edge1);
let is_mid_edge = mid_edges.find(e => sameMeshEdge(e, edge1));
if (edge1_key != edge_key && !is_mid_edge && !perimeter_edges.find(e => sameMeshEdge(e, edge1))) {
mid_edges.push(edge1);
}
}
} else {
//console.error('Knife tool: Failed to find face for edge', edge, nearest_vertices);
break;
}
limiter++;
}
}
}
let selected_faces = all_new_fkeys.filter(fkey => mesh.faces[fkey].vertices.allAre(vkey => all_new_vkeys.includes(vkey)));
mesh.getSelectedFaces(true).replace(selected_faces);
mesh.getSelectedVertices(true).replace(all_new_vkeys);
mesh.getSelectedEdges(true).replace(all_new_edges);
Canvas.updateView({elements: [mesh], element_aspects: {geometry: true, uv: true, faces: true}, selection: true});
Undo.finishEdit('Use knife tool');
this.remove();
}
cancel() {
this.remove();
}
remove() {
this.mesh.mesh.remove(this.points_mesh);
this.mesh.mesh.remove(this.lines_mesh);
delete this.mesh
if (this.toast) this.toast.delete();
KnifeToolContext.current = null;
}
static current = null;
}
async function autoFixMeshEdit() {
let meshes = Mesh.selected;
@ -1000,6 +1522,41 @@ BARS.defineActions(function() {
BarItems.view_mode.onChange();
}
})
new Tool('knife_tool', {
icon: 'surgical',
transformerMode: 'hidden',
category: 'tools',
selectElements: true,
raycast_options: {
edges: true,
vertices: true,
},
modes: ['edit'],
condition: () => Modes.edit && Mesh.hasAny(),
onCanvasMouseMove(data) {
if (!KnifeToolContext.current && Mesh.selected[0] && Mesh.selected.length == 1) {
KnifeToolContext.current = new KnifeToolContext(Mesh.selected[0]);
}
if (KnifeToolContext.current) {
KnifeToolContext.current.hover(data);
}
},
onCanvasClick(data) {
if (!data) return;
if (!KnifeToolContext.current && data.element instanceof Mesh) {
KnifeToolContext.current = new KnifeToolContext(data.element);
}
let context = KnifeToolContext.current;
context.addPoint(data);
},
onSelect() {
},
onUnselect() {
if (KnifeToolContext.current) {
KnifeToolContext.current.apply();
}
}
})
new BarSelect('select_seam', {
options: {
auto: true,

View File

@ -1241,7 +1241,7 @@ new NodePreviewController(Mesh, {
mesh.outline.geometry.setAttribute('color', new THREE.Float32BufferAttribute(line_colors, 3));
mesh.outline.geometry.needsUpdate = true;
mesh.vertex_points.visible = Mode.selected.id == 'edit' && BarItems.selection_mode.value == 'vertex';
mesh.vertex_points.visible = (Mode.selected.id == 'edit' && BarItems.selection_mode.value == 'vertex') || Toolbox.selected.id == 'knife_tool';
this.dispatchEvent('update_selection', {element});
},

View File

@ -346,7 +346,8 @@ class Preview {
}
return this;
}
raycast(event) {
raycast(event, options = Toolbox.selected.raycast_options) {
if (!options) options = 0;
convertTouchEvent(event);
var canvas_offset = $(this.canvas).offset()
this.mouse.x = ((event.clientX - canvas_offset.left) / this.width) * 2 - 1;
@ -358,10 +359,10 @@ class Preview {
if (element.mesh && element.mesh.geometry && element.visibility && !element.locked) {
objects.push(element.mesh);
if (Modes.edit && element.selected) {
if (element.mesh.vertex_points && element.mesh.vertex_points.visible) {
if (element.mesh.vertex_points && (element.mesh.vertex_points.visible || options.vertices)) {
objects.push(element.mesh.vertex_points);
}
if (element instanceof Mesh && element.mesh.outline.visible && BarItems.selection_mode.value == 'edge') {
if (element instanceof Mesh && ((element.mesh.outline.visible && BarItems.selection_mode.value == 'edge') || options.edges)) {
objects.push(element.mesh.outline);
}
}
@ -379,11 +380,20 @@ class Preview {
}
})
}
var intersects = this.raycaster.intersectObjects( objects );
let intersects = this.raycaster.intersectObjects( objects );
if (intersects.length == 0) return false;
let mesh_gizmo = intersects.find(intersect => intersect.object.type == 'Points' || intersect.object.type == 'LineSegments');
let intersect = mesh_gizmo || intersects[0];
let depth_offset = Preview.selected.calculateControlScale(intersects[0].point);
for (let intersect of intersects) {
if (intersect.object.isLine) {
intersect.distance -= depth_offset;
} else if (intersect.object.isPoints) {
intersect.distance -= depth_offset * 1.4;
}
}
intersects.sort((a, b) => a.distance - b.distance);
let intersect = intersects[0];
let intersect_object = intersect.object;
if (intersect_object.isElement) {
@ -1026,8 +1036,9 @@ class Preview {
}
}
mousemove(event) {
let data;
if (Settings.get('highlight_cubes') || Toolbox.selected.brush?.size) {
var data = this.raycast(event);
data = this.raycast(event);
updateCubeHighlights(data && data.element);
if (Toolbox.selected.brush?.size && Settings.get('brush_cursor_3d')) {
@ -1109,6 +1120,10 @@ class Preview {
Canvas.brush_outline.quaternion.premultiply(world_quaternion);
}
}
if (Toolbox.selected.onCanvasMouseMove) {
if (!data) data = this.raycast(event);
Toolbox.selected.onCanvasMouseMove(data);
}
}
mouseup(event) {
this.showContextMenu(event);

View File

@ -590,6 +590,9 @@ function pointInTriangle(pt, v1, v2, v3) {
return !(has_neg && has_pos);
}
function lineIntersectsTriangle(l1, l2, v1, v2, v3) {
return intersectLines(l1, l2, v1, v2) || intersectLines(l1, l2, v2, v3) || intersectLines(l1, l2, v3, v1);
}
function cameraTargetToRotation(position, target) {
let spherical = new THREE.Spherical();

View File

@ -360,6 +360,8 @@
"message.meshes_and_box_uv": "Meshes are not compatible with Box UV. Go to File > Project... and switch to Per-face UV.",
"message.no_valid_elements": "No valid elements selected...",
"message.palette_locked": "The palette is locked",
"message.knife_tool.confirm": "Press %0 or click here to apply Knife Tool",
"message.knife_tool.skipped_face": "Cannot cut between different faces. Include the edges or vertices between them!",
"message.wireframe.enabled": "Wireframe view enabled",
"message.wireframe.disabled": "Wireframe view disabled",
@ -1184,6 +1186,8 @@
"action.rotate_tool.desc": "Tool to select and rotate elements",
"action.pivot_tool": "Pivot Tool",
"action.pivot_tool.desc": "Tool to change the pivot point of elements and bones",
"action.knife_tool": "Knife Tool",
"action.knife_tool.desc": "Tool to cut mesh faces into smaller pieces",
"action.seam_tool": "Seam Tool",
"action.seam_tool.desc": "Tool to define UV seams on mesh edges",
"action.pan_tool": "Pan Tool",