diff --git a/js/modeling/mesh_editing.js b/js/modeling/mesh_editing.js index f2a5d552..9619b23c 100644 --- a/js/modeling/mesh_editing.js +++ b/js/modeling/mesh_editing.js @@ -98,6 +98,186 @@ const ProportionalEdit = { } } + +async function autoFixMeshEdit() { + let meshes = Mesh.selected; + if (!meshes.length || !Modes.edit || BarItems.selection_mode.value == 'object') return; + + // Merge Vertices + let overlaps = {}; + let e = 0.004; + meshes.forEach(mesh => { + let mesh_overlaps = {}; + let vertices = mesh.getSelectedVertices(); + for (let vkey of vertices) { + let vertex = mesh.vertices[vkey]; + let matches = []; + for (let vkey2 in mesh.vertices) { + if (vkey2 == vkey || mesh_overlaps[vkey2]) continue; + let vertex2 = mesh.vertices[vkey2]; + let same_spot = Math.epsilon(vertex[0], vertex2[0], e) && Math.epsilon(vertex[1], vertex2[1], e) && Math.epsilon(vertex[2], vertex2[2], e); + if (same_spot) { + matches.push(vkey2); + } + } + if (matches.length) { + mesh_overlaps[vkey] = matches; + } + } + if (Object.keys(mesh_overlaps).length) overlaps[mesh.uuid] = mesh_overlaps; + }) + if (Object.keys(overlaps).length) { + await new Promise(resolve => {Blockbench.showMessageBox({ + title: 'message.auto_fix_mesh_edit.title', + message: 'message.auto_fix_mesh_edit.overlapping_vertices', + commands: { + merge: 'message.auto_fix_mesh_edit.merge_vertices', + revert: 'message.auto_fix_mesh_edit.revert' + }, + buttons: ['dialog.ignore'] + }, result => { + if (result == 'revert') { + Undo.undo(); + } else if (result == 'merge') { + + let meshes = Mesh.selected.filter(m => overlaps[m.uuid]); + Undo.initEdit({ elements: meshes }); + let merge_counter = 0; + let cluster_counter = 0; + for (let mesh_id in overlaps) { + let mesh = Mesh.selected.find(m => m.uuid == mesh_id); + let selected_vertices = mesh.getSelectedVertices(true); + for (let first_vertex in overlaps[mesh_id]) { + let other_vertices = overlaps[mesh_id][first_vertex]; + cluster_counter++; + + for (let vkey of other_vertices) { + for (let fkey in mesh.faces) { + let face = mesh.faces[fkey]; + let index = face.vertices.indexOf(vkey); + if (index === -1) continue; + + if (face.vertices.includes(first_vertex)) { + face.vertices.remove(vkey); + delete face.uv[vkey]; + if (face.vertices.length < 2) { + delete mesh.faces[fkey]; + } else if (face.vertices.length == 2) { + // Find face that overlaps the remaining edge + for (let fkey2 in mesh.faces) { + let face2 = mesh.faces[fkey2]; + if (face2.vertices.length >= 3 && face2.vertices.includes(face.vertices[0]) && face2.vertices.includes(face.vertices[1])) { + delete mesh.faces[fkey]; + } + } + } + } else { + let uv = face.uv[vkey]; + face.vertices.splice(index, 1, first_vertex); + face.uv[first_vertex] = uv; + delete face.uv[vkey]; + } + } + delete mesh.vertices[vkey]; + selected_vertices.remove(vkey); + merge_counter++; + } + } + } + Undo.finishEdit('Auto-merge vertices') + Canvas.updateView({elements: meshes, element_aspects: {geometry: true, uv: true, faces: true}, selection: true}); + Blockbench.showQuickMessage(tl('message.merged_vertices', [merge_counter, cluster_counter]), 2000); + } + resolve(); + })}) + } + + // Concave quads + let concave_faces = {}; + meshes.forEach(mesh => { + let selected_faces = mesh.getSelectedFaces(); + let selected_vertices = mesh.getSelectedVertices(); + let concave_faces_mesh = []; + for (let fkey in mesh.faces) { + let face = mesh.faces[fkey]; + if (face.vertices.length != 4) continue; + // Check if face is selected or touches selection + if (!selected_faces.includes(fkey) && !face.vertices.find(vkey => selected_vertices.includes(vkey))) continue; + //let vertices = face.getSortedVertices().map(vkey => mesh.vertices[vkey]); + let concave = face.isConcave(); + if (concave != false) { + concave_faces_mesh.push([fkey, concave]); + } + } + if (concave_faces_mesh.length) concave_faces[mesh.uuid] = concave_faces_mesh; + }) + + if (Object.keys(concave_faces).length) { + await new Promise(resolve => {Blockbench.showMessageBox({ + title: 'message.auto_fix_mesh_edit.title', + message: 'message.auto_fix_mesh_edit.concave_quads', + commands: { + split: 'message.auto_fix_mesh_edit.split_quads', + revert: 'message.auto_fix_mesh_edit.revert' + }, + buttons: ['dialog.ignore'] + }, result => { + if (result == 'revert') { + Undo.undo(); + } else if (result == 'split') { + + let meshes = Mesh.selected.filter(m => concave_faces[m.uuid]); + Undo.initEdit({ elements: meshes }); + for (let mesh of meshes) { + for (let [fkey, concave_vkey] of concave_faces[mesh.uuid]) { + let face = mesh.faces[fkey]; + + // Find the edge that needs to be connected + let sorted_vertices = face.getSortedVertices(); + let edges = [ + [sorted_vertices[0], sorted_vertices[1]], + [sorted_vertices[0], sorted_vertices[2]], + [sorted_vertices[0], sorted_vertices[3]], + [sorted_vertices[1], sorted_vertices[2]], + [sorted_vertices[1], sorted_vertices[3]], + [sorted_vertices[2], sorted_vertices[3]], + ] + edges = edges.filter(edge => { + for (let fkey2 in mesh.faces) { + if (fkey2 == fkey) continue; + let face2 = mesh.faces[fkey2]; + if (face2.vertices.includes(edge[0]) && face2.vertices.includes(edge[1])) { + return false; + } + } + return true; + }) + let off_corners = edges.find(edge => !edge.includes(concave_vkey)) + if (!off_corners) return; + + let new_face = new MeshFace(mesh, face); + new_face.vertices.remove(off_corners[0]); + delete new_face.uv[off_corners[0]]; + + face.vertices.remove(off_corners[1]); + delete face.uv[off_corners[1]]; + + let [face_key] = mesh.addFaces(new_face); + UVEditor.selected_faces.push(face_key); + if (face.getAngleTo(new_face) > 90) { + new_face.invert(); + } + } + + } + Undo.finishEdit('Auto-fix concave quads'); + Canvas.updateView({elements: meshes, element_aspects: {geometry: true, uv: true, faces: true}, selection: true}); + } + resolve(); + })}) + } +} + SharedActions.add('delete', { condition: () => Modes.edit && Prop.active_panel == 'preview' && Mesh.selected[0] && Project.mesh_selection[Mesh.selected[0].uuid], run() { diff --git a/js/modeling/transform.js b/js/modeling/transform.js index 40da2445..e7c5ae49 100644 --- a/js/modeling/transform.js +++ b/js/modeling/transform.js @@ -123,6 +123,7 @@ function moveElementsRelative(difference, index, event) { //Multiple updateSelection(); Undo.finishEdit('Move elements') + autoFixMeshEdit() } //Rotate function rotateSelected(axis, steps) { @@ -223,6 +224,7 @@ function mirrorSelected(axis) { }) updateSelection() Undo.finishEdit('Flip selection') + autoFixMeshEdit() } } @@ -490,6 +492,7 @@ const Vertexsnap = { } Canvas.updateView(update_options); Undo.finishEdit('Use vertex snap'); + autoFixMeshEdit() Vertexsnap.step1 = true; } } @@ -1039,6 +1042,7 @@ BARS.defineActions(function() { }, onAfter: function() { Undo.finishEdit('Change element position') + autoFixMeshEdit() } }) new NumSlider('slider_pos_y', { @@ -1059,6 +1063,7 @@ BARS.defineActions(function() { }, onAfter: function() { Undo.finishEdit('Change element position') + autoFixMeshEdit() } }) new NumSlider('slider_pos_z', { @@ -1079,6 +1084,7 @@ BARS.defineActions(function() { }, onAfter: function() { Undo.finishEdit('Change element position') + autoFixMeshEdit() } }) let slider_vector_pos = [BarItems.slider_pos_x, BarItems.slider_pos_y, BarItems.slider_pos_z]; @@ -1118,6 +1124,7 @@ BARS.defineActions(function() { }, onAfter: function() { Undo.finishEdit('Change element size') + autoFixMeshEdit() } }) new NumSlider('slider_size_y', { @@ -1142,6 +1149,7 @@ BARS.defineActions(function() { }, onAfter: function() { Undo.finishEdit('Change element size') + autoFixMeshEdit() } }) new NumSlider('slider_size_z', { @@ -1166,6 +1174,7 @@ BARS.defineActions(function() { }, onAfter: function() { Undo.finishEdit('Change element size') + autoFixMeshEdit() } }) let slider_vector_size = [BarItems.slider_size_x, BarItems.slider_size_y, BarItems.slider_size_z]; diff --git a/js/modeling/transform_gizmo.js b/js/modeling/transform_gizmo.js index d4122477..57873f8c 100644 --- a/js/modeling/transform_gizmo.js +++ b/js/modeling/transform_gizmo.js @@ -1618,6 +1618,7 @@ Undo.finishEdit('Move selection') } } + autoFixMeshEdit() updateSelection() } else if (Modes.id === 'animate' && scope.keyframes && scope.keyframes.length && keep_changes) { diff --git a/js/outliner/mesh.js b/js/outliner/mesh.js index 56188e0a..681875c8 100644 --- a/js/outliner/mesh.js +++ b/js/outliner/mesh.js @@ -196,6 +196,41 @@ class MeshFace extends Face { } return vertices; } + isConcave() { + if (this.vertices.length < 4) return false; + let {vec1, vec2, vec3, vec4} = Reusable; + let normal_vec = vec1.fromArray(this.getNormal(true)); + let plane = new THREE.Plane().setFromNormalAndCoplanarPoint( + normal_vec, + vec2.fromArray(this.mesh.vertices[this.vertices[0]]) + ) + let sorted_vertices = this.getSortedVertices(); + let rot = cameraTargetToRotation([0, 0, 0], normal_vec.toArray()); + let e = new THREE.Euler(Math.degToRad(rot[1] - 90), Math.degToRad(rot[0] + 180), 0); + + let flat_positions = sorted_vertices.map(vkey => { + let coplanar_pos = plane.projectPoint(vec3.fromArray(this.mesh.vertices[vkey]), vec4); + coplanar_pos.applyEuler(e); + return [coplanar_pos.x, coplanar_pos.z]; + }) + let angles = []; + for (let i = 0; i < sorted_vertices.length; i++) { + let a = flat_positions[i]; + let b = flat_positions[(i+1) % 4]; + let direction = b.slice().V2_subtract(a); + let angle = Math.atan2(direction[1], direction[0]); + angles.push(angle); + } + for (let i = 0; i < sorted_vertices.length; i++) { + let a = angles[i]; + let b = angles[(i+1) % 4]; + let difference = Math.trimRad(b - a); + if (difference > 0) { + return sorted_vertices[(i+1) % 4]; + } + } + return false; + } getEdges() { let vertices = this.getSortedVertices(); if (vertices.length == 2) { diff --git a/js/util/math_util.js b/js/util/math_util.js index a025e51e..f62573ba 100644 --- a/js/util/math_util.js +++ b/js/util/math_util.js @@ -50,6 +50,9 @@ Math.epsilon = function(a, b, epsilon = 0.001) { Math.trimDeg = function(a) { return (a+180*15)%360-180 } +Math.trimRad = function(a) { + return (a+Math.PI*15)%(Math.PI*2)-Math.PI +} Math.isPowerOfTwo = function(x) { return (x > 1) && ((x & (x - 1)) == 0); } diff --git a/lang/en.json b/lang/en.json index 93cdf2b7..f8deaa73 100644 --- a/lang/en.json +++ b/lang/en.json @@ -378,6 +378,13 @@ "message.small_face_dimensions.message": "The selection contains faces that are smaller than 1 unit in one direction. The Box UV mapping system considers any faces below that threshold as 0 pixels wide. The texture on those faces therefore may not work correctly.", "message.small_face_dimensions.face_uv": "The current format supports per-face UV maps which can handle small face dimensions. Go to \"File\" > \"Project...\" and change \"UV Mode\" to \"Per-face UV\".", + "message.auto_fix_mesh_edit.title": "Auto Fix", + "message.auto_fix_mesh_edit.revert": "Revert Edit", + "message.auto_fix_mesh_edit.overlapping_vertices": "Certain vertices have been moved into the same spot. How would you like to proceed?", + "message.auto_fix_mesh_edit.concave_quads": "One or more quad faces are now concave, which can cause issues in UV-mapping and rendering. Blockbench can automatically fix it for you:", + "message.auto_fix_mesh_edit.merge_vertices": "Merge Vertices", + "message.auto_fix_mesh_edit.split_quads": "Split Quads", + "message.merged_vertices": "Found and merged %0 vertices in %1 locations", "message.preview_scene_load_failed": "Failed to load preview scene. Check your internet connection.",