function sameMeshEdge(edge_a, edge_b) { return edge_a.equals(edge_b) || (edge_a[0] == edge_b[1] && edge_a[1] == edge_b[0]) } const ProportionalEdit = { vertex_weights: {}, calculateWeights(mesh) { if (!BarItems.proportional_editing.value) return; let selected_vertices = mesh.getSelectedVertices(); let {range, falloff, selection} = StateMemory.proportional_editing_options; let linear_distance = selection == 'linear'; let all_mesh_connections; if (!linear_distance) { all_mesh_connections = {}; for (let fkey in mesh.faces) { let face = mesh.faces[fkey]; face.getEdges().forEach(edge => { if (!all_mesh_connections[edge[0]]) { all_mesh_connections[edge[0]] = [edge[1]]; } else { all_mesh_connections[edge[0]].safePush(edge[1]); } if (!all_mesh_connections[edge[1]]) { all_mesh_connections[edge[1]] = [edge[0]]; } else { all_mesh_connections[edge[1]].safePush(edge[0]); } }) } } ProportionalEdit.vertex_weights[mesh.uuid] = {}; for (let vkey in mesh.vertices) { if (selected_vertices.includes(vkey)) continue; let distance = Infinity; if (linear_distance) { // Linear Distance selected_vertices.forEach(vkey2 => { let pos1 = mesh.vertices[vkey]; let pos2 = mesh.vertices[vkey2]; let distance_square = Math.pow(pos1[0] - pos2[0], 2) + Math.pow(pos1[1] - pos2[1], 2) + Math.pow(pos1[2] - pos2[2], 2); if (distance_square < distance) { distance = distance_square; } }) distance = Math.sqrt(distance); } else { // Connection Distance let found_match_depth = 0; let scanned = []; let frontier = [vkey]; depth_crawler: for (let depth = 1; depth <= range; depth++) { let new_frontier = []; for (let vkey1 of frontier) { let connections = all_mesh_connections[vkey1]?.filter(vkey2 => !scanned.includes(vkey2)); if (!connections || connections.length == 0) continue; scanned.push(...connections); new_frontier.push(...connections); } for (let vkey2 of new_frontier) { if (selected_vertices.includes(vkey2)) { found_match_depth = depth; break depth_crawler; } } frontier = new_frontier; } if (found_match_depth) { distance = found_match_depth; } } if (distance > range) continue; let blend = 1 - (distance / (linear_distance ? range : range+1)); switch (falloff) { case 'hermite_spline': blend = Math.hermiteBlend(blend); break; case 'constant': blend = 1; break; } ProportionalEdit.vertex_weights[mesh.uuid][vkey] = blend; } }, editVertices(mesh, per_vertex) { if (!BarItems.proportional_editing.value) return; let selected_vertices = mesh.getSelectedVertices(); for (let vkey in mesh.vertices) { if (selected_vertices.includes(vkey)) continue; let blend = ProportionalEdit.vertex_weights[mesh.uuid][vkey]; per_vertex(vkey, blend); } } } 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.mesh_3d = 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_3d.add(this.points_mesh); this.mesh_3d.add(this.lines_mesh); this.unselect_listener = Blockbench.on('unselect_project', context => { if (this == KnifeToolContext.current) { this.remove(); } }) } 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 } data.element.mesh.worldToLocal(point.position); // 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); let subline_len = a_p.dot(a_b) / a_b.dot(a_b); if (data.event.shiftKey || Pressing.overrides.shift) { subline_len = Math.round(subline_len * 4) / 4; } point.position.copy(point_a).addScaledVector(a_b, subline_len); point.snapped = true; } // Snap to existing points? let pos = this.mesh_3d.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); } else if (data.event && (data.event.shiftKey || Pressing.overrides.shift) && point.fkey) { let face = this.mesh.faces[point.fkey]; point.position.fromArray(face.getCenter()); } 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 || !Mesh.all.includes(this.mesh)) { 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() { if (this.mesh_3d) { this.mesh_3d.remove(this.points_mesh); this.mesh_3d.remove(this.lines_mesh); } delete this.mesh; delete this.mesh_3d; if (this.toast) this.toast.delete(); this.unselect_listener.delete(); KnifeToolContext.current = null; } static current = null; } 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) { let selected_faces = mesh.getSelectedFaces(true); 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); selected_faces.safePush(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() { let meshes = Mesh.selected.slice(); Undo.initEdit({elements: meshes, outliner: true}) Mesh.selected.forEach(mesh => { let selected_vertices = mesh.getSelectedVertices(); let selected_edges = mesh.getSelectedEdges(); let selected_faces = mesh.getSelectedFaces(); if (BarItems.selection_mode.value == 'face' && selected_faces.length < Object.keys(mesh.faces).length) { let affected_vertices = []; selected_faces.forEach(fkey => { affected_vertices.safePush(...mesh.faces[fkey].vertices); delete mesh.faces[fkey]; }) affected_vertices.forEach(vertex_key => { let used = false; for (let key in mesh.faces) { let face = mesh.faces[key]; if (face.vertices.includes(vertex_key)) used = true; } if (!used) { delete mesh.vertices[vertex_key]; } }) } else if (BarItems.selection_mode.value == 'edge') { for (let key in mesh.faces) { let face = mesh.faces[key]; let sorted_vertices = face.getSortedVertices(); let has_edge = sorted_vertices.find((vkey_a, i) => { let vkey_b = sorted_vertices[i+1] || sorted_vertices[0]; let edge = [vkey_a, vkey_b]; return selected_edges.find(edge2 => sameMeshEdge(edge, edge2)) }) if (has_edge) { delete mesh.faces[key]; } } selected_edges.forEachReverse(edge => { edge.forEach(vkey => { let used = false; for (let key in mesh.faces) { let face = mesh.faces[key]; if (face.vertices.includes(vkey)) used = true; } if (!used) { delete mesh.vertices[vkey]; selected_vertices.remove(vkey); selected_edges.remove(edge); } }) }) } else if (BarItems.selection_mode.value == 'vertex' && selected_vertices.length < Object.keys(mesh.vertices).length) { selected_vertices.forEach(vkey => { for (let key in mesh.faces) { let face = mesh.faces[key]; if (!face.vertices.includes(vkey)) continue; if (face.vertices.length > 2) { let initial_normal; if (face.vertices.length == 4) { initial_normal = face.getNormal(); } face.vertices.remove(vkey); delete face.uv[vkey]; if (face.vertices.length == 3 && face.getAngleTo(initial_normal) > 90) { face.invert(); } if (face.vertices.length == 2) { for (let fkey2 in mesh.faces) { if (fkey2 != key && !face.vertices.find(vkey => !mesh.faces[fkey2].vertices.includes(vkey))) { delete mesh.faces[key]; break; } } } } else { delete mesh.faces[key]; } } delete mesh.vertices[vkey]; }) } else { meshes.remove(mesh); mesh.remove(false); } }) Undo.finishEdit('Delete mesh part') Canvas.updateView({elements: meshes, selection: true, element_aspects: {geometry: true, faces: true, uv: meshes.length > 0}}) } }) SharedActions.add('select_all', { condition: () => Modes.edit && Mesh.selected.length && Mesh.selected.length === Outliner.selected.length && BarItems.selection_mode.value !== 'object', priority: 1, run() { let selection_mode = BarItems.selection_mode.value; if (selection_mode == 'vertex') { let unselect = Mesh.selected[0].getSelectedVertices().length == Object.keys(Mesh.selected[0].vertices).length; Mesh.selected.forEach(mesh => { if (unselect) { mesh.getSelectedVertices(true).empty(); } else { mesh.getSelectedVertices(true).replace(Object.keys(mesh.vertices)); } }) } else if (selection_mode == 'edge') { let unselect = Mesh.selected[0].getSelectedVertices().length == Object.keys(Mesh.selected[0].vertices).length; Mesh.selected.forEach(mesh => { if (unselect) { mesh.getSelectedVertices(true).empty(); mesh.getSelectedEdges(true).empty(); } else { mesh.getSelectedVertices(true).replace(Object.keys(mesh.vertices)); let edges = mesh.getSelectedEdges(true); for (let fkey in mesh.faces) { let face = mesh.faces[fkey]; let f_vertices = face.getSortedVertices(); f_vertices.forEach((vkey_a, i) => { let edge = [vkey_a, (f_vertices[i+1] || f_vertices[0])]; if (edges.find(edge2 => sameMeshEdge(edge2, edge))) return; edges.push(edge); }) } } }) } else { let unselect = Mesh.selected[0].getSelectedFaces().length == Object.keys(Mesh.selected[0].faces).length; Mesh.selected.forEach(mesh => { if (unselect) { delete Project.mesh_selection[mesh.uuid]; } else { mesh.getSelectedVertices(true).replace(Object.keys(mesh.vertices)); mesh.getSelectedFaces(true).replace(Object.keys(mesh.faces)); } }) } updateSelection(); } }) SharedActions.add('unselect_all', { condition: () => Modes.edit && Mesh.selected.length && Mesh.selected.length === Outliner.selected.length && BarItems.selection_mode.value !== 'object', priority: 1, run() { Mesh.selected.forEach(mesh => { delete Project.mesh_selection[mesh.uuid]; }) updateSelection(); } }) SharedActions.add('invert_selection', { condition: () => Modes.edit && Mesh.selected.length && Mesh.selected.length === Outliner.selected.length && BarItems.selection_mode.value !== 'object', priority: 1, run() { let selection_mode = BarItems.selection_mode.value; if (selection_mode == 'vertex') { Mesh.selected.forEach(mesh => { let selected = mesh.getSelectedVertices(); let now_selected = Object.keys(mesh.vertices).filter(vkey => !selected.includes(vkey)); mesh.getSelectedVertices(true).replace(now_selected); }) } else if (selection_mode == 'edge') { Mesh.selected.forEach(mesh => { let old_edges = mesh.getSelectedEdges().slice(); let vertices = mesh.getSelectedVertices(true).empty(); let edges = mesh.getSelectedEdges(true).empty(); for (let fkey in mesh.faces) { let face = mesh.faces[fkey]; let f_vertices = face.getSortedVertices(); f_vertices.forEach((vkey_a, i) => { let edge = [vkey_a, (f_vertices[i+1] || f_vertices[0])]; if (!old_edges.find(edge2 => sameMeshEdge(edge2, edge))) { edges.push(edge); vertices.safePush(edge[0], edge[1]); } }) } }) } else { Mesh.selected.forEach(mesh => { let old_faces = mesh.getSelectedFaces().slice(); let vertices = mesh.getSelectedVertices(true).empty(); let faces = mesh.getSelectedFaces(true).empty(); for (let fkey in mesh.faces) { if (!old_faces.includes(fkey)) { let face = mesh.faces[fkey]; faces.push(fkey); vertices.safePush(...face.vertices); } } }) } updateSelection(); } }) BARS.defineActions(function() { let add_mesh_dialog = new Dialog({ id: 'add_primitive', title: 'action.add_mesh', form: { shape: {label: 'dialog.add_primitive.shape', type: 'select', options: { cuboid: 'dialog.add_primitive.shape.cube', rounded_cuboid: 'dialog.add_primitive.shape.rounded_cuboid', pyramid: 'dialog.add_primitive.shape.pyramid', plane: 'dialog.add_primitive.shape.plane', circle: 'dialog.add_primitive.shape.circle', cylinder: 'dialog.add_primitive.shape.cylinder', tube: 'dialog.add_primitive.shape.tube', cone: 'dialog.add_primitive.shape.cone', sphere: 'dialog.add_primitive.shape.sphere', torus: 'dialog.add_primitive.shape.torus', }}, diameter: {label: 'dialog.add_primitive.diameter', type: 'number', value: 16}, align_edges: {label: 'dialog.add_primitive.align_edges', type: 'checkbox', value: true, condition: ({shape}) => !['cuboid', 'rounded_cuboid', 'pyramid', 'plane'].includes(shape)}, height: {label: 'dialog.add_primitive.height', type: 'number', value: 8, condition: ({shape}) => ['cylinder', 'cone', 'cuboid', 'rounded_cuboid', 'pyramid', 'tube'].includes(shape)}, sides: {label: 'dialog.add_primitive.sides', type: 'number', value: 12, min: 3, max: 48, condition: ({shape}) => ['cylinder', 'cone', 'circle', 'torus', 'sphere', 'tube'].includes(shape)}, minor_diameter: {label: 'dialog.add_primitive.minor_diameter', type: 'number', value: 4, condition: ({shape}) => ['torus', 'tube'].includes(shape)}, minor_sides: {label: 'dialog.add_primitive.minor_sides', type: 'number', value: 8, min: 2, max: 32, condition: ({shape}) => ['torus'].includes(shape)}, edge_size: {label: 'dialog.add_primitive.edge_size', type: 'number', value: 2, condition: ({shape}) => ['rounded_cuboid'].includes(shape)}, }, onConfirm(result) { let original_selection_group = Group.selected && Group.selected.uuid; function runEdit(amended, result) { let elements = []; if (original_selection_group && !Group.selected) { let group_to_select = Group.all.find(g => g.uuid == original_selection_group); if (group_to_select) { Group.selected = group_to_select; } } Undo.initEdit({elements, selection: true}, amended); let mesh = new Mesh({ name: result.shape, vertices: {} }); let group = getCurrentGroup(); if (group) { mesh.addTo(group) mesh.color = group.color; } let diameter_factor = result.align_edges ? 1 / Math.cos(Math.PI/result.sides) : 1; let off_ang = result.align_edges ? 0.5 : 0; if (result.shape == 'circle') { let vertex_keys = mesh.addVertices([0, 0, 0]); let [m] = vertex_keys; for (let i = 0; i < result.sides; i++) { let x = Math.sin(((i+off_ang) / result.sides) * Math.PI * 2) * result.diameter/2 * diameter_factor; let z = Math.cos(((i+off_ang) / result.sides) * Math.PI * 2) * result.diameter/2 * diameter_factor; vertex_keys.push(...mesh.addVertices([x, 0, z])); } for (let i = 0; i < result.sides; i++) { let [a, b] = vertex_keys.slice(i+2, i+2 + 2); if (!a) { b = vertex_keys[2]; a = vertex_keys[1]; } else if (!b) { b = vertex_keys[1]; } mesh.addFaces(new MeshFace( mesh, {vertices: [a, b, m]} )); } } if (result.shape == 'cone') { let vertex_keys = mesh.addVertices([0, 0, 0], [0, result.height, 0]); let [m0, m1] = vertex_keys; for (let i = 0; i < result.sides; i++) { let x = Math.sin(((i+off_ang) / result.sides) * Math.PI * 2) * result.diameter/2 * diameter_factor; let z = Math.cos(((i+off_ang) / result.sides) * Math.PI * 2) * result.diameter/2 * diameter_factor; vertex_keys.push(...mesh.addVertices([x, 0, z])); } for (let i = 0; i < result.sides; i++) { let [a, b] = vertex_keys.slice(i+2, i+2 + 2); if (!b) { b = vertex_keys[2]; } mesh.addFaces( new MeshFace( mesh, {vertices: [b, a, m0]} ), new MeshFace( mesh, {vertices: [a, b, m1]} ) ); } } if (result.shape == 'cylinder') { let vertex_keys = mesh.addVertices([0, 0, 0], [0, result.height, 0]); let [m0, m1] = vertex_keys; for (let i = 0; i < result.sides; i++) { let x = Math.sin(((i+off_ang) / result.sides) * Math.PI * 2) * result.diameter/2 * diameter_factor; let z = Math.cos(((i+off_ang) / result.sides) * Math.PI * 2) * result.diameter/2 * diameter_factor; vertex_keys.push(...mesh.addVertices([x, 0, z], [x, result.height, z])); } for (let i = 0; i < result.sides; i++) { let [a, b, c, d] = vertex_keys.slice(2*i+2, 2*i+2 + 4); if (!c) { c = vertex_keys[2]; d = vertex_keys[3]; } mesh.addFaces( new MeshFace( mesh, {vertices: [c, a, m0]}), new MeshFace( mesh, {vertices: [a, c, d, b]} ), new MeshFace( mesh, {vertices: [b, d, m1]} ) ); } } if (result.shape == 'tube') { let vertex_keys = []; let outer_r = result.diameter/2 * diameter_factor; let inner_r = (outer_r - result.minor_diameter/2) * diameter_factor; for (let i = 0; i < result.sides; i++) { let x = Math.sin(((i+off_ang) / result.sides) * Math.PI * 2); let z = Math.cos(((i+off_ang) / result.sides) * Math.PI * 2); vertex_keys.push(...mesh.addVertices( [x * outer_r, 0, z * outer_r], [x * outer_r, result.height, z * outer_r], [x * inner_r, 0, z * inner_r], [x * inner_r, result.height, z * inner_r], )); } for (let i = 0; i < result.sides; i++) { let [a1, b1, c1, d1, a2, b2, c2, d2] = vertex_keys.slice(4*i, 4*i + 8); if (!a2) { a2 = vertex_keys[0]; b2 = vertex_keys[1]; c2 = vertex_keys[2]; d2 = vertex_keys[3]; } if (a1 && b1 && c1 && d1 && a2 && b2 && c2 && d2) { mesh.addFaces( new MeshFace( mesh, {vertices: [a1, a2, b2, b1]} ), new MeshFace( mesh, {vertices: [d1, d2, c2, c1]} ), new MeshFace( mesh, {vertices: [c1, c2, a2, a1]} ), new MeshFace( mesh, {vertices: [b1, b2, d2, d1]} ), ); } } } if (result.shape == 'torus') { let rings = []; for (let i = 0; i < result.sides; i++) { let circle_x = Math.sin(((i+off_ang) / result.sides) * Math.PI * 2); let circle_z = Math.cos(((i+off_ang) / result.sides) * Math.PI * 2); let vertices = []; for (let j = 0; j < result.minor_sides; j++) { let slice_x = Math.sin((j / result.minor_sides) * Math.PI * 2) * result.minor_diameter/2*diameter_factor; let x = circle_x * (result.diameter/2*diameter_factor + slice_x) let y = Math.cos((j / result.minor_sides) * Math.PI * 2) * result.minor_diameter/2*diameter_factor; let z = circle_z * (result.diameter/2*diameter_factor + slice_x) vertices.push(...mesh.addVertices([x, y, z])); } rings.push(vertices); } for (let i = 0; i < result.sides; i++) { let this_ring = rings[i]; let next_ring = rings[i+1] || rings[0]; for (let j = 0; j < result.minor_sides; j++) { mesh.addFaces(new MeshFace( mesh, {vertices: [ this_ring[j+1] || this_ring[0], next_ring[j+1] || next_ring[0], this_ring[j], next_ring[j], ]} )); } } } if (result.shape == 'sphere') { let rings = []; let sides = Math.round(result.sides/2)*2; let [bottom] = mesh.addVertices([0, -result.diameter/2, 0]); let [top] = mesh.addVertices([0, result.diameter/2, 0]); for (let i = 0; i < result.sides; i++) { let circle_x = Math.sin(((i+off_ang) / result.sides) * Math.PI * 2); let circle_z = Math.cos(((i+off_ang) / result.sides) * Math.PI * 2); let vertices = []; for (let j = 1; j < (sides/2); j++) { let slice_x = Math.sin((j / sides) * Math.PI * 2) * result.diameter/2 * diameter_factor; let x = circle_x * slice_x let y = Math.cos((j / sides) * Math.PI * 2) * result.diameter/2; let z = circle_z * slice_x vertices.push(...mesh.addVertices([x, y, z])); } rings.push(vertices); } for (let i = 0; i < result.sides; i++) { let this_ring = rings[i]; let next_ring = rings[i+1] || rings[0]; for (let j = 0; j < (sides/2); j++) { if (j == 0) { mesh.addFaces(new MeshFace( mesh, {vertices: [ this_ring[j], next_ring[j], top ]} )); } else if (!this_ring[j]) { mesh.addFaces(new MeshFace( mesh, {vertices: [ next_ring[j-1], this_ring[j-1], bottom ]} )); } else { mesh.addFaces(new MeshFace( mesh, {vertices: [ this_ring[j], next_ring[j], this_ring[j-1], next_ring[j-1], ]} )); } } } } if (result.shape == 'cuboid') { let r = result.diameter/2; let h = result.height; mesh.addVertices([r, h, r], [r, h, -r], [r, 0, r], [r, 0, -r], [-r, h, r], [-r, h, -r], [-r, 0, r], [-r, 0, -r]); let vertex_keys = Object.keys(mesh.vertices); mesh.addFaces( new MeshFace( mesh, {vertices: [vertex_keys[0], vertex_keys[2], vertex_keys[1], vertex_keys[3]]} ), // East new MeshFace( mesh, {vertices: [vertex_keys[4], vertex_keys[5], vertex_keys[6], vertex_keys[7]]} ), // West new MeshFace( mesh, {vertices: [vertex_keys[0], vertex_keys[1], vertex_keys[4], vertex_keys[5]]} ), // Up new MeshFace( mesh, {vertices: [vertex_keys[2], vertex_keys[6], vertex_keys[3], vertex_keys[7]]} ), // Down new MeshFace( mesh, {vertices: [vertex_keys[0], vertex_keys[4], vertex_keys[2], vertex_keys[6]]} ), // South new MeshFace( mesh, {vertices: [vertex_keys[1], vertex_keys[3], vertex_keys[5], vertex_keys[7]]} ), // North ); } if (result.shape == 'rounded_cuboid') { let s = result.edge_size; let rs = result.diameter/2 - s; let r = result.diameter/2; let h = result.height; let hs = result.height - s; let up = mesh.addVertices( [rs, h, rs], // 0 [rs, h, -rs], // 1 [-rs, h, rs], // 2 [-rs, h, -rs], // 3 ) let down = mesh.addVertices( [rs, 0, rs], // 4 [rs, 0, -rs], // 5 [-rs, 0, rs], // 6 [-rs, 0, -rs], // 7 ) let west = mesh.addVertices( [-r, s, rs], // 8 [-r, hs, rs], // 9 [-r, s, -rs], // 10 [-r, hs, -rs], // 11 ) let east = mesh.addVertices( [r, s, rs], // 12 [r, hs, rs], // 13 [r, s, -rs], // 14 [r, hs, -rs], // 15 ) let north = mesh.addVertices( [rs, s, -r], // 16 [rs, hs, -r], // 17 [-rs, s, -r], // 18 [-rs, hs, -r], // 19 ) let south = mesh.addVertices( [rs, s, r], // 20 [rs, hs, r], // 21 [-rs, s, r], // 22 [-rs, hs, r] // 23 ) mesh.addFaces( new MeshFace( mesh, {vertices: [ east[1], east[0], east[3], east[2] ]} ), // East new MeshFace( mesh, {vertices: [ west[0], west[1], west[3], west[2] ]} ), // West new MeshFace( mesh, {vertices: [ up[0], up[1], up[3], up[2] ]} ), // Up new MeshFace( mesh, {vertices: [ down[1], down[0], down[3], down[2] ]} ), // Down new MeshFace( mesh, {vertices: [ south[0], south[1], south[3], south[2] ]} ), // South new MeshFace( mesh, {vertices: [ north[1], north[0], north[3], north[2] ]} ), // North ); mesh.addFaces( new MeshFace( mesh, {vertices: [up[1], up[0], east[1], east[3]]} ), // E Up new MeshFace( mesh, {vertices: [up[2], up[3], west[1], west[3]]} ), // W Up new MeshFace( mesh, {vertices: [up[0], up[2], south[1], south[3]]} ), // S Up new MeshFace( mesh, {vertices: [up[3], up[1], north[1], north[3]]} ), // N Up new MeshFace( mesh, {vertices: [down[0], down[1], east[0], east[2]]} ), // E Down new MeshFace( mesh, {vertices: [down[3], down[2], west[0], west[2]]} ), // W Down new MeshFace( mesh, {vertices: [down[2], down[0], south[0], south[2]]} ), // S Down new MeshFace( mesh, {vertices: [down[1], down[3], north[0], north[2]]} ), // N Down new MeshFace( mesh, {vertices: [north[0], north[1], east[2], east[3]]} ), // NE new MeshFace( mesh, {vertices: [south[1], south[0], east[0], east[1]]} ), // SE new MeshFace( mesh, {vertices: [north[3], north[2], west[2], west[3]]} ), // NW new MeshFace( mesh, {vertices: [south[2], south[3], west[0], west[1]]} ) // SW ); mesh.addFaces( new MeshFace( mesh, {vertices: [down[0], east[0], south[0]]} ), // Down1 new MeshFace( mesh, {vertices: [down[2], south[2], west[0]]} ), // Down2 new MeshFace( mesh, {vertices: [down[1], north[0], east[2]]} ), // Down3 new MeshFace( mesh, {vertices: [down[3], west[2], north[2]]} ), // Down4 new MeshFace( mesh, {vertices: [up[0], south[1], east[1]]} ), // Up1 new MeshFace( mesh, {vertices: [up[2], west[1], south[3]]} ), // Up2 new MeshFace( mesh, {vertices: [up[1], east[3], north[1]]} ), // Up3 new MeshFace( mesh, {vertices: [up[3], north[3], west[3]]} ) // Up4 ); } if (result.shape == 'pyramid') { let r = result.diameter/2; let h = result.height; mesh.addVertices([0, h, 0], [r, 0, r], [r, 0, -r], [-r, 0, r], [-r, 0, -r]); let vertex_keys = Object.keys(mesh.vertices); mesh.addFaces( new MeshFace( mesh, {vertices: [vertex_keys[1], vertex_keys[3], vertex_keys[2], vertex_keys[4]]} ), // Down new MeshFace( mesh, {vertices: [vertex_keys[1], vertex_keys[2], vertex_keys[0]]} ), // east new MeshFace( mesh, {vertices: [vertex_keys[3], vertex_keys[1], vertex_keys[0]]} ), // south new MeshFace( mesh, {vertices: [vertex_keys[2], vertex_keys[4], vertex_keys[0]]} ), // north new MeshFace( mesh, {vertices: [vertex_keys[4], vertex_keys[3], vertex_keys[0]]} ), // west ); } if (result.shape == 'plane') { let r = result.diameter/2; mesh.addVertices([r, 0, r], [r, 0, -r], [-r, 0, r], [-r, 0, -r]); let vertex_keys = Object.keys(mesh.vertices); mesh.addFaces( new MeshFace( mesh, {vertices: [vertex_keys[0], vertex_keys[1], vertex_keys[3], vertex_keys[2]]} ) ); } if (Texture.all.length && Format.single_texture) { for (var face in mesh.faces) { mesh.faces[face].texture = Texture.getDefault().uuid } UVEditor.loadData() } if (Format.bone_rig) { if (group) { var pos1 = group.origin.slice() mesh.extend({ origin: pos1.slice() }) } } elements.push(mesh); mesh.init() if (Group.selected) Group.selected.unselect() mesh.select() UVEditor.setAutoSize(null, true, Object.keys(mesh.faces)); Undo.finishEdit('Add primitive'); Blockbench.dispatchEvent( 'add_mesh', {object: mesh} ) Vue.nextTick(function() { if (settings.create_rename.value) { mesh.rename() } }) } runEdit(false, result); Undo.amendEdit({ diameter: {label: 'dialog.add_primitive.diameter', type: 'number', value: result.diameter, interval_type: 'position'}, height: {label: 'dialog.add_primitive.height', type: 'number', value: result.height, condition: ['cylinder', 'cone', 'cuboid', 'rounded_cuboid', 'pyramid', 'tube'].includes(result.shape), interval_type: 'position'}, sides: {label: 'dialog.add_primitive.sides', type: 'number', value: result.sides, min: 3, max: 48, condition: ['cylinder', 'cone', 'circle', 'torus', 'sphere', 'tube'].includes(result.shape)}, minor_diameter: {label: 'dialog.add_primitive.minor_diameter', type: 'number', value: result.minor_diameter, condition: ['torus', 'tube'].includes(result.shape), interval_type: 'position'}, minor_sides: {label: 'dialog.add_primitive.minor_sides', type: 'number', value: result.minor_sides, min: 2, max: 32, condition: ['torus'].includes(result.shape)}, edge_size: {label: 'dialog.add_primitive.edge_size', type: 'number', value: result.edge_size, condition: ['rounded_cuboid'].includes(result.shape)}, }, form => { Object.assign(result, form); runEdit(true, result); }) } }) new Action('add_mesh', { icon: 'fa-gem', category: 'edit', condition: {modes: ['edit'], method: () => (Format.meshes)}, click: function () { add_mesh_dialog.show(); } }) let previous_selection_mode = 'object'; new BarSelect('selection_mode', { options: { object: {name: true, icon: 'far.fa-gem'}, cluster: {name: true, icon: 'link'}, face: {name: true, icon: 'far.fa-square'}, edge: {name: true, icon: 'pen_size_3'}, vertex: {name: true, icon: 'fiber_manual_record'}, }, icon_mode: true, condition: () => Modes.edit && Mesh.hasAny(), onChange({value}) { if (value === previous_selection_mode) return; if (value === 'object') { Mesh.selected.forEach(mesh => { delete Project.mesh_selection[mesh.uuid]; }) } else if (value === 'face') { Mesh.selected.forEach(mesh => { let selected_faces = mesh.getSelectedFaces(true); selected_faces.empty(); for (let fkey in mesh.faces) { let face = mesh.faces[fkey]; if (face.isSelected(fkey)) { selected_faces.safePush(fkey); } } }) } if ((value == 'face' || value == 'cluster') && ['edge', 'vertex'].includes(previous_selection_mode)) { Mesh.selected.forEach(mesh => { let vertices = mesh.getSelectedVertices(); let faces = mesh.getSelectedFaces(true); for (let fkey in mesh.faces) { let face = mesh.faces[fkey]; if (face.vertices.allAre(vkey => vertices.includes(vkey))) { faces.safePush(fkey); } } }) } if (value == 'edge') { Mesh.selected.forEach(mesh => { let edges = mesh.getSelectedEdges(true); edges.empty(); }) } if (value == 'edge' && ['face', 'cluster'].includes(previous_selection_mode)) { Mesh.selected.forEach(mesh => { let edges = mesh.getSelectedEdges(true); let faces = mesh.getSelectedFaces(true); faces.forEach(fkey => { let face = mesh.faces[fkey]; let vertices = face.getSortedVertices(); vertices.forEach((vkey_a, i) => { let edge = [vkey_a, (vertices[i+1] || vertices[0])]; if (!edges.find(edge2 => sameMeshEdge(edge2, edge))) { edges.push(edge); } }) }) faces.empty(); }) } if (value == 'edge' && ['vertex', 'cluster'].includes(previous_selection_mode)) { Mesh.selected.forEach(mesh => { let edges = mesh.getSelectedEdges(true); let vertices = mesh.getSelectedVertices(); if (!vertices.length) return; for (let fkey in mesh.faces) { let face = mesh.faces[fkey]; let f_vertices = face.getSortedVertices(); f_vertices.forEach((vkey_a, i) => { let edge = [vkey_a, (f_vertices[i+1] || f_vertices[0])]; if (!vertices.includes(edge[0]) || !vertices.includes(edge[1])) return; if (edges.find(edge2 => sameMeshEdge(edge2, edge))) return; edges.push(edge); }) } }) } if (value == 'vertex' && ['face', 'cluster'].includes(previous_selection_mode)) { Mesh.selected.forEach(mesh => { let faces = mesh.getSelectedFaces(true); faces.empty(); }) } if (value == 'vertex' && ['edge', 'cluster'].includes(previous_selection_mode)) { Mesh.selected.forEach(mesh => { let edges = mesh.getSelectedEdges(true); edges.empty(); }) } updateSelection(); previous_selection_mode = value; } }) let seam_timeout; new Tool('seam_tool', { icon: 'content_cut', transformerMode: 'hidden', toolbar: 'seam_tool', category: 'tools', selectElements: true, modes: ['edit'], condition: () => Modes.edit && Mesh.hasAny(), onCanvasClick(data) { if (!seam_timeout) { seam_timeout = setTimeout(() => { seam_timeout = null; }, 200) } else { clearTimeout(seam_timeout); seam_timeout = null; BarItems.select_seam.trigger(); } }, onSelect: function() { BarItems.selection_mode.set('edge'); BarItems.view_mode.set('solid'); BarItems.view_mode.onChange(); }, onUnselect: function() { BarItems.selection_mode.set('object'); BarItems.view_mode.set('textured'); BarItems.view_mode.onChange(); } }) new Tool('knife_tool', { icon: 'surgical', transformerMode: 'hidden', category: 'tools', selectElements: true, cursor: 'crosshair', 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, divide: true, join: true, }, condition: () => Modes.edit && Mesh.hasAny(), onChange({value}) { if (value == 'auto') value = null; Undo.initEdit({elements: Mesh.selected}); Mesh.selected.forEach(mesh => { let selected_edges = mesh.getSelectedEdges(); selected_edges.forEach(edge => { mesh.setSeam(edge, value); }) Mesh.preview_controller.updateSelection(mesh); }) Undo.finishEdit('Set mesh seam'); } }) new Action('create_face', { icon: 'fas.fa-draw-polygon', category: 'edit', keybind: new Keybind({key: 'f', shift: true}), condition: {modes: ['edit'], features: ['meshes'], method: () => (Mesh.selected[0] && Mesh.selected[0].getSelectedVertices().length > 1)}, click() { let vec1 = new THREE.Vector3(), vec2 = new THREE.Vector3(), vec3 = new THREE.Vector3(), vec4 = new THREE.Vector3(); Undo.initEdit({elements: Mesh.selected}); let faces_to_autouv = []; Mesh.selected.forEach(mesh => { let selected_vertices = mesh.getSelectedVertices(); let selected_faces = mesh.getSelectedFaces(true); selected_faces.empty(); if (selected_vertices.length >= 2 && selected_vertices.length <= 4) { let reference_face; let reference_face_strength = 0; for (let key in mesh.faces) { let face = mesh.faces[key]; let match_strength = face.vertices.filter(vkey => selected_vertices.includes(vkey)).length; if (match_strength > reference_face_strength) { reference_face = face; reference_face_strength = match_strength; } if (match_strength == face.vertices.length) { delete mesh.faces[key]; } } // Split face if ( reference_face && (selected_vertices.length == 2 || selected_vertices.length == 3) && reference_face.vertices.length == 4 && reference_face.vertices.filter(vkey => selected_vertices.includes(vkey)).length == selected_vertices.length ) { let sorted_vertices = reference_face.getSortedVertices(); let unselected_vertices = sorted_vertices.filter(vkey => !selected_vertices.includes(vkey)); let side_index_diff = Math.abs(sorted_vertices.indexOf(selected_vertices[0]) - sorted_vertices.indexOf(selected_vertices[1])); if (side_index_diff != 1 || selected_vertices.length == 3) { let new_face = new MeshFace(mesh, reference_face); new_face.vertices.remove(unselected_vertices[0]); delete new_face.uv[unselected_vertices[0]]; let reference_corner_vertex = unselected_vertices[1] || sorted_vertices[sorted_vertices.indexOf(unselected_vertices[0]) + 2] || sorted_vertices[sorted_vertices.indexOf(unselected_vertices[0]) - 2]; reference_face.vertices.remove(reference_corner_vertex); delete reference_face.uv[reference_corner_vertex]; let [face_key] = mesh.addFaces(new_face); selected_faces.push(face_key); if (reference_face.getAngleTo(new_face) > 90) { new_face.invert(); } } } else { let new_face = new MeshFace(mesh, { vertices: selected_vertices, texture: reference_face?.texture, } ); let [face_key] = mesh.addFaces(new_face); selected_faces.push(face_key); faces_to_autouv.push(face_key); // Correct direction if (selected_vertices.length > 2) { // find face with shared line to compare let fixed_via_face; for (let key in mesh.faces) { let face = mesh.faces[key]; let common = face.vertices.filter(vertex_key => selected_vertices.includes(vertex_key)) if (common.length == 2) { let old_vertices = face.getSortedVertices(); let new_vertices = new_face.getSortedVertices(); let index_diff = old_vertices.indexOf(common[0]) - old_vertices.indexOf(common[1]); let new_index_diff = new_vertices.indexOf(common[0]) - new_vertices.indexOf(common[1]); if (index_diff == 1 - face.vertices.length) index_diff = 1; if (new_index_diff == 1 - new_face.vertices.length) new_index_diff = 1; if (Math.abs(index_diff) == 1 && Math.abs(new_index_diff) == 1) { if (index_diff == new_index_diff) { new_face.invert(); } fixed_via_face = true; break; } } } // If no face available, orient based on camera orientation if (!fixed_via_face) { let normal = new THREE.Vector3().fromArray(new_face.getNormal()); normal.applyQuaternion(mesh.mesh.getWorldQuaternion(new THREE.Quaternion())) let cam_direction = Preview.selected.camera.getWorldDirection(new THREE.Vector3()); let angle = normal.angleTo(cam_direction); if (angle < Math.PI/2) { new_face.invert(); } } } } } else if (selected_vertices.length > 4) { let reference_face; for (let key in mesh.faces) { let face = mesh.faces[key]; if (!reference_face && face.vertices.find(vkey => selected_vertices.includes(vkey))) { reference_face = face; } } let vertices = selected_vertices.slice(); let v1 = vec1.fromArray(mesh.vertices[vertices[1]].slice().V3_subtract(mesh.vertices[vertices[0]])); let v2 = vec2.fromArray(mesh.vertices[vertices[2]].slice().V3_subtract(mesh.vertices[vertices[0]])); let normal = v2.cross(v1); let plane = new THREE.Plane().setFromNormalAndCoplanarPoint( normal, new THREE.Vector3().fromArray(mesh.vertices[vertices[0]]) ) let center = [0, 0]; let vertex_uvs = {}; vertices.forEach((vkey) => { let coplanar_pos = plane.projectPoint(vec3.fromArray(mesh.vertices[vkey]), vec4); let q = Reusable.quat1.setFromUnitVectors(normal, THREE.NormalY) coplanar_pos.applyQuaternion(q); vertex_uvs[vkey] = [ Math.roundTo(coplanar_pos.x, 4), Math.roundTo(coplanar_pos.z, 4), ] center[0] += vertex_uvs[vkey][0]; center[1] += vertex_uvs[vkey][1]; }) center[0] /= vertices.length; center[1] /= vertices.length; vertices.forEach(vkey => { vertex_uvs[vkey][0] -= center[0]; vertex_uvs[vkey][1] -= center[1]; vertex_uvs[vkey][2] = Math.atan2(vertex_uvs[vkey][0], vertex_uvs[vkey][1]); }) vertices.sort((a, b) => vertex_uvs[a][2] - vertex_uvs[b][2]); let start_index = 0; while (start_index < vertices.length) { let face_vertices = vertices.slice(start_index, start_index+4); vertices.push(face_vertices[0]); let new_face = new MeshFace(mesh, {vertices: face_vertices, texture: reference_face.texture}); let [face_key] = mesh.addFaces(new_face); selected_faces.push(face_key); if (face_vertices.length < 4) break; start_index += 3; } } }) UVEditor.setAutoSize(null, true, faces_to_autouv); Undo.finishEdit('Create mesh face') Canvas.updateView({elements: Mesh.selected, element_aspects: {geometry: true, uv: true, faces: true}, selection: true}) } }) new Action('convert_to_mesh', { icon: 'fa-gem', category: 'edit', condition: {modes: ['edit'], features: ['meshes'], method: () => (Cube.selected.length)}, click() { Undo.initEdit({elements: Cube.selected, outliner: true}); let new_meshes = []; Cube.selected.forEach(cube => { let mesh = new Mesh({ name: cube.name, color: cube.color, origin: cube.origin, rotation: cube.rotation, vertices: [] }) let rotation_euler = new THREE.Euler(0, 0, 0, 'ZYX').fromArray(cube.rotation.map(Math.degToRad)); rotation_euler.reorder('XYZ'); mesh.rotation.V3_set(rotation_euler.toArray().map(r => Math.roundTo(Math.radToDeg(r), 4))); var adjustedFrom = cube.from.slice(); var adjustedTo = cube.to.slice(); adjustFromAndToForInflateAndStretch(adjustedFrom, adjustedTo, cube); for (let i = 0; i < adjustedFrom.length; i++) { adjustedFrom[i] -= cube.origin[i]; adjustedTo[i] -= cube.origin[i] } let vertex_keys = [ mesh.addVertices([adjustedTo[0], adjustedTo[1], adjustedTo[2] ])[0], mesh.addVertices([adjustedTo[0], adjustedTo[1], adjustedFrom[2] ])[0], mesh.addVertices([adjustedTo[0], adjustedFrom[1], adjustedTo[2] ])[0], mesh.addVertices([adjustedTo[0], adjustedFrom[1], adjustedFrom[2] ])[0], mesh.addVertices([adjustedFrom[0], adjustedTo[1], adjustedTo[2] ])[0], mesh.addVertices([adjustedFrom[0], adjustedTo[1], adjustedFrom[2] ])[0], mesh.addVertices([adjustedFrom[0], adjustedFrom[1], adjustedTo[2] ])[0], mesh.addVertices([adjustedFrom[0], adjustedFrom[1], adjustedFrom[2] ])[0], ]; let unused_vkeys = vertex_keys.slice(); function addFace(direction, vertices) { let cube_face = cube.faces[direction]; if (cube_face.texture === null) return; let uv_points = [ [cube_face.uv[0], cube_face.uv[1]], [cube_face.uv[2], cube_face.uv[1]], [cube_face.uv[2], cube_face.uv[3]], [cube_face.uv[0], cube_face.uv[3]] ]; let rotation = cube_face.rotation || 0; while (rotation > 0) { rotation -= 90; uv_points.splice(0, 0, uv_points.pop()); } let uv = { [vertices[0]]: uv_points[1], [vertices[1]]: uv_points[0], [vertices[2]]: uv_points[2], [vertices[3]]: uv_points[3], }; mesh.addFaces( new MeshFace( mesh, { vertices, uv, texture: cube_face.texture, } )); vertices.forEach(vkey => unused_vkeys.remove(vkey)); } addFace('east', [vertex_keys[1], vertex_keys[0], vertex_keys[3], vertex_keys[2]]); addFace('west', [vertex_keys[4], vertex_keys[5], vertex_keys[6], vertex_keys[7]]); addFace('up', [vertex_keys[1], vertex_keys[5], vertex_keys[0], vertex_keys[4]]); // 4 0 5 1 addFace('down', [vertex_keys[2], vertex_keys[6], vertex_keys[3], vertex_keys[7]]); addFace('south', [vertex_keys[0], vertex_keys[4], vertex_keys[2], vertex_keys[6]]); addFace('north', [vertex_keys[5], vertex_keys[1], vertex_keys[7], vertex_keys[3]]); unused_vkeys.forEach(vkey => { delete mesh.vertices[vkey]; }) mesh.sortInBefore(cube).init(); new_meshes.push(mesh); selected.push(mesh); cube.remove(); }) updateSelection(); Undo.finishEdit('Convert cubes to meshes', {elements: new_meshes, outliner: true}); } }) new Action('apply_mesh_rotation', { icon: 'published_with_changes', category: 'edit', condition: {modes: ['edit'], features: ['meshes'], method: () => (Mesh.selected.length)}, click() { let vec = new THREE.Vector3(); Undo.initEdit({elements: Mesh.selected}); Mesh.selected.forEach(mesh => { let rotation = mesh.mesh.rotation; for (let vkey in mesh.vertices) { vec.fromArray(mesh.vertices[vkey]); vec.applyEuler(rotation); mesh.vertices[vkey].V3_set(vec.x, vec.y, vec.z); } mesh.rotation.V3_set(0, 0, 0); }) Undo.finishEdit('Apply mesh rotation') Canvas.updateView({elements: Mesh.selected, element_aspects: {geometry: true, transform: true}, selection: true}) } }) new Action('invert_face', { icon: 'flip_to_back', category: 'edit', condition: {modes: ['edit'], features: ['meshes'], method: () => (Mesh.selected[0] && Mesh.selected[0].getSelectedFaces().length)}, click() { Undo.initEdit({elements: Mesh.selected}); Mesh.selected.forEach(mesh => { for (let fkey in mesh.faces) { let face = mesh.faces[fkey]; if (face.isSelected(fkey)) { face.invert(); } } }) Undo.finishEdit('Invert mesh faces'); Canvas.updateView({elements: Mesh.selected, element_aspects: {geometry: true, uv: true, faces: true}}); } }) new Action('switch_face_crease', { icon: 'signal_cellular_off', category: 'edit', condition: {modes: ['edit'], features: ['meshes'], method: () => (Mesh.selected[0] && Mesh.selected[0].getSelectedFaces().find(fkey => Mesh.selected[0].faces[fkey].vertices.length == 4))}, click() { Undo.initEdit({elements: Mesh.selected}); Mesh.selected.forEach(mesh => { for (let fkey in mesh.faces) { let face = mesh.faces[fkey]; if (face.vertices.length == 4 && face.isSelected(fkey)) { let new_vertices = face.getSortedVertices().slice(); new_vertices.push(new_vertices.shift()); face.vertices.replace(new_vertices); } } }) Undo.finishEdit('Switch mesh face crease'); Canvas.updateView({elements: Mesh.selected, element_aspects: {geometry: true, uv: true, faces: true}}); } }) new Action('extrude_mesh_selection', { icon: 'upload', category: 'edit', keybind: new Keybind({key: 'e', shift: true}), condition: {modes: ['edit'], features: ['meshes'], method: () => (Mesh.selected[0] && Mesh.selected[0].getSelectedVertices().length)}, click() { function runEdit(amended, extend = 1) { Undo.initEdit({elements: Mesh.selected, selection: true}, amended); Mesh.selected.forEach(mesh => { let original_vertices = mesh.getSelectedVertices().slice(); let selected_edges = mesh.getSelectedEdges(true); let new_vertices; let new_face_keys = []; let selected_face_keys = mesh.getSelectedFaces(); let selected_faces = selected_face_keys.map(fkey => mesh.faces[fkey]); let combined_direction; selected_faces.forEach(face => { original_vertices.safePush(...face.vertices); }) selected_edges.forEach(edge => { original_vertices.safePush(...edge); }) if (original_vertices.length >= 3 && !selected_faces.length) { let [a, b, c] = original_vertices.slice(0, 3).map(vkey => mesh.vertices[vkey].slice()); let normal = new THREE.Vector3().fromArray(a.V3_subtract(c)); normal.cross(new THREE.Vector3().fromArray(b.V3_subtract(c))).normalize(); let face; for (let fkey in mesh.faces) { let face2 = mesh.faces[fkey]; let face_selected_vertices = face2.vertices.filter(vkey => original_vertices.includes(vkey)); if (face_selected_vertices.length >= 2 && face_selected_vertices.length < face2.vertices.length && face2.vertices.length > 2) { face = face2; break; } } if (face) { let selected_corner = mesh.vertices[face.vertices.find(vkey => original_vertices.includes(vkey))]; let opposite_corner = mesh.vertices[face.vertices.find(vkey => !original_vertices.includes(vkey))]; let face_geo_dir = opposite_corner.slice().V3_subtract(selected_corner); if (Reusable.vec1.fromArray(face_geo_dir).angleTo(normal) < 1) { normal.negate(); } } combined_direction = normal.toArray(); } new_vertices = mesh.addVertices(...original_vertices.map(key => { let vector = mesh.vertices[key].slice(); let direction; let count = 0; selected_faces.forEach(face => { if (face.vertices.includes(key)) { count++; if (!direction) { direction = face.getNormal(true); } else { direction.V3_add(face.getNormal(true)); } } }) if (count > 1) { direction.V3_divide(count); } if (!direction) { let match; let match_level = 0; let match_count = 0; for (let key in mesh.faces) { let face = mesh.faces[key]; let matches = face.vertices.filter(vkey => original_vertices.includes(vkey)); if (match_level < matches.length) { match_level = matches.length; match_count = 1; match = face; } else if (match_level === matches.length) { match_count++; } if (match_level == 3) break; } if (match_level < 3 && match_count > 2 && original_vertices.length > 2) { // If multiple faces connect to the line, there is no point in choosing one for the normal // Instead, construct the normal between the first 2 selected vertices direction = combined_direction; } else if (match) { let difference = new THREE.Vector3(); let signs_done = []; match.vertices.forEach(vkey => { let sign = original_vertices.includes(vkey) ? 1 : -1; difference.x += mesh.vertices[vkey][0] * sign; difference.y += mesh.vertices[vkey][1] * sign; difference.z += mesh.vertices[vkey][2] * sign; signs_done.push(sign); }) direction = difference.normalize().toArray(); } else if (match) { // perpendicular edge, currently unused direction = match.getNormal(true); } else { direction = [0, 1, 0]; } } vector.V3_add(direction.map(v => v * extend)); return vector; })) Project.mesh_selection[mesh.uuid].vertices.replace(new_vertices); // Move Faces selected_faces.forEach(face => { face.vertices.forEach((key, index) => { face.vertices[index] = new_vertices[original_vertices.indexOf(key)]; let uv = face.uv[key]; delete face.uv[key]; face.uv[face.vertices[index]] = uv; }) }) // Create extra quads on sides let remaining_vertices = new_vertices.slice(); selected_faces.forEach((face, face_index) => { let vertices = face.getSortedVertices(); vertices.forEach((a, i) => { let b = vertices[i+1] || vertices[0]; if (vertices.length == 2 && i) return; // Only create one quad when extruding line if (selected_faces.find(f => f != face && f.vertices.includes(a) && f.vertices.includes(b))) return; let new_face = new MeshFace(mesh, mesh.faces[selected_face_keys[face_index]]).extend({ vertices: [ b, a, original_vertices[new_vertices.indexOf(a)], original_vertices[new_vertices.indexOf(b)], ] }); let [face_key] = mesh.addFaces(new_face); new_face_keys.push(face_key); remaining_vertices.remove(a); remaining_vertices.remove(b); }) if (vertices.length == 2) delete mesh.faces[selected_face_keys[face_index]]; }) // Create Face between extruded edges let new_faces = []; selected_edges.forEach(edge => { let face, sorted_vertices; for (let fkey in mesh.faces) { let face2 = mesh.faces[fkey]; let vertices = face2.getSortedVertices(); if (vertices.includes(edge[0]) && vertices.includes(edge[1])) { face = face2; sorted_vertices = vertices; break; } } if (sorted_vertices[0] == edge[0] && sorted_vertices[1] != edge[1]) { edge.reverse(); } let [a, b] = edge.map(vkey => new_vertices[original_vertices.indexOf(vkey)]); let [c, d] = edge; let new_face = new MeshFace(mesh, face).extend({ vertices: [a, b, c, d] }); let [face_key] = mesh.addFaces(new_face); new_face_keys.push(face_key); new_faces.push(new_face); remaining_vertices.remove(a); remaining_vertices.remove(b); }) // Create line between points remaining_vertices.forEach(a => { let b = original_vertices[new_vertices.indexOf(a)] let b_in_face = false; mesh.forAllFaces(face => { if (face.vertices.includes(b)) b_in_face = true; }) if (selected_faces.find(f => f.vertices.includes(a)) && !b_in_face) { // Remove line if in the middle of other faces delete mesh.vertices[b]; } else { let new_face = new MeshFace(mesh, { vertices: [b, a] }); mesh.addFaces(new_face); } }) // Update edge selection selected_edges.forEach(edge => { edge.forEach((vkey, i) => { edge[i] = new_vertices[original_vertices.indexOf(vkey)]; }); }) UVEditor.setAutoSize(null, true, new_face_keys); }) Undo.finishEdit('Extrude mesh selection'); Canvas.updateView({elements: Mesh.selected, element_aspects: {geometry: true, uv: true, faces: true}, selection: true}); } runEdit(); Undo.amendEdit({ extend: {type: 'number', value: 1, label: 'edit.extrude_mesh_selection.extend', interval_type: 'position'}, }, form => { runEdit(true, form.extend); }) } }) new Action('solidify_mesh_selection', { icon: 'bottom_panel_open', category: 'edit', condition: {modes: ['edit'], features: ['meshes'], method: () => (Mesh.selected[0] && Mesh.selected[0].getSelectedFaces().length)}, click() { function runEdit(amended, extend = 1) { Undo.initEdit({elements: Mesh.selected, selection: true}, amended); Mesh.selected.forEach(mesh => { let original_vertices = []; let new_vertices; let new_face_keys = []; let selected_face_keys = mesh.getSelectedFaces(true); let selected_faces = selected_face_keys.map(fkey => mesh.faces[fkey]); let combined_direction; selected_faces.forEach(face => { original_vertices.safePush(...face.vertices); }) // Calculate direction if (original_vertices.length >= 3 && !selected_faces.length) { let [a, b, c] = original_vertices.slice(0, 3).map(vkey => mesh.vertices[vkey].slice()); let normal = new THREE.Vector3().fromArray(a.V3_subtract(c)); normal.cross(new THREE.Vector3().fromArray(b.V3_subtract(c))).normalize(); let face; for (let fkey in mesh.faces) { let face2 = mesh.faces[fkey]; let face_selected_vertices = face2.vertices.filter(vkey => original_vertices.includes(vkey)); if (face_selected_vertices.length >= 2 && face_selected_vertices.length < face2.vertices.length && face2.vertices.length > 2) { face = face2; break; } } if (face) { let selected_corner = mesh.vertices[face.vertices.find(vkey => original_vertices.includes(vkey))]; let opposite_corner = mesh.vertices[face.vertices.find(vkey => !original_vertices.includes(vkey))]; let face_geo_dir = opposite_corner.slice().V3_subtract(selected_corner); if (Reusable.vec1.fromArray(face_geo_dir).angleTo(normal) < 1) { normal.negate(); } } combined_direction = normal.toArray(); } new_vertices = mesh.addVertices(...original_vertices.map(key => { let vector = mesh.vertices[key].slice(); let direction; let count = 0; selected_faces.forEach(face => { if (face.vertices.includes(key)) { count++; if (!direction) { direction = face.getNormal(true); } else { direction.V3_add(face.getNormal(true)); } } }) if (count > 1) { direction.V3_divide(count); } if (!direction) { let match; let match_level = 0; let match_count = 0; for (let key in mesh.faces) { let face = mesh.faces[key]; let matches = face.vertices.filter(vkey => original_vertices.includes(vkey)); if (match_level < matches.length) { match_level = matches.length; match_count = 1; match = face; } else if (match_level === matches.length) { match_count++; } if (match_level == 3) break; } if (match_level < 3 && match_count > 2 && original_vertices.length > 2) { // If multiple faces connect to the line, there is no point in choosing one for the normal // Instead, construct the normal between the first 2 selected vertices direction = combined_direction; } else if (match) { let difference = new THREE.Vector3(); let signs_done = []; match.vertices.forEach(vkey => { let sign = original_vertices.includes(vkey) ? 1 : -1; difference.x += mesh.vertices[vkey][0] * sign; difference.y += mesh.vertices[vkey][1] * sign; difference.z += mesh.vertices[vkey][2] * sign; signs_done.push(sign); }) direction = difference.normalize().toArray(); } else if (match) { // perpendicular edge, currently unused direction = match.getNormal(true); } else { direction = [0, 1, 0]; } } vector.V3_add(direction.map(v => v * extend)); return vector; })) Project.mesh_selection[mesh.uuid].vertices.replace(new_vertices); // Duplicate faces selected_faces.forEach(face => { // Copy face and invert let face_copy = new MeshFace(mesh, face); let [new_face_key] = mesh.addFaces(face_copy); selected_face_keys.push(new_face_key); face_copy.invert(); // Move original face to new spot face.vertices.forEach((key, index) => { face.vertices[index] = new_vertices[original_vertices.indexOf(key)]; let uv = face.uv[key]; delete face.uv[key]; face.uv[face.vertices[index]] = uv; }) }) // Create extra quads on sides let remaining_vertices = new_vertices.slice(); selected_faces.forEach((face, face_index) => { let vertices = face.getSortedVertices(); vertices.forEach((a, i) => { let b = vertices[i+1] || vertices[0]; if (vertices.length == 2 && i) return; // Only create one quad when extruding line if (selected_faces.find(f => f != face && f.vertices.includes(a) && f.vertices.includes(b))) return; let new_face = new MeshFace(mesh, mesh.faces[selected_face_keys[face_index]]).extend({ vertices: [ b, a, original_vertices[new_vertices.indexOf(a)], original_vertices[new_vertices.indexOf(b)], ] }); let [face_key] = mesh.addFaces(new_face); selected_face_keys.push(face_key); new_face_keys.push(face_key); remaining_vertices.remove(a); remaining_vertices.remove(b); }) if (vertices.length == 2) delete mesh.faces[selected_face_keys[face_index]]; }) UVEditor.setAutoSize(null, true, new_face_keys); }) Undo.finishEdit('Solidify mesh selection'); Canvas.updateView({elements: Mesh.selected, element_aspects: {geometry: true, uv: true, faces: true}, selection: true}); } runEdit(); Undo.amendEdit({ thickness: {type: 'number', value: 1, label: 'edit.solidify_mesh_selection.thickness', interval_type: 'position'}, }, form => { runEdit(true, form.thickness); }) } }) new Action('inset_mesh_selection', { icon: 'fa-compress-arrows-alt', category: 'edit', keybind: new Keybind({key: 'i', shift: true}), condition: {modes: ['edit'], features: ['meshes'], method: () => (Mesh.selected[0] && Mesh.selected[0].getSelectedVertices().length >= 3)}, click() { function runEdit(amended, offset = 50) { Undo.initEdit({elements: Mesh.selected, selection: true}, amended); Mesh.selected.forEach(mesh => { let original_vertices = mesh.getSelectedVertices(); if (original_vertices.length < 3) return; original_vertices = original_vertices.slice(); let new_vertices; let selected_face_keys = mesh.getSelectedFaces(); let selected_faces = selected_face_keys.map(fkey => mesh.faces[fkey]); let modified_face_keys = selected_face_keys.slice(); new_vertices = mesh.addVertices(...original_vertices.map(vkey => { let vector = mesh.vertices[vkey].slice(); affected_faces = selected_faces.filter(face => { return face.vertices.includes(vkey) }) if (affected_faces.length == 0) return; let inset = [0, 0, 0]; if (affected_faces.length == 3 || affected_faces.length == 1) { affected_faces.sort((a, b) => { let ax = 0; a.vertices.forEach(vkey => { ax += affected_faces.filter(face => face.vertices.includes(vkey)).length; }) let bx = 0; b.vertices.forEach(vkey => { bx += affected_faces.filter(face => face.vertices.includes(vkey)).length; }) return bx - ax; }) affected_faces[0].vertices.forEach(vkey2 => { inset.V3_add(mesh.vertices[vkey2]); }) inset.V3_divide(affected_faces[0].vertices.length); vector = vector.map((v, i) => Math.lerp(v, inset[i], offset/100)); } if (affected_faces.length == 2) { let vkey2 = affected_faces[0].vertices.find(_vkey => _vkey != vkey && affected_faces[1].vertices.includes(_vkey)); vector = vector.map((v, i) => Math.lerp(v, mesh.vertices[vkey2][i], offset/200)); } return vector; }).filter(vec => vec instanceof Array)) if (!new_vertices.length) return; Project.mesh_selection[mesh.uuid].vertices.replace(new_vertices); // Move Faces selected_faces.forEach(face => { face.vertices.forEach((key, index) => { face.vertices[index] = new_vertices[original_vertices.indexOf(key)]; let uv = face.uv[key]; delete face.uv[key]; face.uv[face.vertices[index]] = uv; }) }) // Create extra quads on sides let remaining_vertices = new_vertices.slice(); selected_faces.forEach((face, face_index) => { let vertices = face.getSortedVertices(); vertices.forEach((a, i) => { let b = vertices[i+1] || vertices[0]; if (vertices.length == 2 && i) return; // Only create one quad when extruding line if (selected_faces.find(f => f != face && f.vertices.includes(a) && f.vertices.includes(b))) return; let new_face_vertices = [ b, a, original_vertices[new_vertices.indexOf(a)], original_vertices[new_vertices.indexOf(b)], ]; let new_face_uv = { [a]: face.uv[a], [b]: face.uv[b], [new_face_vertices[2]]: face.uv[a], [new_face_vertices[3]]: face.uv[b], }; let new_face = new MeshFace(mesh, mesh.faces[selected_face_keys[face_index]]).extend({ vertices: new_face_vertices, uv: new_face_uv }); let [fkey] = mesh.addFaces(new_face); modified_face_keys.push(fkey); remaining_vertices.remove(a); remaining_vertices.remove(b); }) if (vertices.length == 2) delete mesh.faces[selected_face_keys[face_index]]; }) remaining_vertices.forEach(a => { let b = original_vertices[new_vertices.indexOf(a)]; for (let fkey in mesh.faces) { let face = mesh.faces[fkey]; if (face.vertices.includes(b)) { face.vertices.splice(face.vertices.indexOf(b), 1, a); face.uv[a] = face.uv[b]; delete face.uv[b]; } } delete mesh.vertices[b]; }) UVEditor.setAutoSize(null, true, modified_face_keys); }) Undo.finishEdit('Extrude mesh selection') Canvas.updateView({elements: Mesh.selected, element_aspects: {geometry: true, uv: true, faces: true}, selection: true}) } runEdit(); Undo.amendEdit({ offset: {type: 'number', value: 50, label: 'edit.loop_cut.offset', min: 0, max: 100, interval_type: 'position'}, }, form => { runEdit(true, form.offset); }) } }) new Action('loop_cut', { icon: 'carpenter', category: 'edit', keybind: new Keybind({key: 'r', shift: true}), condition: {modes: ['edit'], features: ['meshes'], method: () => (Mesh.selected[0] && Mesh.selected[0].getSelectedVertices().length > 1)}, click() { let selected_face, selected_face_key; let saved_direction = 0; Mesh.selected.forEach(mesh => { if (!selected_face) { selected_face_key = mesh.getSelectedFaces()[0]; selected_face = mesh.faces[selected_face_key]; } }) function getLength(direction = 0) { selected_face = Mesh.selected.last().faces[selected_face_key]; if (selected_face) { let vertices = selected_face.getSortedVertices(); let pos1 = Mesh.selected[0].vertices[vertices[(0 + direction) % selected_face.vertices.length]]; let pos2 = Mesh.selected[0].vertices[vertices[(1 + direction) % selected_face.vertices.length]]; return Math.sqrt(Math.pow(pos2[0] - pos1[0], 2) + Math.pow(pos2[1] - pos1[1], 2) + Math.pow(pos2[2] - pos1[2], 2)); } else { let vertices = Mesh.selected[0].getSelectedVertices(); let pos1 = Mesh.selected[0].vertices[vertices[0]]; let pos2 = Mesh.selected[0].vertices[vertices[1]]; return Math.sqrt(Math.pow(pos2[0] - pos1[0], 2) + Math.pow(pos2[1] - pos1[1], 2) + Math.pow(pos2[2] - pos1[2], 2)); } } let length = getLength(); function runEdit(amended, offset, direction = 0, cuts = 1) { Undo.initEdit({elements: Mesh.selected, selection: true}, amended); if (offset == undefined) offset = length / (cuts+1); Mesh.selected.forEach(mesh => { let selected_vertices = mesh.getSelectedVertices(); let start_face; let start_face_quality = 1; for (let fkey in mesh.faces) { let face = mesh.faces[fkey]; if (face.vertices.length < 2) continue; let vertices = face.vertices.filter(vkey => selected_vertices.includes(vkey)) if (vertices.length > start_face_quality) { start_face = face; start_face_quality = vertices.length; } } if (!start_face) return; let processed_faces = [start_face]; let center_vertices_map = {}; function getCenterVertex(vertices, ratio) { let edge_key = vertices.slice().sort().join('.'); let existing_key = center_vertices_map[edge_key]; if (existing_key) return existing_key; let vector = mesh.vertices[vertices[0]].map((v, i) => Math.lerp(v, mesh.vertices[vertices[1]][i], ratio)) let [vkey] = mesh.addVertices(vector); center_vertices_map[edge_key] = vkey; return vkey; } function splitFace(face, side_vertices, double_side, cut_no) { processed_faces.push(face); let sorted_vertices = face.getSortedVertices(); let side_index_diff = sorted_vertices.indexOf(side_vertices[0]) - sorted_vertices.indexOf(side_vertices[1]); if (side_index_diff == -1 || side_index_diff > 2) side_vertices.reverse(); if (face.vertices.length == 4) { let opposite_vertices = sorted_vertices.filter(vkey => !side_vertices.includes(vkey)); let opposite_index_diff = sorted_vertices.indexOf(opposite_vertices[0]) - sorted_vertices.indexOf(opposite_vertices[1]); if (opposite_index_diff == 1 || opposite_index_diff < -2) opposite_vertices.reverse(); let ratio = offset/length; if (cuts > 1) { ratio = 1 - (1 / (cuts + 1 - cut_no) * ratio * 2); } let center_vertices = [ getCenterVertex(side_vertices, ratio), getCenterVertex(opposite_vertices, ratio) ] let c1_uv_coords = [ Math.lerp(face.uv[side_vertices[0]][0], face.uv[side_vertices[1]][0], ratio), Math.lerp(face.uv[side_vertices[0]][1], face.uv[side_vertices[1]][1], ratio), ]; let c2_uv_coords = [ Math.lerp(face.uv[opposite_vertices[0]][0], face.uv[opposite_vertices[1]][0], ratio), Math.lerp(face.uv[opposite_vertices[0]][1], face.uv[opposite_vertices[1]][1], ratio), ]; let new_face = new MeshFace(mesh, face).extend({ vertices: [side_vertices[1], center_vertices[0], center_vertices[1], opposite_vertices[1]], uv: { [side_vertices[1]]: face.uv[side_vertices[1]], [center_vertices[0]]: c1_uv_coords, [center_vertices[1]]: c2_uv_coords, [opposite_vertices[1]]: face.uv[opposite_vertices[1]], } }) face.extend({ vertices: [opposite_vertices[0], center_vertices[0], center_vertices[1], side_vertices[0]], uv: { [opposite_vertices[0]]: face.uv[opposite_vertices[0]], [center_vertices[0]]: c1_uv_coords, [center_vertices[1]]: c2_uv_coords, [side_vertices[0]]: face.uv[side_vertices[0]], } }) mesh.addFaces(new_face); // Multiple loop cuts if (cut_no+1 < cuts) { splitFace(face, [center_vertices[0], side_vertices[0]], double_side, cut_no+1); } if (cut_no != 0) return; // Find next (and previous) face for (let fkey in mesh.faces) { let ref_face = mesh.faces[fkey]; if (ref_face.vertices.length < 3 || processed_faces.includes(ref_face)) continue; let vertices = ref_face.vertices.filter(vkey => opposite_vertices.includes(vkey)) if (vertices.length >= 2) { splitFace(ref_face, opposite_vertices, ref_face.vertices.length == 4, 0); break; } } if (double_side) { for (let fkey in mesh.faces) { let ref_face = mesh.faces[fkey]; if (ref_face.vertices.length < 3 || processed_faces.includes(ref_face)) continue; let vertices = ref_face.vertices.filter(vkey => side_vertices.includes(vkey)) if (vertices.length >= 2) { let ref_sorted_vertices = ref_face.getSortedVertices(); let ref_opposite_vertices = ref_sorted_vertices.filter(vkey => !side_vertices.includes(vkey)); if (ref_opposite_vertices.length == 2) { splitFace(ref_face, ref_opposite_vertices, ref_face.vertices.length == 4, 0); break; } else if (ref_opposite_vertices.length == 1) { splitFace(ref_face, side_vertices, false, 0); break; } } } } } else if (face.vertices.length == 3) { if (direction > 2) { // Split tri from edge to edge let opposed_vertex = sorted_vertices.find(vkey => !side_vertices.includes(vkey)); let opposite_vertices = [opposed_vertex, side_vertices[direction % side_vertices.length]]; let opposite_index_diff = sorted_vertices.indexOf(opposite_vertices[0]) - sorted_vertices.indexOf(opposite_vertices[1]); if (opposite_index_diff == 1 || opposite_index_diff < -2) opposite_vertices.reverse(); let ratio = offset/length; if (cuts > 1) { ratio = 1 - (1 / (cuts + 1 - cut_no) * ratio * 2); } let center_vertices = [ getCenterVertex(side_vertices, ratio), getCenterVertex(opposite_vertices, ratio) ] let c1_uv_coords = [ Math.lerp(face.uv[side_vertices[0]][0], face.uv[side_vertices[1]][0], ratio), Math.lerp(face.uv[side_vertices[0]][1], face.uv[side_vertices[1]][1], ratio), ]; let c2_uv_coords = [ Math.lerp(face.uv[opposite_vertices[0]][0], face.uv[opposite_vertices[1]][0], ratio), Math.lerp(face.uv[opposite_vertices[0]][1], face.uv[opposite_vertices[1]][1], ratio), ]; let other_quad_vertex = side_vertices.find(vkey => !opposite_vertices.includes(vkey)); let other_tri_vertex = side_vertices.find(vkey => opposite_vertices.includes(vkey)); let new_face = new MeshFace(mesh, face).extend({ vertices: [other_tri_vertex, center_vertices[0], center_vertices[1]], uv: { [other_tri_vertex]: face.uv[other_tri_vertex], [center_vertices[0]]: c1_uv_coords, [center_vertices[1]]: c2_uv_coords, } }) if (new_face.getAngleTo(face) > 90) { new_face.invert(); } face.extend({ vertices: [opposed_vertex, center_vertices[0], center_vertices[1], other_quad_vertex], uv: { [opposed_vertex]: face.uv[opposed_vertex], [center_vertices[0]]: c1_uv_coords, [center_vertices[1]]: c2_uv_coords, [other_quad_vertex]: face.uv[other_quad_vertex], } }) if (face.getAngleTo(new_face) > 90) { face.invert(); } mesh.addFaces(new_face); // Multiple loop cuts if (cut_no+1 < cuts) { splitFace(face, [center_vertices[0], other_quad_vertex], double_side, cut_no+1); } if (cut_no != 0) return; // Find next (and previous) face for (let fkey in mesh.faces) { let ref_face = mesh.faces[fkey]; if (ref_face.vertices.length < 3 || processed_faces.includes(ref_face)) continue; let vertices = ref_face.vertices.filter(vkey => opposite_vertices.includes(vkey)) if (vertices.length >= 2) { splitFace(ref_face, opposite_vertices, ref_face.vertices.length == 4, 0); break; } } if (double_side) { for (let fkey in mesh.faces) { let ref_face = mesh.faces[fkey]; if (ref_face.vertices.length < 3 || processed_faces.includes(ref_face)) continue; let vertices = ref_face.vertices.filter(vkey => side_vertices.includes(vkey)) if (vertices.length >= 2) { let ref_sorted_vertices = ref_face.getSortedVertices(); let ref_opposite_vertices = ref_sorted_vertices.filter(vkey => !side_vertices.includes(vkey)); if (ref_opposite_vertices.length == 2) { splitFace(ref_face, ref_opposite_vertices, ref_face.vertices.length == 4, 0); break; } } } } } else { let opposite_vertex = sorted_vertices.find(vkey => !side_vertices.includes(vkey)); let ratio = offset/length; if (cuts > 1) { ratio = 1 - (1 / (cuts + 1 - cut_no) * ratio * 2); } let center_vertex = getCenterVertex(side_vertices, ratio); let c1_uv_coords = [ Math.lerp(face.uv[side_vertices[0]][0], face.uv[side_vertices[1]][0], ratio), Math.lerp(face.uv[side_vertices[0]][1], face.uv[side_vertices[1]][1], ratio), ]; let new_face = new MeshFace(mesh, face).extend({ vertices: [side_vertices[1], center_vertex, opposite_vertex], uv: { [side_vertices[1]]: face.uv[side_vertices[1]], [center_vertex]: c1_uv_coords, [opposite_vertex]: face.uv[opposite_vertex], } }) face.extend({ vertices: [opposite_vertex, center_vertex, side_vertices[0]], uv: { [opposite_vertex]: face.uv[opposite_vertex], [center_vertex]: c1_uv_coords, [side_vertices[0]]: face.uv[side_vertices[0]], } }) if (direction % 3 == 2) { new_face.invert(); face.invert(); } mesh.addFaces(new_face); } } else if (face.vertices.length == 2) { let ratio = offset/length; if (cuts > 1) { ratio = 1 - (1 / (cuts + 1 - cut_no) * ratio * 2); } let center_vertex = getCenterVertex(side_vertices, ratio); let c1_uv_coords = [ Math.lerp(face.uv[side_vertices[0]][0], face.uv[side_vertices[1]][0], ratio), Math.lerp(face.uv[side_vertices[0]][1], face.uv[side_vertices[1]][1], ratio), ]; let new_face = new MeshFace(mesh, face).extend({ vertices: [side_vertices[1], center_vertex], uv: { [side_vertices[1]]: face.uv[side_vertices[1]], [center_vertex]: c1_uv_coords, } }) face.extend({ vertices: [center_vertex, side_vertices[0]], uv: { [center_vertex]: c1_uv_coords, [side_vertices[0]]: face.uv[side_vertices[0]], } }) mesh.addFaces(new_face); } } let start_vertices = start_face.getSortedVertices().filter((vkey, i) => selected_vertices.includes(vkey)); let start_edge = [start_vertices[direction % start_vertices.length], start_vertices[(direction+1) % start_vertices.length]]; if (start_edge.length == 1) start_edge.splice(0, 0, start_vertices[0]); splitFace(start_face, start_edge, start_face.vertices.length == 4 || direction > 2, 0); selected_vertices.empty(); for (let key in center_vertices_map) { selected_vertices.safePush(center_vertices_map[key]); } }) Undo.finishEdit('Create loop cut') Canvas.updateView({elements: Mesh.selected, element_aspects: {geometry: true, uv: true, faces: true}, selection: true}) } runEdit(); Undo.amendEdit({ direction: {type: 'number', value: 0, label: 'edit.loop_cut.direction', condition: !!selected_face, min: 0}, cuts: {type: 'number', value: 1, label: 'edit.loop_cut.cuts', min: 0, max: 16}, offset: {type: 'number', value: length/2, label: 'edit.loop_cut.offset', min: 0, max: length, interval_type: 'position'}, }, (form, form_options) => { let direction = form.direction || 0; length = getLength(direction); form_options.offset.slider.settings.max = length; if(saved_direction !== direction) { form_options.offset.slider.value = length/2; form_options.offset.slider.update(); saved_direction = direction; } runEdit(true, form_options.offset.slider.value, form_options.direction ? direction : 0, form.cuts); }) } }) new Action('dissolve_edges', { icon: 'border_vertical', category: 'edit', condition: {modes: ['edit'], features: ['meshes'], method: () => (Mesh.selected[0] && Mesh.selected[0].getSelectedVertices().length > 1)}, click() { Undo.initEdit({elements: Mesh.selected}); Mesh.selected.forEach(mesh => { let edges = mesh.getSelectedEdges(true); let selected_vertices = mesh.getSelectedVertices(true); for (let edge of edges) { let adjacent_faces = []; let adjacent_fkeys = []; for (let fkey in mesh.faces) { let face = mesh.faces[fkey]; if (!face.vertices.includes(edge[0]) || !face.vertices.includes(edge[1])) continue; let vertices = face.getSortedVertices(); let index_a = vertices.indexOf(edge[0]), index_b = vertices.indexOf(edge[1]); if (vertices.length < 4 || (Math.abs(index_a - index_b) != 2)) { adjacent_faces.push(face); adjacent_fkeys.push(fkey); } } // Connect adjacent faces let keep_faces = adjacent_fkeys.length >= 2; if (keep_faces) { let face_a = mesh.faces[adjacent_fkeys[0]], face_b = mesh.faces[adjacent_fkeys[1]]; let vertices_from_a = face_a.vertices.filter(vkey => edge.indexOf(vkey) == -1); delete mesh.faces[adjacent_fkeys[0]]; adjacent_fkeys.remove(adjacent_fkeys[0]); face_b.vertices.safePush(...vertices_from_a); vertices_from_a.forEach((vkey, i) => { face_b.uv[vkey] = face_a.uv[vkey] ? face_a.uv[vkey].slice() : [0, 0]; }) // Ensure face has no more than 4 vertices edge.forEach(edge_vkey => { if (face_b.vertices.length > 4) { face_b.vertices.remove(edge_vkey); delete face_b.uv[edge_vkey]; } }) // Make sure orientation stays the same if (face_b.getAngleTo(face_a) > 90) { face_b.invert(); } } // Remove all other faces and lines adjacent_fkeys.forEach((fkey, i) => { let face = mesh.faces[fkey]; if (face && (i > 1 || !keep_faces)) { delete mesh.faces[fkey]; } }) } // Remove leftover vertices let vertices_used = []; for (let edge of edges) { vertices_used.safePush(...edge); } for (let vkey of vertices_used) { let used = false; for (let fkey in mesh.faces) { if (mesh.faces[fkey].vertices.includes(vkey)) { used = true; break; } } if (!used) { delete mesh.vertices[vkey]; } } selected_vertices.empty(); }) Undo.finishEdit('Dissolve edges') Canvas.updateView({elements: Mesh.selected, element_aspects: {geometry: true, uv: true, faces: true}, selection: true}) } }) function mergeVertices(by_distance, in_center) { let found = 0, result = 0; Undo.initEdit({elements: Mesh.selected}); Mesh.selected.forEach(mesh => { let selected_vertices = mesh.getSelectedVertices(); if (selected_vertices.length < 2) return; if (!by_distance) { let first_vertex = selected_vertices[0]; if (in_center) { let center = [0, 0, 0]; selected_vertices.forEach(vkey => { center.V3_add(mesh.vertices[vkey]); }) center.V3_divide(selected_vertices.length); mesh.vertices[first_vertex].V3_set(center); for (let fkey in mesh.faces) { let face = mesh.faces[fkey]; let matches = selected_vertices.filter(vkey => face.vertices.includes(vkey)); if (matches.length < 2) continue; let center = [0, 0]; matches.forEach(vkey => { center[0] += face.uv[vkey][0]; center[1] += face.uv[vkey][1]; }) center[0] /= matches.length; center[1] /= matches.length; matches.forEach(vkey => { face.uv[vkey][0] = center[0]; face.uv[vkey][1] = center[1]; }) } } selected_vertices.forEach(vkey => { if (vkey == first_vertex) return; 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 { 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.splice(1, selected_vertices.length); } else { let selected_vertices = mesh.getSelectedVertices().slice(); if (selected_vertices.length < 2) return; let groups = {}; let i = 0; while (selected_vertices[i]) { let vkey1 = selected_vertices[i]; let j = i+1; while (selected_vertices[j]) { let vkey2 = selected_vertices[j]; let vector1 = mesh.vertices[vkey1]; let vector2 = mesh.vertices[vkey2]; if (Math.sqrt(Math.pow(vector2[0] - vector1[0], 2) + Math.pow(vector2[1] - vector1[1], 2) + Math.pow(vector2[2] - vector1[2], 2)) < settings.vertex_merge_distance.value) { if (!groups[vkey1]) groups[vkey1] = []; groups[vkey1].push(vkey2); } j++; } if (groups[vkey1]) { groups[vkey1].forEach(vkey2 => { selected_vertices.remove(vkey2); }) } i++; } let current_selected_vertices = mesh.getSelectedVertices(); for (let first_vertex in groups) { let group = groups[first_vertex]; if (in_center) { let group_all = [first_vertex, ...group]; let center = [0, 0, 0]; group_all.forEach(vkey => { center.V3_add(mesh.vertices[vkey]); }) center.V3_divide(group_all.length); mesh.vertices[first_vertex].V3_set(center); for (let fkey in mesh.faces) { let face = mesh.faces[fkey]; let matches = group_all.filter(vkey => face.vertices.includes(vkey)); if (matches.length < 2) continue; let center = [0, 0]; matches.forEach(vkey => { center[0] += face.uv[vkey][0]; center[1] += face.uv[vkey][1]; }) center[0] /= matches.length; center[1] /= matches.length; matches.forEach(vkey => { face.uv[vkey][0] = center[0]; face.uv[vkey][1] = center[1]; }) } } group.forEach(vkey => { 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 { let uv = face.uv[vkey]; face.vertices.splice(index, 1, first_vertex); face.uv[first_vertex] = uv; delete face.uv[vkey]; } } found++; delete mesh.vertices[vkey]; current_selected_vertices.remove(vkey); }) found++; result++; } } }) Undo.finishEdit('Merge vertices') Canvas.updateView({elements: Mesh.selected, element_aspects: {geometry: true, uv: true, faces: true}, selection: true}) if (by_distance) { Blockbench.showQuickMessage(tl('message.merged_vertices', [found, result]), 2000); } } new Action('merge_vertices', { icon: 'close_fullscreen', category: 'edit', keybind: new Keybind({key: 'm', shift: true}), condition: {modes: ['edit'], features: ['meshes'], method: () => (Mesh.selected[0] && Mesh.selected[0].getSelectedVertices().length > 1)}, click() { new Menu(this.children).open('mouse'); }, children: [ { id: 'merge_all', name: 'action.merge_vertices.merge_all', icon: 'north_east', click() {mergeVertices(false, false);} }, { id: 'merge_all_in_center', name: 'action.merge_vertices.merge_all_in_center', icon: 'close_fullscreen', click() {mergeVertices(false, true);} }, { id: 'merge_by_distance', name: 'action.merge_vertices.merge_by_distance', icon: 'expand_less', click() {mergeVertices(true, false);} }, { id: 'merge_by_distance_in_center', name: 'action.merge_vertices.merge_by_distance_in_center', icon: 'unfold_less', click() {mergeVertices(true, true);} } ] }) new Action('merge_meshes', { icon: 'upload', category: 'edit', condition: {modes: ['edit'], features: ['meshes'], method: () => (Mesh.selected.length >= 2)}, click() { let elements = Mesh.selected.slice(); Undo.initEdit({elements, outliner: true}); let original = Mesh.selected[0]; let vector = new THREE.Vector3(); Mesh.selected.forEach(mesh => { if (mesh == original) return; let old_vertex_keys = Object.keys(mesh.vertices); let new_vertex_keys = original.addVertices(...mesh.vertice_list.map(arr => { vector.fromArray(arr); mesh.mesh.localToWorld(vector); original.mesh.worldToLocal(vector); return vector.toArray() })); for (let key in mesh.faces) { let old_face = mesh.faces[key]; let new_face = new MeshFace(original, old_face); let uv = {}; for (let vkey in old_face.uv) { let new_vkey = new_vertex_keys[old_vertex_keys.indexOf(vkey)] uv[new_vkey] = old_face.uv[vkey]; } new_face.extend({ vertices: old_face.vertices.map(v => new_vertex_keys[old_vertex_keys.indexOf(v)]), uv }) original.addFaces(new_face) } mesh.remove(); elements.remove(mesh); Mesh.selected.remove(mesh) }) updateSelection(); Undo.finishEdit('Merge meshes') Canvas.updateView({elements, element_aspects: {geometry: true, uv: true, faces: true}, selection: true}) } }) new Action('split_mesh', { icon: 'call_split', category: 'edit', condition: {modes: ['edit'], features: ['meshes'], method: () => (Mesh.selected[0] && Mesh.selected[0].getSelectedVertices().length)}, click() { let elements = Mesh.selected.slice(); Undo.initEdit({elements}); Mesh.selected.forEach(mesh => { let selected_vertices = mesh.getSelectedVertices(); let mesh_selection = Project.mesh_selection[mesh.uuid]; let copy = new Mesh(mesh); elements.push(copy); for (let fkey in mesh.faces) { let face = mesh.faces[fkey]; if (face.isSelected(fkey)) { delete mesh.faces[fkey]; } else { delete copy.faces[fkey]; } } selected_vertices.forEach(vkey => { let used = false; for (let key in mesh.faces) { let face = mesh.faces[key]; if (face.vertices.includes(vkey)) used = true; } if (!used) { delete mesh.vertices[vkey]; } }) Object.keys(copy.vertices).filter(vkey => !selected_vertices.includes(vkey)).forEach(vkey => { let used = false; for (let key in copy.faces) { let face = copy.faces[key]; if (face.vertices.includes(vkey)) used = true; } if (!used) { delete copy.vertices[vkey]; } }) copy.name += '_selection' copy.sortInBefore(mesh, 1).init(); delete Project.mesh_selection[mesh.uuid]; Project.mesh_selection[copy.uuid] = mesh_selection; mesh.preview_controller.updateGeometry(mesh); selected[selected.indexOf(mesh)] = copy; }) Undo.finishEdit('Merge meshes'); updateSelection(); Canvas.updateView({elements, element_aspects: {geometry: true, uv: true, faces: true}, selection: true}) } }) let import_obj_dialog; new Action('import_obj', { icon: 'fa-gem', category: 'file', condition: {modes: ['edit'], method: () => (Format.meshes)}, click: function () { function importOBJ(result) { let mtl_materials = {}; if (result.mtl) { let mtl_lines = result.mtl.content.split(/[\r\n]+/); let current_material; for (let line of mtl_lines) { let args = line.split(/\s+/).filter(arg => typeof arg !== 'undefined' && arg !== ''); let cmd = args.shift(); switch (cmd) { case 'newmtl': { current_material = mtl_materials[args[0]] = {}; break; } case 'map_Kd': { let texture_name = args[0]; let texture_path = isApp ? PathModule.join(result.mtl.path, '..', texture_name) : ''; let texture = new Texture().fromPath(texture_path).add(); current_material.texture = texture; } } } } let {content} = result.obj; let lines = content.split(/[\r\n]+/); function toVector(args, length) { return args.map(v => parseFloat(v)); } let mesh; let vertices = []; let vertex_keys = {}; let vertex_textures = []; let vertex_normals = []; let meshes = []; let vector1 = new THREE.Vector3(); let vector2 = new THREE.Vector3(); let current_texture; Undo.initEdit({outliner: true, elements: meshes, selection: true}); lines.forEach(line => { if (line.substr(0, 1) == '#' || !line) return; let args = line.split(/\s+/).filter(arg => typeof arg !== 'undefined' && arg !== ''); let cmd = args.shift(); if (['o', 'g'].includes(cmd) || (cmd == 'v' && !mesh)) { mesh = new Mesh({ name: ['o', 'g'].includes(cmd) ? args[0] : 'unknown', vertices: {} }) vertex_keys = {}; meshes.push(mesh); } if (cmd == 'v') { vertices.push(toVector(args, 3).map(v => v * result.scale)); } if (cmd == 'vt') { vertex_textures.push(toVector(args, 2)) } if (cmd == 'vn') { vertex_normals.push(toVector(args, 3)) } if (cmd == 'f') { let f = { vertices: [], vertex_textures: [], vertex_normals: [], } args.forEach((triplet, i) => { if (i >= 4) return; let [v, vt, vn] = triplet.split('/').map(v => parseInt(v)); if (!vertex_keys[ v-1 ]) { vertex_keys[ v-1 ] = mesh.addVertices(vertices[v-1])[0]; } f.vertices.push(vertex_keys[ v-1 ]); f.vertex_textures.push(vertex_textures[ vt-1 ]); f.vertex_normals.push(vertex_normals[ vn-1 ]); }) let uv = {}; f.vertex_textures.forEach((vt, i) => { let key = f.vertices[i]; if (vt instanceof Array) { uv[key] = [ vt[0] * Project.texture_width, (1-vt[1]) * Project.texture_width ]; } else { uv[key] = [0, 0]; } }) let face = new MeshFace(mesh, { vertices: f.vertices, uv, texture: current_texture }) mesh.addFaces(face); if (f.vertex_normals.find(v => v)) { vector1.fromArray(face.getNormal()); vector2.fromArray(f.vertex_normals[0]); let angle = vector1.angleTo(vector2); if (angle > Math.PI/2) { face.invert(); } } } if (cmd == 'usemtl') { current_texture = mtl_materials[args[0]]?.texture; } }) meshes.forEach(mesh => { mesh.init(); }) Undo.finishEdit('Import OBJ'); } if (!import_obj_dialog) { import_obj_dialog = new Dialog('import_obj', { title: 'action.import_obj', form: { obj: {type: 'file', label: 'dialog.import_obj.obj', return_as: 'file', extensions: ['obj'], resource_id: 'obj', filetype: 'OBJ Wavefront Model'}, mtl: {type: 'file', label: 'dialog.import_obj.mtl', return_as: 'file', extensions: ['mtl'], resource_id: 'obj', filetype: 'OBJ Material File'}, scale: {type: 'number', label: 'dialog.import_obj.scale', value: 16}, }, onConfirm(result) { importOBJ(result); } }) } import_obj_dialog.show(); } }) StateMemory.init('proportional_editing_options', 'object'); if (!StateMemory.proportional_editing_options.range) { StateMemory.proportional_editing_options.range = 8; } if (!StateMemory.proportional_editing_options.falloff) { StateMemory.proportional_editing_options.falloff = 'linear'; } if (!StateMemory.proportional_editing_options.selection) { StateMemory.proportional_editing_options.selection = 'linear'; } new NumSlider('proportional_editing_range', { category: 'edit', condition: {modes: ['edit'], features: ['meshes']}, get() { return StateMemory.proportional_editing_options.range }, change(modify) { StateMemory.proportional_editing_options.range = modify(StateMemory.proportional_editing_options.range); }, onAfter() { StateMemory.save('proportional_editing_options'); } }) new Toggle('proportional_editing', { icon: 'wifi_tethering', category: 'edit', condition: {modes: ['edit'], features: ['meshes'], method: () => (Mesh.selected[0] && Mesh.selected[0].getSelectedVertices().length > 0)}, side_menu: new Dialog('proportional_editing_options', { title: 'action.proportional_editing', width: 400, singleButton: true, form: { enabled: {type: 'checkbox', label: 'menu.mirror_painting.enabled', value: false}, range: {type: 'number', label: 'dialog.proportional_editing.range', value: StateMemory.proportional_editing_options.range}, falloff: {type: 'select', label: 'dialog.proportional_editing.falloff', value: StateMemory.proportional_editing_options.falloff, options: { linear: 'dialog.proportional_editing.falloff.linear', hermite_spline: 'dialog.proportional_editing.falloff.hermite_spline', constant: 'dialog.proportional_editing.falloff.constant', }}, selection: {type: 'select', label: 'dialog.proportional_editing.selection', value: StateMemory.proportional_editing_options.selection, options: { linear: 'dialog.proportional_editing.selection.linear', connections: 'dialog.proportional_editing.selection.connections', //path: 'Connection Path', }}, }, onOpen() { this.setFormValues({enabled: BarItems.proportional_editing.value}); }, onFormChange(formResult) { if (BarItems.proportional_editing.value != formResult.enabled) { BarItems.proportional_editing.trigger(); } StateMemory.proportional_editing_options.range = formResult.range; StateMemory.proportional_editing_options.falloff = formResult.falloff; StateMemory.proportional_editing_options.selection = formResult.selection; StateMemory.save('proportional_editing_options'); BarItems.proportional_editing_range.update(); } }) }) })