diff --git a/index.html b/index.html index 3d753636..fa54fc5f 100644 --- a/index.html +++ b/index.html @@ -120,8 +120,9 @@ - - + + + diff --git a/js/modeling/mesh_editing.js b/js/modeling/mesh_editing.js new file mode 100644 index 00000000..a306bda5 --- /dev/null +++ b/js/modeling/mesh_editing.js @@ -0,0 +1,1577 @@ +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: { + cube: 'dialog.add_primitive.shape.cube', + 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}) => !['cube', 'pyramid', 'plane'].includes(shape)}, + height: {label: 'dialog.add_primitive.height', type: 'number', value: 8, condition: ({shape}) => ['cylinder', 'cone', 'cube', '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)}, + }, + 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: {} + }); + var group = getCurrentGroup(); + mesh.addTo(group) + 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 == 'cube') { + 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 == '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)); + UVEditor.selected_faces.empty(); + 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}, + height: {label: 'dialog.add_primitive.height', type: 'number', value: result.height, condition: ['cylinder', 'cone', 'cube', 'pyramid', 'tube'].includes(result.shape)}, + 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)}, + minor_sides: {label: 'dialog.add_primitive.minor_sides', type: 'number', value: result.minor_sides, min: 2, max: 32, condition: ['torus'].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(); + } + }) + new BarSelect('selection_mode', { + options: { + object: {name: true, icon: 'far.fa-gem'}, + face: {name: true, icon: 'crop_portrait'}, + edge: {name: true, icon: 'fa-grip-lines-vertical'}, + vertex: {name: true, icon: 'fiber_manual_record'}, + }, + icon_mode: true, + condition: () => Modes.edit && Mesh.all.length, + onChange({value}) { + if (value === 'object') { + Mesh.selected.forEach(mesh => { + delete Project.selected_vertices[mesh.uuid]; + }) + } else if (value === 'face') { + UVEditor.vue.selected_faces.empty(); + Mesh.selected.forEach(mesh => { + for (let fkey in mesh.faces) { + let face = mesh.faces[fkey]; + if (face.isSelected()) { + UVEditor.vue.selected_faces.safePush(fkey); + } + } + }) + } + updateSelection(); + } + }) + + 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.all.length, + 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 BarSelect('select_seam', { + options: { + auto: true, + divide: true, + join: true, + }, + condition: () => Modes.edit && Mesh.all.length, + onChange({value}) { + if (value == 'auto') value = null; + Undo.initEdit({elements: Mesh.selected}); + Mesh.selected.forEach(mesh => { + let selected_vertices = mesh.getSelectedVertices(); + mesh.forAllFaces((face) => { + let vertices = face.getSortedVertices(); + vertices.forEach((vkey_a, i) => { + let vkey_b = vertices[i+1] || vertices[0]; + if (selected_vertices.includes(vkey_a) && selected_vertices.includes(vkey_b)) { + mesh.setSeam([vkey_a, vkey_b], 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 => { + UVEditor.selected_faces.empty(); + let selected_vertices = mesh.getSelectedVertices(); + 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 (face.isSelected()) { + delete mesh.faces[key]; + } + } + // Split face + if ( + (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); + UVEditor.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); + UVEditor.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); + UVEditor.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}); + + let new_meshes = []; + Cube.selected.forEach(cube => { + + let mesh = new Mesh({ + name: cube.name, + color: cube.color, + origin: cube.origin, + rotation: cube.rotation, + vertices: [ + [cube.to[0] + cube.inflate - cube.origin[0], cube.to[1] + cube.inflate - cube.origin[1], cube.to[2] + cube.inflate - cube.origin[2]], + [cube.to[0] + cube.inflate - cube.origin[0], cube.to[1] + cube.inflate - cube.origin[1], cube.from[2] - cube.inflate - cube.origin[2]], + [cube.to[0] + cube.inflate - cube.origin[0], cube.from[1] - cube.inflate - cube.origin[1], cube.to[2] + cube.inflate - cube.origin[2]], + [cube.to[0] + cube.inflate - cube.origin[0], cube.from[1] - cube.inflate - cube.origin[1], cube.from[2] - cube.inflate - cube.origin[2]], + [cube.from[0] - cube.inflate - cube.origin[0], cube.to[1] + cube.inflate - cube.origin[1], cube.to[2] + cube.inflate - cube.origin[2]], + [cube.from[0] - cube.inflate - cube.origin[0], cube.to[1] + cube.inflate - cube.origin[1], cube.from[2] - cube.inflate - cube.origin[2]], + [cube.from[0] - cube.inflate - cube.origin[0], cube.from[1] - cube.inflate - cube.origin[1], cube.to[2] + cube.inflate - cube.origin[2]], + [cube.from[0] - cube.inflate - cube.origin[0], cube.from[1] - cube.inflate - cube.origin[1], cube.from[2] - cube.inflate - cube.origin[2]], + ], + }) + + let vertex_keys = Object.keys(mesh.vertices); + let unused_vkeys = vertex_keys.slice(); + function addFace(direction, vertices) { + let cube_face = cube.faces[direction]; + if (cube_face.texture === null) return; + let uv = { + [vertices[0]]: [cube_face.uv[2], cube_face.uv[1]], + [vertices[1]]: [cube_face.uv[0], cube_face.uv[1]], + [vertices[2]]: [cube_face.uv[2], cube_face.uv[3]], + [vertices[3]]: [cube_face.uv[0], cube_face.uv[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); + cube.remove(); + }) + Undo.finishEdit('Convert cubes to meshes', {elements: new_meshes}); + } + }) + 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 key in mesh.faces) { + let face = mesh.faces[key]; + if (face.isSelected()) { + face.invert(); + } + } + }) + Undo.finishEdit('Invert mesh faces'); + 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 = Project.selected_vertices[mesh.uuid].slice(); + let new_vertices; + let new_face_keys = []; + let selected_faces = []; + let selected_face_keys = []; + let combined_direction; + for (let fkey in mesh.faces) { + let face = mesh.faces[fkey]; + if (face.isSelected()) { + selected_faces.push(face); + selected_face_keys.push(fkey); + } + } + + 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) { + if (mesh.faces[fkey].vertices.filter(vkey => original_vertices.includes(vkey)).length >= 2 && mesh.faces[fkey].vertices.length > 2) { + face = mesh.faces[fkey]; + 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) { + direction = match.getNormal(true); + } + } + + vector.V3_add(direction.map(v => v * extend)); + return vector; + })) + Project.selected_vertices[mesh.uuid].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 line + let line_vertices = remaining_vertices.slice(); + let covered_edges = []; + let new_faces = []; + for (let fkey in mesh.faces) { + let face = mesh.faces[fkey]; + let sorted_vertices = face.getSortedVertices(); + let matched_vertices = sorted_vertices.filter(vkey => line_vertices.includes(new_vertices[original_vertices.indexOf(vkey)])); + if (matched_vertices.length >= 2) { + let already_handled_edge = covered_edges.find(edge => edge.includes(matched_vertices[0]) && edge.includes(matched_vertices[1])) + if (already_handled_edge) { + let handled_face = new_faces[covered_edges.indexOf(already_handled_edge)] + if (handled_face) handled_face.invert(); + continue; + } + covered_edges.push(matched_vertices.slice(0, 2)); + + if (sorted_vertices[0] == matched_vertices[0] && sorted_vertices[1] != matched_vertices[1]) { + matched_vertices.reverse(); + } + let [a, b] = matched_vertices.map(vkey => new_vertices[original_vertices.indexOf(vkey)]); + let [c, d] = matched_vertices; + 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); + } + }) + + 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'}, + }, form => { + runEdit(true, form.extend); + }) + } + }) + 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 = Project.selected_vertices[mesh.uuid].slice(); + if (original_vertices.length < 3) return; + let new_vertices; + let selected_faces = []; + let selected_face_keys = []; + for (let key in mesh.faces) { + let face = mesh.faces[key]; + if (face.isSelected()) { + selected_faces.push(face); + selected_face_keys.push(key); + } + } + + 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.selected_vertices[mesh.uuid].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)], + ] + }); + mesh.addFaces(new_face); + 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]; + }) + + }) + 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}, + }, 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() { + function runEdit(amended, offset = 50, direction = 0) { + Undo.initEdit({elements: Mesh.selected, selection: true}, amended); + 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 = {}; + + function getCenterVertex(vertices) { + let existing_key = center_vertices[vertices[0]] || center_vertices[vertices[1]]; + if (existing_key) return existing_key; + + let vector = mesh.vertices[vertices[0]].map((v, i) => Math.lerp(v, mesh.vertices[vertices[1]][i], offset/100)) + let [vkey] = mesh.addVertices(vector); + center_vertices[vertices[0]] = center_vertices[vertices[1]] = vkey; + return vkey; + } + + function splitFace(face, side_vertices, double_side) { + 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 center_vertices = [ + getCenterVertex(side_vertices), + getCenterVertex(opposite_vertices) + ] + + let c1_uv_coords = [ + Math.lerp(face.uv[side_vertices[0]][0], face.uv[side_vertices[1]][0], offset/100), + Math.lerp(face.uv[side_vertices[0]][1], face.uv[side_vertices[1]][1], offset/100), + ]; + let c2_uv_coords = [ + Math.lerp(face.uv[opposite_vertices[0]][0], face.uv[opposite_vertices[1]][0], offset/100), + Math.lerp(face.uv[opposite_vertices[0]][1], face.uv[opposite_vertices[1]][1], offset/100), + ]; + + 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); + + // 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); + 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) { + splitFace(ref_face, side_vertices); + break; + } + } + } + + } else { + let opposite_vertex = sorted_vertices.find(vkey => !side_vertices.includes(vkey)); + + let center_vertex = getCenterVertex(side_vertices); + + let c1_uv_coords = [ + (face.uv[side_vertices[0]][0] + face.uv[side_vertices[1]][0]) / 2, + (face.uv[side_vertices[0]][1] + face.uv[side_vertices[1]][1]) / 2, + ]; + + 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]], + } + }) + mesh.addFaces(new_face); + } + } + + let start_vertices = start_face.getSortedVertices().filter((vkey, i) => selected_vertices.includes(vkey)); + let start_offset = direction % start_vertices.length; + let start_edge = start_vertices.slice(start_offset, start_offset+2); + if (start_edge.length == 1) start_edge.splice(0, 0, start_vertices[0]); + + splitFace(start_face, start_edge, start_face.vertices.length == 4); + + selected_vertices.empty(); + for (let key in center_vertices) { + selected_vertices.safePush(center_vertices[key]); + } + }) + Undo.finishEdit('Create loop cut') + Canvas.updateView({elements: Mesh.selected, element_aspects: {geometry: true, uv: true, faces: true}, selection: true}) + } + + let selected_face; + Mesh.selected.forEach(mesh => { + if (!selected_face) { + selected_face = mesh.getSelectedFaces()[0]; + } + }) + + 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: 50, label: 'edit.loop_cut.offset', min: 0, max: 100}, + }, form => { + runEdit(true, form.offset, form.direction); + }) + } + }) + 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 selected_vertices = mesh.getSelectedVertices(); + let faces = Object.keys(mesh.faces); + for (let fkey in mesh.faces) { + let face = mesh.faces[fkey]; + let sorted_vertices = face.getSortedVertices(); + let side_vertices = faces.includes(fkey) && sorted_vertices.filter(vkey => selected_vertices.includes(vkey)); + if (side_vertices && side_vertices.length == 2) { + if (side_vertices[0] == sorted_vertices[0] && side_vertices[1] == sorted_vertices.last()) { + side_vertices.reverse(); + } + let original_face_normal = face.getNormal(true); + let index_difference = sorted_vertices.indexOf(side_vertices[1]) - sorted_vertices.indexOf(side_vertices[0]); + if (index_difference == -1 || index_difference > 2) side_vertices.reverse(); + let other_face = face.getAdjacentFace(sorted_vertices.indexOf(side_vertices[0])); + face.vertices.remove(...side_vertices); + delete face.uv[side_vertices[0]]; + delete face.uv[side_vertices[1]]; + if (other_face) { + let new_vertices = other_face.face.getSortedVertices().filter(vkey => !side_vertices.includes(vkey)); + face.vertices.push(...new_vertices); + new_vertices.forEach(vkey => { + face.uv[vkey] = other_face.face.uv[vkey]; + }) + delete mesh.faces[other_face.key]; + } + faces.remove(fkey); + if (Reusable.vec1.fromArray(face.getNormal(true)).angleTo(Reusable.vec2.fromArray(original_face_normal)) > Math.PI/2) { + face.invert(); + } + side_vertices.forEach(vkey => { + let is_used; + for (let fkey2 in mesh.faces) { + if (mesh.faces[fkey2].vertices.includes(vkey)) { + is_used = true; + break; + } + } + if (!is_used) { + delete mesh.vertices[vkey]; + selected_vertices.remove(vkey); + } + }) + } + } + }) + 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}); + 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 copy = new Mesh(mesh); + elements.push(copy); + + for (let fkey in mesh.faces) { + let face = mesh.faces[fkey]; + if (face.isSelected()) { + 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.selected_vertices[mesh.uuid]; + Project.selected_vertices[copy.uuid] = selected_vertices; + 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}) + } + }) + new Action('import_obj', { + icon: 'fa-gem', + category: 'file', + condition: {modes: ['edit'], method: () => (Format.meshes)}, + click: function () { + + + Blockbench.import({ + resource_id: 'obj', + extensions: ['obj'], + name: 'OBJ Wavefront Model', + }, function(files) { + let {content} = files[0]; + 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(); + + 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 (cmd == 'o' || cmd == 'g') { + mesh = new Mesh({ + name: args[0], + vertices: {} + }) + vertex_keys = {}; + meshes.push(mesh); + } + if (cmd == 'v') { + vertices.push(toVector(args, 3).map(v => v * 16)); + } + 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 => { + 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 + }) + 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(); + } + } + } + }) + meshes.forEach(mesh => { + mesh.init(); + }) + + Undo.finishEdit('Import OBJ'); + }) + } + }) +}) diff --git a/js/transform.js b/js/modeling/transform.js similarity index 96% rename from js/transform.js rename to js/modeling/transform.js index 61d31e67..d3653cfa 100644 --- a/js/transform.js +++ b/js/modeling/transform.js @@ -1,1867 +1,1867 @@ -//Actions -function origin2geometry() { - - if (Format.bone_rig && Group.selected) { - Undo.initEdit({group: Group.selected}) - - if (!Group.selected || Group.selected.children.length === 0) return; - var position = new THREE.Vector3(); - let amount = 0; - Group.selected.children.forEach(function(obj) { - if (obj.getWorldCenter) { - position.add(obj.getWorldCenter()); - amount++; - } - }) - position.divideScalar(amount); - Group.selected.mesh.parent.worldToLocal(position); - if (Group.selected.parent instanceof Group) { - position.x += Group.selected.parent.origin[0]; - position.y += Group.selected.parent.origin[1]; - position.z += Group.selected.parent.origin[2]; - } - Group.selected.transferOrigin(position.toArray()); - - } else if (Outliner.selected[0]) { - Undo.initEdit({elements: Outliner.selected}) - - var center = getSelectionCenter(); - var original_center = center.slice(); - - Outliner.selected.forEach(element => { - if (!element.transferOrigin) return; - if (Format.bone_rig && element.parent instanceof Group) { - var v = new THREE.Vector3().fromArray(original_center); - element.parent.mesh.worldToLocal(v); - v.x += element.parent.origin[0]; - v.y += element.parent.origin[1]; - v.z += element.parent.origin[2]; - center = v.toArray(); - element.transferOrigin(center) - } else { - element.transferOrigin(original_center) - } - }) - } - Canvas.updateView({ - elements: Outliner.selected, - element_aspects: {transform: true, geometry: true}, - groups: Group.selected && [Group.selected], - selection: true - }); - Undo.finishEdit('Center pivot') -} -function getSelectionCenter(all = false) { - if (Group.selected && selected.length == 0 && !all) { - let vec = THREE.fastWorldPosition(Group.selected.mesh, new THREE.Vector3()); - return vec.toArray(); - } - - let max = [-Infinity, -Infinity, -Infinity]; - let min = [ Infinity, Infinity, Infinity]; - let elements = Outliner.selected.length ? Outliner.selected : Outliner.elements; - elements.forEach(element => { - if (element.getWorldCenter) { - var pos = element.getWorldCenter(); - min[0] = Math.min(pos.x, min[0]); max[0] = Math.max(pos.x, max[0]); - min[1] = Math.min(pos.y, min[1]); max[1] = Math.max(pos.y, max[1]); - min[2] = Math.min(pos.z, min[2]); max[2] = Math.max(pos.z, max[2]); - } - }) - let center = max.V3_add(min).V3_divide(2); - - if (!Format.centered_grid) { - center.V3_add(8, 8, 8) - } - return center; -} -function limitToBox(val, inflate) { - if (typeof inflate != 'number') inflate = 0; - if (!(Format.canvas_limit && !settings.deactivate_size_limit.value)) { - return val; - } else if (val + inflate > 32) { - return 32 - inflate; - } else if (val - inflate < -16) { - return -16 + inflate; - } else { - return val; - } -} -//Movement -function moveElementsRelative(difference, index, event) { //Multiple - if (!quad_previews.current || !Outliner.selected.length) { - return; - } - var _has_groups = Format.bone_rig && Group.selected && Group.selected.matchesSelection() && Toolbox.selected.transformerMode == 'translate'; - - Undo.initEdit({elements: Outliner.selected, outliner: _has_groups}) - var axes = [] - // < > - // PageUpDown - // ^ v - var facing = quad_previews.current.getFacingDirection() - var height = quad_previews.current.getFacingHeight() - switch (facing) { - case 'north': axes = [0, 2, 1]; break; - case 'south': axes = [0, 2, 1]; break; - case 'west': axes = [2, 0, 1]; break; - case 'east': axes = [2, 0, 1]; break; - } - - if (height !== 'middle') { - if (index === 1) { - index = 2 - } else if (index === 2) { - index = 1 - } - } - if (facing === 'south' && (index === 0 || index === 1)) difference *= -1 - if (facing === 'west' && index === 0) difference *= -1 - if (facing === 'east' && index === 1) difference *= -1 - if (index === 2 && height !== 'down') difference *= -1 - if (index === 1 && height === 'up') difference *= -1 - - if (event) { - difference *= canvasGridSize(event.shiftKey || Pressing.overrides.shift, event.ctrlOrCmd || Pressing.overrides.ctrl); - } - - moveElementsInSpace(difference, axes[index]); - updateSelection(); - - Undo.finishEdit('Move elements') -} -//Rotate -function rotateSelected(axis, steps) { - let affected = [...Cube.selected, ...Mesh.selected]; - if (!affected.length) return; - Undo.initEdit({elements: affected}); - if (!steps) steps = 1 - var origin = [8, 8, 8] - if (Group.selected && Format.bone_rig) { - origin = Group.selected.origin.slice() - } else if (Format.centered_grid) { - origin = [0, 0, 0] - } else { - origin = affected[0].origin.slice() - } - affected.forEach(function(obj) { - obj.roll(axis, steps, origin) - }) - updateSelection(); - Undo.finishEdit('Rotate elements') -} -//Mirror -function mirrorSelected(axis) { - if (Modes.animate && Timeline.selected.length) { - - Undo.initEdit({keyframes: Timeline.selected}) - for (var kf of Timeline.selected) { - kf.flip(axis) - } - Undo.finishEdit('Flipped keyframes'); - updateKeyframeSelection(); - Animator.preview(); - - } else if (Modes.edit && (Outliner.selected.length || Group.selected)) { - Undo.initEdit({elements: selected, outliner: Format.bone_rig || Group.selected, selection: true}) - var center = Format.centered_grid ? 0 : 8; - if (Format.bone_rig) { - let flip_pairs = { - 0: { - right: 'left', - Right: 'Left', - RIGHT: 'LEFT', - }, - 1: { - top: 'bottom', - Top: 'Bottom', - TOP: 'BOTTOM', - }, - 2: { - back: 'front', - rear: 'front', - Back: 'Front', - Rear: 'Front', - BACK: 'FRONT', - REAR: 'FRONT', - } - } - if (Group.selected && Group.selected.matchesSelection()) { - function flipGroup(group) { - if (group.type === 'group') { - for (var i = 0; i < 3; i++) { - if (i === axis) { - group.origin[i] *= -1 - } else { - group.rotation[i] *= -1 - } - } - function matchAndReplace(a, b) { - if (group.name.includes(a)) { - let name = group._original_name - ? group._original_name.replace(a, b) - : group.name.replace(a, b).replace(/2/, ''); - if (!Group.all.find(g => g.name == name)) group.name = name; - return true; - } - } - let pairs = flip_pairs[axis]; - for (let a in pairs) { - let b = pairs[a]; - if (matchAndReplace(a, b)) break; - if (matchAndReplace(b, a)) break; - } - } - Canvas.updateAllBones([group]); - } - flipGroup(Group.selected) - Group.selected.forEachChild(flipGroup) - } - } - selected.forEach(function(obj) { - obj.flip(axis, center, false) - if (Project.box_uv && obj instanceof Cube && axis === 0) { - obj.shade = !obj.shade - Canvas.updateUV(obj) - } - }) - updateSelection() - Undo.finishEdit('Flip selection') - } -} - -const Vertexsnap = { - step1: true, - line: new THREE.Line(new THREE.BufferGeometry(), Canvas.outlineMaterial), - elements_with_vertex_gizmos: [], - hovering: false, - addVertices: function(element) { - if (Vertexsnap.elements_with_vertex_gizmos.includes(element)) return; - if (element.visibility === false) return; - let {mesh} = element; - - $('#preview').get(0).removeEventListener("mousemove", Vertexsnap.hoverCanvas) - $('#preview').get(0).addEventListener("mousemove", Vertexsnap.hoverCanvas) - - if (!mesh.vertex_points) { - mesh.updateMatrixWorld() - let vectors = []; - if (mesh.geometry) { - let positions = mesh.geometry.attributes.position.array; - for (let i = 0; i < positions.length; i += 3) { - let vec = [positions[i], positions[i+1], positions[i+2]]; - if (!vectors.find(vec2 => vec.equals(vec2))) { - vectors.push(vec); - } - } - } - vectors.push([0, 0, 0]); - - let points = new THREE.Points(new THREE.BufferGeometry(), new THREE.PointsMaterial().copy(Canvas.meshVertexMaterial)); - points.element_uuid = element.uuid; - points.vertices = vectors; - let vector_positions = []; - vectors.forEach(vector => vector_positions.push(...vector)); - let vector_colors = []; - vectors.forEach(vector => vector_colors.push(gizmo_colors.grid.r, gizmo_colors.grid.g, gizmo_colors.grid.b)); - points.geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vector_positions), 3)); - points.geometry.setAttribute('color', new THREE.Float32BufferAttribute(new Float32Array(vector_colors), 3)); - points.material.transparent = true; - mesh.vertex_points = points; - if (mesh.outline) { - mesh.outline.add(points); - } else { - mesh.add(points); - } - } - mesh.vertex_points.visible = true; - mesh.vertex_points.renderOrder = 900; - - Vertexsnap.elements_with_vertex_gizmos.push(element) - }, - clearVertexGizmos: function() { - Project.model_3d.remove(Vertexsnap.line); - Vertexsnap.elements_with_vertex_gizmos.forEach(element => { - if (element.mesh && element.mesh.vertex_points) { - element.mesh.vertex_points.visible = false; - if (element instanceof Mesh == false) { - element.mesh.vertex_points.parent.remove(element.mesh.vertex_points); - delete element.mesh.vertex_points; - } - } - - }) - Vertexsnap.elements_with_vertex_gizmos.empty(); - $('#preview').get(0).removeEventListener("mousemove", Vertexsnap.hoverCanvas) - }, - hoverCanvas: function(event) { - let data = Canvas.raycast(event) - - if (Vertexsnap.hovering) { - Project.model_3d.remove(Vertexsnap.line); - Vertexsnap.elements_with_vertex_gizmos.forEach(el => { - let points = el.mesh.vertex_points; - let colors = []; - for (let i = 0; i < points.geometry.attributes.position.count; i++) { - let color; - if (data && data.element == el && data.type == 'vertex' && data.vertex_index == i) { - color = gizmo_colors.outline; - } else { - color = gizmo_colors.grid; - } - colors.push(color.r, color.g, color.b); - } - points.material.depthTest = !(data.element == el); - points.geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)); - }) - } - if (!data || data.type !== 'vertex') { - Blockbench.setStatusBarText() - return; - } - Vertexsnap.hovering = true - - if (Vertexsnap.step1 === false) { - let {line} = Vertexsnap; - let {geometry} = line; - - let vertex_pos = Vertexsnap.getGlobalVertexPos(data.element, data.vertex); - geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array([...Vertexsnap.vertex_pos.toArray(), ...vertex_pos.toArray()]), 3)); - - line.renderOrder = 900 - Project.model_3d.add(Vertexsnap.line); - Vertexsnap.line.position.copy(scene.position).multiplyScalar(-1); - //Measure - var diff = new THREE.Vector3().copy(Vertexsnap.vertex_pos); - diff.sub(vertex_pos); - Blockbench.setStatusBarText(tl('status_bar.vertex_distance', [trimFloatNumber(diff.length())] )); - } - }, - select: function() { - Vertexsnap.clearVertexGizmos() - Outliner.selected.forEach(function(element) { - Vertexsnap.addVertices(element) - }) - if (Group.selected) { - Vertexsnap.addVertices(Group.selected) - } - if (Outliner.selected.length) { - $('#preview').css('cursor', (Vertexsnap.step1 ? 'copy' : 'alias')) - } - }, - canvasClick: function(data) { - if (!data || data.type !== 'vertex') return; - - if (Vertexsnap.step1) { - Vertexsnap.step1 = false - Vertexsnap.vertex_pos = Vertexsnap.getGlobalVertexPos(data.element, data.vertex); - Vertexsnap.vertex_index = data.vertex_index; - Vertexsnap.move_origin = typeof data.vertex !== 'string' && data.vertex.allEqual(0); - Vertexsnap.elements = Outliner.selected.slice(); - Vertexsnap.group = Group.selected; - Vertexsnap.selected_vertices = JSON.parse(JSON.stringify(Project.selected_vertices)); - Vertexsnap.clearVertexGizmos() - $('#preview').css('cursor', (Vertexsnap.step1 ? 'copy' : 'alias')) - - } else { - Vertexsnap.snap(data) - $('#preview').css('cursor', (Vertexsnap.step1 ? 'copy' : 'alias')) - } - Blockbench.setStatusBarText() - }, - getGlobalVertexPos(element, vertex) { - let vector = new THREE.Vector3(); - vector.fromArray(vertex instanceof Array ? vertex : element.vertices[vertex]); - element.mesh.localToWorld(vector); - return vector; - }, - snap: function(data) { - Undo.initEdit({elements: Vertexsnap.elements, outliner: !!Vertexsnap.group}); - - let mode = BarItems.vertex_snap_mode.get(); - - if (Vertexsnap.move_origin) { - if (Vertexsnap.group) { - let vec = Vertexsnap.getGlobalVertexPos(data.element, data.vertex); - - if (Format.bone_rig && Vertexsnap.group.parent instanceof Group && Vertexsnap.group.mesh.parent) { - Vertexsnap.group.mesh.parent.worldToLocal(vec); - } - let vec_array = vec.toArray() - vec_array.V3_add(Vertexsnap.group.parent.origin); - Vertexsnap.group.transferOrigin(vec_array) - - } else { - Vertexsnap.elements.forEach(function(element) { - let vec = Vertexsnap.getGlobalVertexPos(data.element, data.vertex); - - if (Format.bone_rig && element.parent instanceof Group && element.mesh.parent) { - element.mesh.parent.worldToLocal(vec); - } - let vec_array = vec.toArray() - vec_array.V3_add(element.parent.origin); - element.transferOrigin(vec_array) - }) - } - } else { - - var global_delta = Vertexsnap.getGlobalVertexPos(data.element, data.vertex); - global_delta.sub(Vertexsnap.vertex_pos) - - if (mode === 'scale' && !Format.integer_size && Vertexsnap.elements[0] instanceof Cube) { - //Scale - - var m; - switch (Vertexsnap.vertex_index) { - case 0: m=[ 1,1,1 ]; break; - case 1: m=[ 1,1,0 ]; break; - case 2: m=[ 1,0,1 ]; break; - case 3: m=[ 1,0,0 ]; break; - case 4: m=[ 0,1,0 ]; break; - case 5: m=[ 0,1,1 ]; break; - case 6: m=[ 0,0,0 ]; break; - case 7: m=[ 0,0,1 ]; break; - } - - Vertexsnap.elements.forEach(function(obj) { - if (obj instanceof Cube == false) return; - var q = obj.mesh.getWorldQuaternion(new THREE.Quaternion()).invert() - var cube_pos = new THREE.Vector3().copy(global_delta).applyQuaternion(q) - - for (i=0; i<3; i++) { - if (m[i] === 1) { - obj.to[i] = limitToBox(obj.to[i] + cube_pos.getComponent(i), obj.inflate) - } else { - obj.from[i] = limitToBox(obj.from[i] + cube_pos.getComponent(i), -obj.inflate) - } - } - if (Project.box_uv && obj.visibility) { - Canvas.updateUV(obj) - } - }) - } else if (mode === 'move') { - Vertexsnap.elements.forEach(function(obj) { - var cube_pos = new THREE.Vector3().copy(global_delta) - - if (obj instanceof Mesh && Vertexsnap.selected_vertices && Vertexsnap.selected_vertices[obj.uuid]) { - let vertices = Vertexsnap.selected_vertices[obj.uuid]; - var q = obj.mesh.getWorldQuaternion(Reusable.quat1).invert(); - cube_pos.applyQuaternion(q); - let cube_pos_array = cube_pos.toArray(); - vertices.forEach(vkey => { - if (obj.vertices[vkey]) obj.vertices[vkey].V3_add(cube_pos_array); - }) - - } else { - if (Format.bone_rig && obj.parent instanceof Group) { - var q = obj.mesh.parent.getWorldQuaternion(Reusable.quat1).invert(); - cube_pos.applyQuaternion(q); - } - if (obj instanceof Cube && Format.rotate_cubes) { - obj.origin.V3_add(cube_pos); - } - var in_box = obj.moveVector(cube_pos.toArray()); - if (!in_box && Format.canvas_limit && !settings.deactivate_size_limit.value) { - Blockbench.showMessageBox({translateKey: 'canvas_limit_error'}) - } - } - }) - } - - } - - Vertexsnap.clearVertexGizmos() - let update_options = { - elements: Vertexsnap.elements, - element_aspects: {transform: true, geometry: true}, - }; - if (Vertexsnap.group) { - update_options.elements = [...update_options.elements]; - Vertexsnap.group.forEachChild(child => { - update_options.elements.safePush(child); - }, OutlinerElement); - update_options.groups = [Vertexsnap.group]; - update_options.group_aspects = {transform: true}; - } - Canvas.updateView(update_options); - Undo.finishEdit('Use vertex snap'); - Vertexsnap.step1 = true; - } -} -//Scale -function getScaleAllGroups() { - let groups = []; - if (!Format.bone_rig) return groups; - if (Group.selected) { - Group.selected.forEachChild((g) => { - groups.push(g); - }, Group) - } else if (Outliner.selected.length == Outliner.elements.length && Group.all.length) { - groups = Group.all; - } - return groups; -} -function scaleAll(save, size) { - if (save === true) { - hideDialog() - } - if (size === undefined) { - size = $('#model_scale_label').val() - } - var origin = [ - parseFloat($('#scaling_origin_x').val())||0, - parseFloat($('#scaling_origin_y').val())||0, - parseFloat($('#scaling_origin_z').val())||0, - ] - var overflow = []; - Outliner.selected.forEach(function(obj) { - obj.autouv = 0; - origin.forEach(function(ogn, i) { - if ($('#model_scale_'+getAxisLetter(i)+'_axis').is(':checked')) { - - if (obj.from) { - obj.from[i] = (obj.before.from[i] - obj.inflate - ogn) * size; - if (obj.from[i] + ogn > 32 || obj.from[i] + ogn < -16) overflow.push(obj); - obj.from[i] = limitToBox(obj.from[i] + obj.inflate + ogn, -obj.inflate); - } - - if (obj.to) { - obj.to[i] = (obj.before.to[i] + obj.inflate - ogn) * size; - if (obj.to[i] + ogn > 32 || obj.to[i] + ogn < -16) overflow.push(obj); - obj.to[i] = limitToBox(obj.to[i] - obj.inflate + ogn, obj.inflate); - if (Format.integer_size) { - obj.to[i] = obj.from[i] + Math.round(obj.to[i] - obj.from[i]) - } - } - - if (obj.origin) { - obj.origin[i] = (obj.before.origin[i] - ogn) * size; - obj.origin[i] = obj.origin[i] + ogn; - } - - if (obj instanceof Mesh) { - for (let key in obj.vertices) { - obj.vertices[key][i] = (obj.before.vertices[key][i] - ogn) * size + ogn; - } - } - } else { - - if (obj.from) obj.from[i] = obj.before.from[i]; - if (obj.to) obj.to[i] = obj.before.to[i]; - - if (obj.origin) obj.origin[i] = obj.before.origin[i]; - - if (obj instanceof Mesh) { - for (let key in obj.vertices) { - obj.vertices[key][i] = obj.before.vertices[key][i]; - } - } - } - }) - if (save === true) { - delete obj.before - } - if (Project.box_uv) { - Canvas.updateUV(obj) - } - }) - getScaleAllGroups().forEach((g) => { - g.origin[0] = g.old_origin[0] * size - g.origin[1] = g.old_origin[1] * size - g.origin[2] = g.old_origin[2] * size - if (save === true) { - delete g.old_origin - } - }, Group) - if (overflow.length && Format.canvas_limit && !settings.deactivate_size_limit.value) { - scaleAll.overflow = overflow; - $('#scaling_clipping_warning').text('Model clipping: Your model is too large for the canvas') - $('#scale_overflow_btn').css('display', 'inline-block') - } else { - $('#scaling_clipping_warning').text('') - $('#scale_overflow_btn').hide() - } - Canvas.updateView({ - elements: Outliner.selected, - element_aspects: {geometry: true, transform: true}, - groups: getScaleAllGroups(), - group_aspects: {transform: true}, - }) - if (save === true) { - Undo.finishEdit('Scale model') - } -} -function modelScaleSync(label) { - if (label) { - var size = $('#model_scale_label').val() - $('#model_scale_range').val(size) - } else { - var size = $('#model_scale_range').val() - $('#model_scale_label').val(size) - } - scaleAll(false, size) -} -function cancelScaleAll() { - Outliner.selected.forEach(function(obj) { - if (obj === undefined) return; - if (obj.from) obj.from.V3_set(obj.before.from); - if (obj.to) obj.to.V3_set(obj.before.to); - if (obj.origin) obj.origin.V3_set(obj.before.origin); - if (obj instanceof Mesh) { - for (let key in obj.vertices) { - obj.vertices[key].V3_set(obj.before.vertices[key]); - } - } - delete obj.before - if (Project.box_uv) { - Canvas.updateUV(obj) - } - }) - getScaleAllGroups().forEach((g) => { - g.origin[0] = g.old_origin[0] - g.origin[1] = g.old_origin[1] - g.origin[2] = g.old_origin[2] - delete g.old_origin - }, Group) - Canvas.updateView({ - elements: Outliner.selected, - element_aspects: {geometry: true, transform: true}, - groups: getScaleAllGroups(), - group_aspects: {transform: true}, - }) - hideDialog() -} -function setScaleAllPivot(mode) { - if (mode === 'selection') { - var center = getSelectionCenter() - } else { - var center = Cube.selected[0] && Cube.selected[0].origin; - } - if (center) { - $('input#scaling_origin_x').val(center[0]); - $('input#scaling_origin_y').val(center[1]); - $('input#scaling_origin_z').val(center[2]); - } -} -function scaleAllSelectOverflow() { - cancelScaleAll() - selected.empty(); - scaleAll.overflow.forEach(obj => { - obj.selectLow() - }) - updateSelection(); -} -//Center -function centerElementsAll(axis) { - centerElements(0, false) - centerElements(1, false) - centerElements(2, false) -} -function centerElements(axis, update) { - if (!Outliner.selected.length) return; - let center = getSelectionCenter()[axis]; - var difference = (Format.centered_grid ? 0 : 8) - center - - Outliner.selected.forEach(function(obj) { - if (obj.movable) obj.origin[axis] += difference; - if (obj.to) obj.to[axis] = limitToBox(obj.to[axis] + difference, obj.inflate); - if (obj instanceof Cube) obj.from[axis] = limitToBox(obj.from[axis] + difference, obj.inflate); - }) - Group.all.forEach(group => { - if (!group.selected) return; - group.origin[axis] += difference; - }) - Canvas.updateView({ - elements: Outliner.selected, - groups: Group.all.filter(g => g.selected), - element_aspects: {transform: true}, - group_aspects: {transform: true}, - selection: true - }) -} - -//Move -function moveElementsInSpace(difference, axis) { - let space = Transformer.getTransformSpace() - let group = Format.bone_rig && Group.selected && Group.selected.matchesSelection() && Group.selected; - var group_m; - let quaternion = new THREE.Quaternion(); - let vector = new THREE.Vector3(); - - if (group) { - if (space === 0) { - group_m = vector.set(0, 0, 0); - group_m[getAxisLetter(axis)] = difference; - - var rotation = new THREE.Quaternion(); - group.mesh.parent.getWorldQuaternion(rotation); - group_m.applyQuaternion(rotation.invert()); - - group.forEachChild(g => { - g.origin.V3_add(group_m.x, group_m.y, group_m.z); - }, Group, true) - - } else if (space === 2) { - group_m = new THREE.Vector3(); - group_m[getAxisLetter(axis)] = difference; - - group_m.applyQuaternion(group.mesh.quaternion); - - group.forEachChild(g => { - g.origin.V3_add(group_m.x, group_m.y, group_m.z); - }, Group, true) - - } else { - group.forEachChild(g => { - g.origin[axis] += difference - }, Group, true) - } - Canvas.updateAllBones([Group.selected]); - } - - Outliner.selected.forEach(el => { - - if (!group_m && el instanceof Mesh && (el.getSelectedVertices().length > 0 || space >= 2)) { - - let selection_rotation = space == 3 && el.getSelectionRotation(); - let selected_vertices = el.getSelectedVertices(); - if (!selected_vertices.length) selected_vertices = Object.keys(el.vertices) - selected_vertices.forEach(key => { - - if (space == 2) { - el.vertices[key][axis] += difference; - - } else if (space == 3) { - let m = vector.set(0, 0, 0); - m[getAxisLetter(axis)] = difference; - m.applyEuler(selection_rotation); - el.vertices[key].V3_add(m.x, m.y, m.z); - - } else { - let m = vector.set(0, 0, 0); - m[getAxisLetter(axis)] = difference; - m.applyQuaternion(el.mesh.getWorldQuaternion(quaternion).invert()); - el.vertices[key].V3_add(m.x, m.y, m.z); - } - - }) - - } else { - - if (space == 2 && !group_m) { - if (el instanceof Locator) { - let m = vector.set(0, 0, 0); - m[getAxisLetter(axis)] = difference; - m.applyQuaternion(el.mesh.quaternion); - el.from.V3_add(m.x, m.y, m.z); - - } else if (el instanceof TextureMesh) { - el.local_pivot[axis] += difference; - - } else { - if (el.movable) el.from[axis] += difference; - if (el.resizable && el.to) el.to[axis] += difference; - } - - } else if (space instanceof Group) { - if (el.movable && el instanceof Mesh == false) el.from[axis] += difference; - if (el.resizable && el.to) el.to[axis] += difference; - if (el.rotatable && el instanceof Locator == false) el.origin[axis] += difference; - } else { - let move_origin = !!group; - if (group_m) { - var m = group_m - } else { - var m = vector.set(0, 0, 0); - m[getAxisLetter(axis)] = difference; - - let parent = el.parent; - while (parent instanceof Group) { - if (!parent.rotation.allEqual(0)) break; - parent = parent.parent; - } - - if (parent == 'root') { - // If none of the parent groups are rotated, move origin. - move_origin = true; - } else { - var rotation = new THREE.Quaternion(); - if (el.mesh && el instanceof Locator == false && el instanceof Mesh == false) { - el.mesh.getWorldQuaternion(rotation); - } else if (el.parent instanceof Group) { - el.parent.mesh.getWorldQuaternion(rotation); - } - m.applyQuaternion(rotation.invert()); - } - } - - if (el.movable && (el instanceof Mesh == false || !move_origin)) el.from.V3_add(m.x, m.y, m.z); - if (el.resizable && el.to) el.to.V3_add(m.x, m.y, m.z); - if (move_origin) { - if (el.rotatable && el instanceof Locator == false && el instanceof TextureMesh == false) el.origin.V3_add(m.x, m.y, m.z); - } - } - } - if (el instanceof Cube) { - el.mapAutoUV() - } - }) - Canvas.updateView({ - elements: Outliner.selected, - element_aspects: {transform: true, geometry: true}, - groups: Group.all.filter(g => g.selected), - group_aspects: {transform: true} - }) -} - -//Rotate -function getRotationInterval(event) { - if (Format.rotation_limit) { - return 22.5; - } else if ((event.shiftKey || Pressing.overrides.shift) && (event.ctrlOrCmd || Pressing.overrides.ctrl)) { - return 0.25; - } else if (event.shiftKey || Pressing.overrides.shift) { - return 22.5; - } else if (event.ctrlOrCmd || Pressing.overrides.ctrl) { - return 1; - } else { - return 2.5; - } -} -function getRotationObject() { - if (Format.bone_rig && Group.selected) return Group.selected; - let elements = Outliner.selected.filter(element => { - return element.rotatable && (element instanceof Cube == false || Format.rotate_cubes); - }) - if (elements.length) return elements; -} -function rotateOnAxis(modify, axis, slider) { - var things = getRotationObject(); - if (!things) return; - if (things instanceof Array == false) things = [things]; - /* - if (Format.bone_rig && Group.selected) { - if (!Group.selected) return; - let obj = Group.selected.mesh - - if (typeof space == 'object') { - let normal = axis == 0 ? THREE.NormalX : (axis == 1 ? THREE.NormalY : THREE.NormalZ) - let rotWorldMatrix = new THREE.Matrix4(); - rotWorldMatrix.makeRotationAxis(normal, Math.degToRad(modify(0))) - rotWorldMatrix.multiply(obj.matrix) - obj.matrix.copy(rotWorldMatrix) - obj.setRotationFromMatrix(rotWorldMatrix) - let e = obj.rotation; - Group.selected.rotation[0] = Math.radToDeg(e.x); - Group.selected.rotation[1] = Math.radToDeg(e.y); - Group.selected.rotation[2] = Math.radToDeg(e.z); - Canvas.updateAllBones() - - } else if (space == 0) { - let normal = axis == 0 ? THREE.NormalX : (axis == 1 ? THREE.NormalY : THREE.NormalZ) - let rotWorldMatrix = new THREE.Matrix4(); - rotWorldMatrix.makeRotationAxis(normal, Math.degToRad(modify(0))) - rotWorldMatrix.multiply(obj.matrixWorld) - - let inverse = new THREE.Matrix4().copy(obj.parent.matrixWorld).invert() - rotWorldMatrix.premultiply(inverse) - - obj.matrix.copy(rotWorldMatrix) - obj.setRotationFromMatrix(rotWorldMatrix) - let e = obj.rotation; - Group.selected.rotation[0] = Math.radToDeg(e.x); - Group.selected.rotation[1] = Math.radToDeg(e.y); - Group.selected.rotation[2] = Math.radToDeg(e.z); - Canvas.updateAllBones() - - } else { - var value = modify(Group.selected.rotation[axis]); - Group.selected.rotation[axis] = Math.trimDeg(value) - Canvas.updateAllBones() - } - return; - } - */ - //Warning - if (Format.rotation_limit && settings.dialog_rotation_limit.value) { - var i = 0; - while (i < Cube.selected.length) { - if (Cube.selected[i].rotation[(axis+1)%3] || - Cube.selected[i].rotation[(axis+2)%3] - ) { - i = Infinity - - Blockbench.showMessageBox({ - title: tl('message.rotation_limit.title'), - icon: 'rotate_right', - message: tl('message.rotation_limit.message'), - buttons: [tl('dialog.ok'), tl('dialog.dontshowagain')] - }, function(r) { - if (r === 1) { - settings.dialog_rotation_limit.value = false - Settings.save() - } - }) - return; - //Gotta stop the numslider here - } - i++; - } - } - var axis_letter = getAxisLetter(axis) - var origin = things[0].origin - things.forEach(function(obj, i) { - if (!obj.rotation.allEqual(0)) { - origin = obj.origin - } - }) - - let space = Transformer.getTransformSpace() - if (axis instanceof THREE.Vector3) space = 0; - things.forEach(obj => { - let mesh = obj.mesh; - if (obj instanceof Cube && !Format.bone_rig) { - if (obj.origin.allEqual(0)) { - obj.origin.V3_set(origin) - } - } - - if (!Group.selected && obj instanceof Mesh && Project.selected_vertices[obj.uuid] && Project.selected_vertices[obj.uuid].length > 0) { - - let normal = axis == 0 ? THREE.NormalX : (axis == 1 ? THREE.NormalY : THREE.NormalZ) - let rotWorldMatrix = new THREE.Matrix4(); - rotWorldMatrix.makeRotationAxis(normal, Math.degToRad(modify(0))) - if (space instanceof Group || space == 'root') { - rotWorldMatrix.multiply(mesh.matrix); - } else if (space == 0) { - rotWorldMatrix.multiply(mesh.matrixWorld); - } - let q = new THREE.Quaternion().setFromRotationMatrix(rotWorldMatrix); - if (space instanceof Group || space == 'root') { - q.premultiply(mesh.quaternion.invert()); - mesh.quaternion.invert(); - } else if (space == 0) { - let quat = mesh.getWorldQuaternion(new THREE.Quaternion()).invert(); - q.premultiply(quat); - } - - let vector = new THREE.Vector3(); - let local_pivot = obj.mesh.worldToLocal(new THREE.Vector3().copy(Transformer.position)) - - Project.selected_vertices[obj.uuid].forEach(key => { - vector.fromArray(obj.vertices[key]); - vector.sub(local_pivot); - vector.applyQuaternion(q); - vector.add(local_pivot); - obj.vertices[key].V3_set(vector.x, vector.y, vector.z); - }) - - } else if (slider || (space == 2 && Format.rotation_limit)) { - var obj_val = modify(obj.rotation[axis]); - obj_val = Math.trimDeg(obj_val) - if (Format.rotation_limit && obj instanceof Cube) { - //Limit To 1 Axis - obj.rotation[(axis+1)%3] = 0 - obj.rotation[(axis+2)%3] = 0 - //Limit Angle - obj_val = Math.round(obj_val/22.5)*22.5 - if (obj_val > 45 || obj_val < -45) { - - let f = obj_val > 45 - let can_roll = obj.roll(axis, f!=(axis==1) ? 1 : 3); - if (can_roll) { - obj_val = f ? -22.5 : 22.5; - } else { - obj_val = Math.clamp(obj_val, -45, 45); - } - } - } - obj.rotation[axis] = obj_val - if (obj instanceof Cube) { - obj.rotation_axis = axis_letter - } - } else if (space == 2) { - if ([0, 1, 2].find(axis2 => axis2 !== axis && Math.abs(obj.rotation[axis2]) > 0.1) !== undefined) { - let old_order = mesh.rotation.order; - mesh.rotation.reorder(axis == 0 ? 'ZYX' : (axis == 1 ? 'ZXY' : 'XYZ')) - var obj_val = modify(Math.radToDeg(mesh.rotation[axis_letter])); - obj_val = Math.trimDeg(obj_val) - mesh.rotation[axis_letter] = Math.degToRad(obj_val); - mesh.rotation.reorder(old_order); - - obj.rotation[0] = Math.radToDeg(mesh.rotation.x); - obj.rotation[1] = Math.radToDeg(mesh.rotation.y); - obj.rotation[2] = Math.radToDeg(mesh.rotation.z); - } else { - var obj_val = modify(Math.radToDeg(mesh.rotation[axis_letter])); - obj.rotation[axis] = Math.trimDeg(obj_val); - } - - } else if (space instanceof Group) { - let normal = axis == 0 ? THREE.NormalX : (axis == 1 ? THREE.NormalY : THREE.NormalZ) - let rotWorldMatrix = new THREE.Matrix4(); - rotWorldMatrix.makeRotationAxis(normal, Math.degToRad(modify(0))) - rotWorldMatrix.multiply(mesh.matrix) - mesh.matrix.copy(rotWorldMatrix) - mesh.setRotationFromMatrix(rotWorldMatrix) - let e = mesh.rotation; - obj.rotation[0] = Math.radToDeg(e.x); - obj.rotation[1] = Math.radToDeg(e.y); - obj.rotation[2] = Math.radToDeg(e.z); - - } else if (space == 0) { - let normal = axis instanceof THREE.Vector3 - ? axis - : axis == 0 ? THREE.NormalX : (axis == 1 ? THREE.NormalY : THREE.NormalZ) - let rotWorldMatrix = new THREE.Matrix4(); - rotWorldMatrix.makeRotationAxis(normal, Math.degToRad(modify(0))) - rotWorldMatrix.multiply(mesh.matrixWorld) - - let inverse = new THREE.Matrix4().copy(mesh.parent.matrixWorld).invert() - rotWorldMatrix.premultiply(inverse) - - mesh.matrix.copy(rotWorldMatrix) - mesh.setRotationFromMatrix(rotWorldMatrix) - let e = mesh.rotation; - obj.rotation[0] = Math.radToDeg(e.x); - obj.rotation[1] = Math.radToDeg(e.y); - obj.rotation[2] = Math.radToDeg(e.z); - - } - if (obj instanceof Group) { - Canvas.updateView({groups: [obj]}); - } - }) -} - -BARS.defineActions(function() { - - - new BarSelect('transform_space', { - condition: { - modes: ['edit', 'animate'], - tools: ['move_tool', 'pivot_tool', 'resize_tool'], - method: () => !(Toolbox && Toolbox.selected.id === 'resize_tool' && Mesh.all.length === 0) - }, - category: 'transform', - value: 'local', - options: { - global: true, - bone: {condition: () => Format.bone_rig, name: true}, - local: true, - normal: {condition: () => Mesh.selected.length, name: true} - }, - onChange() { - updateSelection(); - } - }) - new BarSelect('rotation_space', { - condition: {modes: ['edit', 'animate', 'pose'], tools: ['rotate_tool']}, - category: 'transform', - value: 'local', - options: { - global: 'action.transform_space.global', - bone: {condition: () => Format.bone_rig, name: true, name: 'action.transform_space.bone'}, - local: 'action.transform_space.local' - }, - onChange() { - updateSelection(); - } - }) - let grid_locked_interval = function(event) { - event = event||0; - return canvasGridSize(event.shiftKey || Pressing.overrides.shift, event.ctrlOrCmd || Pressing.overrides.ctrl); - } - - function moveOnAxis(modify, axis) { - selected.forEach(function(obj, i) { - if (obj instanceof Mesh && obj.getSelectedVertices().length) { - - let vertices = obj.getSelectedVertices(); - vertices.forEach(vkey => { - obj.vertices[vkey][axis] = modify(obj.vertices[vkey][axis]); - }) - obj.preview_controller.updateGeometry(obj); - - } else if (obj.movable) { - var val = modify(obj.from[axis]); - - if (Format.canvas_limit && !settings.deactivate_size_limit.value) { - var size = obj.to ? obj.size(axis) : 0; - val = limitToBox(limitToBox(val, -obj.inflate) + size, obj.inflate) - size - } - - var before = obj.from[axis]; - obj.from[axis] = val; - if (obj.to) { - obj.to[axis] += (val - before); - } - if (obj instanceof Cube) { - obj.mapAutoUV() - } - obj.preview_controller.updateTransform(obj); - if (obj.preview_controller.updateGeometry) obj.preview_controller.updateGeometry(obj); - } - }) - TickUpdates.selection = true; - } - function getPos(axis) { - let element = Outliner.selected[0]; - if (element instanceof Mesh && element.getSelectedVertices().length) { - let vertices = element.getSelectedVertices(); - let sum = 0; - vertices.forEach(vkey => sum += element.vertices[vkey][axis]); - return sum / vertices.length; - - } else if (element instanceof Cube) { - return element.from[axis]; - } else { - return element.origin[axis] - } - } - new NumSlider('slider_pos_x', { - name: tl('action.slider_pos', ['X']), - description: tl('action.slider_pos.desc', ['X']), - color: 'x', - category: 'transform', - condition: () => (selected.length && Modes.edit), - getInterval: grid_locked_interval, - get: function() { - return getPos(0); - }, - change: function(modify) { - moveOnAxis(modify, 0) - }, - onBefore: function() { - Undo.initEdit({elements: selected}) - }, - onAfter: function() { - Undo.finishEdit('Change element position') - } - }) - new NumSlider('slider_pos_y', { - name: tl('action.slider_pos', ['Y']), - description: tl('action.slider_pos.desc', ['Y']), - color: 'y', - category: 'transform', - condition: () => (selected.length && Modes.edit), - getInterval: grid_locked_interval, - get: function() { - return getPos(1); - }, - change: function(modify) { - moveOnAxis(modify, 1) - }, - onBefore: function() { - Undo.initEdit({elements: selected}) - }, - onAfter: function() { - Undo.finishEdit('Change element position') - } - }) - new NumSlider('slider_pos_z', { - name: tl('action.slider_pos', ['Z']), - description: tl('action.slider_pos.desc', ['Z']), - color: 'z', - category: 'transform', - condition: () => (selected.length && Modes.edit), - getInterval: grid_locked_interval, - get: function() { - return getPos(2); - }, - change: function(modify) { - moveOnAxis(modify, 2) - }, - onBefore: function() { - Undo.initEdit({elements: selected}) - }, - onAfter: function() { - Undo.finishEdit('Change element position') - } - }) - let slider_vector_pos = [BarItems.slider_pos_x, BarItems.slider_pos_y, BarItems.slider_pos_z]; - slider_vector_pos.forEach(slider => slider.slider_vector = slider_vector_pos); - - - function resizeOnAxis(modify, axis) { - selected.forEach(function(obj, i) { - if (obj.resizable) { - obj.resize(modify, axis, false, true) - } else if (obj.scalable) { - obj.scale[axis] = modify(obj.scale[axis]); - obj.preview_controller.updateTransform(obj); - if (obj.preview_controller.updateGeometry) obj.preview_controller.updateGeometry(obj); - } - }) - } - new NumSlider('slider_size_x', { - name: tl('action.slider_size', ['X']), - description: tl('action.slider_size.desc', ['X']), - color: 'x', - category: 'transform', - condition: () => (Outliner.selected[0] && (Outliner.selected[0].resizable || Outliner.selected[0].scalable) && Outliner.selected[0] instanceof Mesh == false && Modes.edit), - getInterval: grid_locked_interval, - get: function() { - if (Outliner.selected[0].scalable) { - return Outliner.selected[0].scale[0] - } else if (Outliner.selected[0].resizable) { - return Outliner.selected[0].to[0] - Outliner.selected[0].from[0] - } - }, - change: function(modify) { - resizeOnAxis(modify, 0) - }, - onBefore: function() { - Undo.initEdit({elements: Cube.selected}) - }, - onAfter: function() { - Undo.finishEdit('Change element size') - } - }) - new NumSlider('slider_size_y', { - name: tl('action.slider_size', ['Y']), - description: tl('action.slider_size.desc', ['Y']), - color: 'y', - category: 'transform', - condition: () => (Outliner.selected[0] && (Outliner.selected[0].resizable || Outliner.selected[0].scalable) && Outliner.selected[0] instanceof Mesh == false && Modes.edit), - getInterval: grid_locked_interval, - get: function() { - if (Outliner.selected[0].scalable) { - return Outliner.selected[0].scale[1] - } else if (Outliner.selected[0].resizable) { - return Outliner.selected[0].to[1] - Outliner.selected[0].from[1] - } - }, - change: function(modify) { - resizeOnAxis(modify, 1) - }, - onBefore: function() { - Undo.initEdit({elements: Cube.selected}) - }, - onAfter: function() { - Undo.finishEdit('Change element size') - } - }) - new NumSlider('slider_size_z', { - name: tl('action.slider_size', ['Z']), - description: tl('action.slider_size.desc', ['Z']), - color: 'z', - category: 'transform', - condition: () => (Outliner.selected[0] && (Outliner.selected[0].resizable || Outliner.selected[0].scalable)&& Outliner.selected[0] instanceof Mesh == false && Modes.edit), - getInterval: grid_locked_interval, - get: function() { - if (Outliner.selected[0].scalable) { - return Outliner.selected[0].scale[2] - } else if (Outliner.selected[0].resizable) { - return Outliner.selected[0].to[2] - Outliner.selected[0].from[2] - } - }, - change: function(modify) { - resizeOnAxis(modify, 2) - }, - onBefore: function() { - Undo.initEdit({elements: Cube.selected}) - }, - onAfter: function() { - Undo.finishEdit('Change element size') - } - }) - let slider_vector_size = [BarItems.slider_size_x, BarItems.slider_size_y, BarItems.slider_size_z]; - slider_vector_size.forEach(slider => slider.slider_vector = slider_vector_size); - //Inflate - new NumSlider('slider_inflate', { - category: 'transform', - condition: function() {return Cube.selected.length && Modes.edit}, - getInterval: grid_locked_interval, - get: function() { - return Cube.selected[0].inflate - }, - change: function(modify) { - Cube.selected.forEach(function(obj, i) { - var v = modify(obj.inflate) - if (Format.canvas_limit && !settings.deactivate_size_limit.value) { - v = obj.from[0] - Math.clamp(obj.from[0]-v, -16, 32); - v = obj.from[1] - Math.clamp(obj.from[1]-v, -16, 32); - v = obj.from[2] - Math.clamp(obj.from[2]-v, -16, 32); - v = Math.clamp(obj.to[0]+v, -16, 32) - obj.to[0]; - v = Math.clamp(obj.to[1]+v, -16, 32) - obj.to[1]; - v = Math.clamp(obj.to[2]+v, -16, 32) - obj.to[2]; - } - obj.inflate = v - }) - Canvas.updatePositions() - }, - onBefore: function() { - Undo.initEdit({elements: Cube.selected}) - }, - onAfter: function() { - Undo.finishEdit('Inflate elements') - } - }) - - //Rotation - new NumSlider('slider_rotation_x', { - name: tl('action.slider_rotation', ['X']), - description: tl('action.slider_rotation.desc', ['X']), - color: 'x', - category: 'transform', - condition: () => ((Modes.edit || Modes.pose) && getRotationObject()), - get: function() { - if (Format.bone_rig && Group.selected) { - return Group.selected.rotation[0]; - } - let ref = Outliner.selected.find(el => { - return el.rotatable && (Format.rotate_cubes || el instanceof Cube == false) - }) - if (ref) return ref.rotation[0]; - }, - change: function(modify) { - rotateOnAxis(modify, 0, true) - Canvas.updatePositions() - }, - onBefore: function() { - Undo.initEdit({elements: Outliner.selected.filter(el => el.rotatable), group: Group.selected}) - }, - onAfter: function() { - Undo.finishEdit(getRotationObject() instanceof Group ? 'Rotate group' : 'Rotate elements'); - }, - getInterval: getRotationInterval - }) - new NumSlider('slider_rotation_y', { - name: tl('action.slider_rotation', ['Y']), - description: tl('action.slider_rotation.desc', ['Y']), - color: 'y', - category: 'transform', - condition: () => ((Modes.edit || Modes.pose) && getRotationObject()), - get: function() { - if (Format.bone_rig && Group.selected) { - return Group.selected.rotation[1]; - } - let ref = Outliner.selected.find(el => { - return el.rotatable && (Format.rotate_cubes || el instanceof Cube == false) - }) - if (ref) return ref.rotation[1]; - }, - change: function(modify) { - rotateOnAxis(modify, 1, true) - Canvas.updatePositions() - }, - onBefore: function() { - Undo.initEdit({elements: Outliner.selected.filter(el => el.rotatable), group: Group.selected}) - }, - onAfter: function() { - Undo.finishEdit(getRotationObject() instanceof Group ? 'Rotate group' : 'Rotate elements'); - }, - getInterval: getRotationInterval - }) - new NumSlider('slider_rotation_z', { - name: tl('action.slider_rotation', ['Z']), - description: tl('action.slider_rotation.desc', ['Z']), - color: 'z', - category: 'transform', - condition: () => ((Modes.edit || Modes.pose) && getRotationObject()), - get: function() { - if (Format.bone_rig && Group.selected) { - return Group.selected.rotation[2]; - } - let ref = Outliner.selected.find(el => { - return el.rotatable && (Format.rotate_cubes || el instanceof Cube == false) - }) - if (ref) return ref.rotation[2]; - }, - change: function(modify) { - rotateOnAxis(modify, 2, true) - Canvas.updatePositions() - }, - onBefore: function() { - Undo.initEdit({elements: Outliner.selected.filter(el => el.rotatable), group: Group.selected}) - }, - onAfter: function() { - Undo.finishEdit(getRotationObject() instanceof Group ? 'Rotate group' : 'Rotate elements'); - }, - getInterval: getRotationInterval - }) - let slider_vector_rotation = [BarItems.slider_rotation_x, BarItems.slider_rotation_y, BarItems.slider_rotation_z]; - slider_vector_rotation.forEach(slider => slider.slider_vector = slider_vector_rotation); - - //Origin - function moveOriginOnAxis(modify, axis) { - var rotation_object = getRotationObject() - - if (rotation_object instanceof Group) { - var val = modify(rotation_object.origin[axis]); - rotation_object.origin[axis] = val; - let elements_to_update = []; - rotation_object.forEachChild(element => elements_to_update.push(element), OutlinerElement); - Canvas.updateView({ - groups: [rotation_object], - group_aspects: {transform: true}, - elements: elements_to_update, - element_aspects: {transform: true}, - selection: true - }); - if (Format.bone_rig) { - Canvas.updateAllBones(); - } - } else { - rotation_object.forEach(function(obj, i) { - var val = modify(obj.origin[axis]); - obj.origin[axis] = val; - }) - Canvas.updateView({elements: rotation_object, element_aspects: {transform: true, geometry: true}, selection: true}) - } - if (Modes.animate) { - Animator.preview(); - } - } - new NumSlider('slider_origin_x', { - name: tl('action.slider_origin', ['X']), - description: tl('action.slider_origin.desc', ['X']), - color: 'x', - category: 'transform', - condition: () => (Modes.edit || Modes.animate) && getRotationObject() && (Group.selected || Outliner.selected.length > Locator.selected.length), - getInterval: grid_locked_interval, - get: function() { - if (Format.bone_rig && Group.selected) { - return Group.selected.origin[0]; - } - let ref = Outliner.selected.find(el => { - return el.rotatable && el.origin && (Format.rotate_cubes || el instanceof Cube == false) - }) - if (ref) return ref.origin[0]; - }, - change: function(modify) { - moveOriginOnAxis(modify, 0) - }, - onBefore: function() { - Undo.initEdit({elements: selected, group: Group.selected}) - }, - onAfter: function() { - Undo.finishEdit('Change pivot point') - } - }) - new NumSlider('slider_origin_y', { - name: tl('action.slider_origin', ['Y']), - description: tl('action.slider_origin.desc', ['Y']), - color: 'y', - category: 'transform', - condition: () => (Modes.edit || Modes.animate) && getRotationObject() && (Group.selected || Outliner.selected.length > Locator.selected.length), - getInterval: grid_locked_interval, - get: function() { - if (Format.bone_rig && Group.selected) { - return Group.selected.origin[1]; - } - let ref = Outliner.selected.find(el => { - return el.rotatable && el.origin && (Format.rotate_cubes || el instanceof Cube == false) - }) - if (ref) return ref.origin[1]; - }, - change: function(modify) { - moveOriginOnAxis(modify, 1) - }, - onBefore: function() { - Undo.initEdit({elements: selected, group: Group.selected}) - }, - onAfter: function() { - Undo.finishEdit('Change pivot point') - } - }) - new NumSlider('slider_origin_z', { - name: tl('action.slider_origin', ['Z']), - description: tl('action.slider_origin.desc', ['Z']), - color: 'z', - category: 'transform', - condition: () => (Modes.edit || Modes.animate) && getRotationObject() && (Group.selected || Outliner.selected.length > Locator.selected.length), - getInterval: grid_locked_interval, - get: function() { - if (Format.bone_rig && Group.selected) { - return Group.selected.origin[2]; - } - let ref = Outliner.selected.find(el => { - return el.rotatable && el.origin && (Format.rotate_cubes || el instanceof Cube == false) - }) - if (ref) return ref.origin[2]; - }, - change: function(modify) { - moveOriginOnAxis(modify, 2) - }, - onBefore: function() { - Undo.initEdit({elements: selected, group: Group.selected}) - }, - onAfter: function() { - Undo.finishEdit('Change pivot point') - } - }) - let slider_vector_origin = [BarItems.slider_origin_x, BarItems.slider_origin_y, BarItems.slider_origin_z]; - slider_vector_origin.forEach(slider => slider.slider_vector = slider_vector_origin); - - new Action('scale', { - icon: 'settings_overscan', - category: 'transform', - condition: () => (Modes.edit && selected.length), - click: function () { - $('#model_scale_range, #model_scale_label').val(1) - $('#scaling_clipping_warning').text('') - - Undo.initEdit({elements: Outliner.selected, outliner: Format.bone_rig}) - - Outliner.selected.forEach(function(obj) { - obj.before = { - from: obj.from ? obj.from.slice() : undefined, - to: obj.to ? obj.to.slice() : undefined, - origin: obj.origin ? obj.origin.slice() : undefined - } - if (obj instanceof Mesh) { - obj.before.vertices = {}; - for (let key in obj.vertices) { - obj.before.vertices[key] = obj.vertices[key].slice(); - } - } - }) - getScaleAllGroups().forEach((g) => { - g.old_origin = g.origin.slice(); - }, Group, true) - showDialog('scaling') - var v = Format.centered_grid ? 0 : 8; - var origin = Group.selected ? Group.selected.origin : [v, 0, v]; - $('#scaling_origin_x').val(origin[0]) - $('#scaling_origin_y').val(origin[1]) - $('#scaling_origin_z').val(origin[2]) - scaleAll(false, 1) - } - }) - new Action('rotate_x_cw', { - name: tl('action.rotate_cw', 'X'), - icon: 'rotate_right', - color: 'x', - category: 'transform', - click: function () { - rotateSelected(0, 1); - } - }) - new Action('rotate_x_ccw', { - name: tl('action.rotate_ccw', 'X'), - icon: 'rotate_left', - color: 'x', - category: 'transform', - click: function () { - rotateSelected(0, 3); - } - }) - new Action('rotate_y_cw', { - name: tl('action.rotate_cw', 'Y'), - icon: 'rotate_right', - color: 'y', - category: 'transform', - click: function () { - rotateSelected(1, 1); - } - }) - new Action('rotate_y_ccw', { - name: tl('action.rotate_ccw', 'Y'), - icon: 'rotate_left', - color: 'y', - category: 'transform', - click: function () { - rotateSelected(1, 3); - } - }) - new Action('rotate_z_cw', { - name: tl('action.rotate_cw', 'Z'), - icon: 'rotate_right', - color: 'z', - category: 'transform', - click: function () { - rotateSelected(2, 1); - } - }) - new Action('rotate_z_ccw', { - name: tl('action.rotate_ccw', 'Z'), - icon: 'rotate_left', - color: 'z', - category: 'transform', - click: function () { - rotateSelected(2, 3); - } - }) - - new Action('flip_x', { - name: tl('action.flip', 'X'), - icon: 'icon-mirror_x', - color: 'x', - category: 'transform', - click: function () { - mirrorSelected(0); - } - }) - new Action('flip_y', { - name: tl('action.flip', 'Y'), - icon: 'icon-mirror_y', - color: 'y', - category: 'transform', - click: function () { - mirrorSelected(1); - } - }) - new Action('flip_z', { - name: tl('action.flip', 'Z'), - icon: 'icon-mirror_z', - color: 'z', - category: 'transform', - click: function () { - mirrorSelected(2); - } - }) - - new Action('center_x', { - name: tl('action.center', 'X'), - icon: 'vertical_align_center', - color: 'x', - category: 'transform', - click: function () { - Undo.initEdit({elements: Outliner.selected, outliner: true}); - centerElements(0); - Undo.finishEdit('Center selection on X axis') - } - }) - new Action('center_y', { - name: tl('action.center', 'Y'), - icon: 'vertical_align_center', - color: 'y', - category: 'transform', - click: function () { - Undo.initEdit({elements: Outliner.selected, outliner: true}); - centerElements(1); - Undo.finishEdit('Center selection on Y axis') - } - }) - new Action('center_z', { - name: tl('action.center', 'Z'), - icon: 'vertical_align_center', - color: 'z', - category: 'transform', - click: function () { - Undo.initEdit({elements: Outliner.selected, outliner: true}); - centerElements(2); - Undo.finishEdit('Center selection on Z axis') - } - }) - new Action('center_all', { - icon: 'filter_center_focus', - category: 'transform', - click: function () { - Undo.initEdit({elements: Outliner.selected, outliner: true}); - centerElementsAll(); - Undo.finishEdit('Center selection') - } - }) - - //Move Cube Keys - new Action('move_up', { - icon: 'arrow_upward', - category: 'transform', - condition: {modes: ['edit'], method: () => (!open_menu && selected.length)}, - keybind: new Keybind({key: 38, ctrl: null, shift: null}), - click: function (e) { - if (Prop.active_panel === 'uv') { - UVEditor.moveSelection([0, -1], e) - } else { - moveElementsRelative(-1, 2, e) - } - } - }) - new Action('move_down', { - icon: 'arrow_downward', - category: 'transform', - condition: {modes: ['edit'], method: () => (!open_menu && selected.length)}, - keybind: new Keybind({key: 40, ctrl: null, shift: null}), - click: function (e) { - if (Prop.active_panel === 'uv') { - UVEditor.moveSelection([0, 1], e) - } else { - moveElementsRelative(1, 2, e) - } - } - }) - new Action('move_left', { - icon: 'arrow_back', - category: 'transform', - condition: {modes: ['edit'], method: () => (!open_menu && selected.length)}, - keybind: new Keybind({key: 37, ctrl: null, shift: null}), - click: function (e) { - if (Prop.active_panel === 'uv') { - UVEditor.moveSelection([-1, 0], e) - } else { - moveElementsRelative(-1, 0, e) - } - } - }) - new Action('move_right', { - icon: 'arrow_forward', - category: 'transform', - condition: {modes: ['edit'], method: () => (!open_menu && selected.length)}, - keybind: new Keybind({key: 39, ctrl: null, shift: null}), - click: function (e) { - if (Prop.active_panel === 'uv') { - UVEditor.moveSelection([1, 0], e) - } else { - moveElementsRelative(1, 0, e) - } - } - }) - new Action('move_forth', { - icon: 'keyboard_arrow_up', - category: 'transform', - condition: {modes: ['edit'], method: () => (!open_menu && selected.length)}, - keybind: new Keybind({key: 33, ctrl: null, shift: null}), - click: function (e) {moveElementsRelative(-1, 1, e)} - }) - new Action('move_back', { - icon: 'keyboard_arrow_down', - category: 'transform', - condition: {modes: ['edit'], method: () => (!open_menu && selected.length)}, - keybind: new Keybind({key: 34, ctrl: null, shift: null}), - click: function (e) {moveElementsRelative(1, 1, e)} - }) - - new Action('toggle_visibility', { - icon: 'visibility', - category: 'transform', - click: function () {toggleCubeProperty('visibility')} - }) - new Action('toggle_locked', { - icon: 'fas.fa-lock', - category: 'transform', - click: function () {toggleCubeProperty('locked')} - }) - new Action('toggle_export', { - icon: 'save', - category: 'transform', - click: function () {toggleCubeProperty('export')} - }) - new Action('toggle_autouv', { - icon: 'fullscreen_exit', - category: 'transform', - condition: {modes: ['edit']}, - click: function () {toggleCubeProperty('autouv')} - }) - new Action('toggle_shade', { - icon: 'wb_sunny', - category: 'transform', - condition: () => !Project.box_uv && Modes.edit, - click: function () {toggleCubeProperty('shade')} - }) - new Action('toggle_mirror_uv', { - icon: 'icon-mirror_x', - category: 'transform', - condition: () => Project.box_uv && (Modes.edit || Modes.paint), - click: function () {toggleCubeProperty('shade')} - }) - new Action('update_autouv', { - icon: 'brightness_auto', - category: 'transform', - condition: () => !Project.box_uv && Modes.edit, - click: function () { - if (Cube.selected.length) { - Undo.initEdit({elements: Cube.selected[0].forSelected(), selection: true}) - Cube.selected[0].forSelected(function(cube) { - cube.mapAutoUV() - }) - Undo.finishEdit('Update auto UV') - } - } - }) - new Action('origin_to_geometry', { - icon: 'filter_center_focus', - category: 'transform', - condition: {modes: ['edit', 'animate']}, - click: function () {origin2geometry()} - }) - new Action('rescale_toggle', { - icon: 'check_box_outline_blank', - category: 'transform', - condition: function() {return Format.rotation_limit && Cube.selected.length;}, - click: function () { - Undo.initEdit({elements: Cube.selected}) - var value = !Cube.selected[0].rescale - Cube.selected.forEach(function(cube) { - cube.rescale = value - }) - Canvas.updatePositions() - updateNslideValues() - Undo.finishEdit('Toggle cube rescale') - } - }) - new Action('bone_reset_toggle', { - icon: 'check_box_outline_blank', - category: 'transform', - condition: function() {return Format.bone_rig && Group.selected;}, - click: function () { - Undo.initEdit({group: Group.selected}) - Group.selected.reset = !Group.selected.reset - updateNslideValues() - Undo.finishEdit('Toggle bone reset') - } - }) - - new Action('remove_blank_faces', { - icon: 'cancel_presentation', - condition: () => !Format.box_uv, - click: function () { - let elements = Outliner.selected.filter(el => el.faces); - Undo.initEdit({elements}) - var arr = elements.slice() - var empty_elements = []; - var cleared_total = 0; - unselectAll() - arr.forEach(element => { - var clear_count = 0; - var original_face_count = Object.keys(element.faces).length - for (var face in element.faces) { - var face_tag = element.faces[face]; - if (face_tag.texture == false) { - if (element instanceof Cube) { - face_tag.texture = null; - } else { - delete element.faces[face]; - } - clear_count++; - cleared_total++; - } - } - if (clear_count == original_face_count) { - empty_elements.push(element); - } - }) - updateSelection(); - Blockbench.showQuickMessage(tl('message.removed_faces', [cleared_total])) - if (empty_elements.length) { - Blockbench.showMessageBox({ - title: tl('message.cleared_blank_faces.title'), - icon: 'rotate_right', - message: tl('message.cleared_blank_faces.message', [empty_elements.length]), - buttons: ['generic.remove', 'dialog.cancel'], - confirm: 0, - cancel: 1, - }, function(r) { - empty_elements.forEach(element => { - if (r == 0) { - element.remove(); - elements.remove(element) - } else { - for (var face in element.faces) { - element.faces[face].texture = false; - } - } - }) - updateSelection(); - Canvas.updateView({elements, element_aspects: {geometry: true, faces: true, uv: true}}) - Undo.finishEdit('Remove blank faces'); - }) - } else { - Canvas.updateView({elements, element_aspects: {geometry: true, faces: true, uv: true}}) - Undo.finishEdit('Remove blank faces'); - } - } - }) -}) +//Actions +function origin2geometry() { + + if (Format.bone_rig && Group.selected) { + Undo.initEdit({group: Group.selected}) + + if (!Group.selected || Group.selected.children.length === 0) return; + var position = new THREE.Vector3(); + let amount = 0; + Group.selected.children.forEach(function(obj) { + if (obj.getWorldCenter) { + position.add(obj.getWorldCenter()); + amount++; + } + }) + position.divideScalar(amount); + Group.selected.mesh.parent.worldToLocal(position); + if (Group.selected.parent instanceof Group) { + position.x += Group.selected.parent.origin[0]; + position.y += Group.selected.parent.origin[1]; + position.z += Group.selected.parent.origin[2]; + } + Group.selected.transferOrigin(position.toArray()); + + } else if (Outliner.selected[0]) { + Undo.initEdit({elements: Outliner.selected}) + + var center = getSelectionCenter(); + var original_center = center.slice(); + + Outliner.selected.forEach(element => { + if (!element.transferOrigin) return; + if (Format.bone_rig && element.parent instanceof Group) { + var v = new THREE.Vector3().fromArray(original_center); + element.parent.mesh.worldToLocal(v); + v.x += element.parent.origin[0]; + v.y += element.parent.origin[1]; + v.z += element.parent.origin[2]; + center = v.toArray(); + element.transferOrigin(center) + } else { + element.transferOrigin(original_center) + } + }) + } + Canvas.updateView({ + elements: Outliner.selected, + element_aspects: {transform: true, geometry: true}, + groups: Group.selected && [Group.selected], + selection: true + }); + Undo.finishEdit('Center pivot') +} +function getSelectionCenter(all = false) { + if (Group.selected && selected.length == 0 && !all) { + let vec = THREE.fastWorldPosition(Group.selected.mesh, new THREE.Vector3()); + return vec.toArray(); + } + + let max = [-Infinity, -Infinity, -Infinity]; + let min = [ Infinity, Infinity, Infinity]; + let elements = Outliner.selected.length ? Outliner.selected : Outliner.elements; + elements.forEach(element => { + if (element.getWorldCenter) { + var pos = element.getWorldCenter(); + min[0] = Math.min(pos.x, min[0]); max[0] = Math.max(pos.x, max[0]); + min[1] = Math.min(pos.y, min[1]); max[1] = Math.max(pos.y, max[1]); + min[2] = Math.min(pos.z, min[2]); max[2] = Math.max(pos.z, max[2]); + } + }) + let center = max.V3_add(min).V3_divide(2); + + if (!Format.centered_grid) { + center.V3_add(8, 8, 8) + } + return center; +} +function limitToBox(val, inflate) { + if (typeof inflate != 'number') inflate = 0; + if (!(Format.canvas_limit && !settings.deactivate_size_limit.value)) { + return val; + } else if (val + inflate > 32) { + return 32 - inflate; + } else if (val - inflate < -16) { + return -16 + inflate; + } else { + return val; + } +} +//Movement +function moveElementsRelative(difference, index, event) { //Multiple + if (!quad_previews.current || !Outliner.selected.length) { + return; + } + var _has_groups = Format.bone_rig && Group.selected && Group.selected.matchesSelection() && Toolbox.selected.transformerMode == 'translate'; + + Undo.initEdit({elements: Outliner.selected, outliner: _has_groups}) + var axes = [] + // < > + // PageUpDown + // ^ v + var facing = quad_previews.current.getFacingDirection() + var height = quad_previews.current.getFacingHeight() + switch (facing) { + case 'north': axes = [0, 2, 1]; break; + case 'south': axes = [0, 2, 1]; break; + case 'west': axes = [2, 0, 1]; break; + case 'east': axes = [2, 0, 1]; break; + } + + if (height !== 'middle') { + if (index === 1) { + index = 2 + } else if (index === 2) { + index = 1 + } + } + if (facing === 'south' && (index === 0 || index === 1)) difference *= -1 + if (facing === 'west' && index === 0) difference *= -1 + if (facing === 'east' && index === 1) difference *= -1 + if (index === 2 && height !== 'down') difference *= -1 + if (index === 1 && height === 'up') difference *= -1 + + if (event) { + difference *= canvasGridSize(event.shiftKey || Pressing.overrides.shift, event.ctrlOrCmd || Pressing.overrides.ctrl); + } + + moveElementsInSpace(difference, axes[index]); + updateSelection(); + + Undo.finishEdit('Move elements') +} +//Rotate +function rotateSelected(axis, steps) { + let affected = [...Cube.selected, ...Mesh.selected]; + if (!affected.length) return; + Undo.initEdit({elements: affected}); + if (!steps) steps = 1 + var origin = [8, 8, 8] + if (Group.selected && Format.bone_rig) { + origin = Group.selected.origin.slice() + } else if (Format.centered_grid) { + origin = [0, 0, 0] + } else { + origin = affected[0].origin.slice() + } + affected.forEach(function(obj) { + obj.roll(axis, steps, origin) + }) + updateSelection(); + Undo.finishEdit('Rotate elements') +} +//Mirror +function mirrorSelected(axis) { + if (Modes.animate && Timeline.selected.length) { + + Undo.initEdit({keyframes: Timeline.selected}) + for (var kf of Timeline.selected) { + kf.flip(axis) + } + Undo.finishEdit('Flipped keyframes'); + updateKeyframeSelection(); + Animator.preview(); + + } else if (Modes.edit && (Outliner.selected.length || Group.selected)) { + Undo.initEdit({elements: selected, outliner: Format.bone_rig || Group.selected, selection: true}) + var center = Format.centered_grid ? 0 : 8; + if (Format.bone_rig) { + let flip_pairs = { + 0: { + right: 'left', + Right: 'Left', + RIGHT: 'LEFT', + }, + 1: { + top: 'bottom', + Top: 'Bottom', + TOP: 'BOTTOM', + }, + 2: { + back: 'front', + rear: 'front', + Back: 'Front', + Rear: 'Front', + BACK: 'FRONT', + REAR: 'FRONT', + } + } + if (Group.selected && Group.selected.matchesSelection()) { + function flipGroup(group) { + if (group.type === 'group') { + for (var i = 0; i < 3; i++) { + if (i === axis) { + group.origin[i] *= -1 + } else { + group.rotation[i] *= -1 + } + } + function matchAndReplace(a, b) { + if (group.name.includes(a)) { + let name = group._original_name + ? group._original_name.replace(a, b) + : group.name.replace(a, b).replace(/2/, ''); + if (!Group.all.find(g => g.name == name)) group.name = name; + return true; + } + } + let pairs = flip_pairs[axis]; + for (let a in pairs) { + let b = pairs[a]; + if (matchAndReplace(a, b)) break; + if (matchAndReplace(b, a)) break; + } + } + Canvas.updateAllBones([group]); + } + flipGroup(Group.selected) + Group.selected.forEachChild(flipGroup) + } + } + selected.forEach(function(obj) { + obj.flip(axis, center, false) + if (Project.box_uv && obj instanceof Cube && axis === 0) { + obj.shade = !obj.shade + Canvas.updateUV(obj) + } + }) + updateSelection() + Undo.finishEdit('Flip selection') + } +} + +const Vertexsnap = { + step1: true, + line: new THREE.Line(new THREE.BufferGeometry(), Canvas.outlineMaterial), + elements_with_vertex_gizmos: [], + hovering: false, + addVertices: function(element) { + if (Vertexsnap.elements_with_vertex_gizmos.includes(element)) return; + if (element.visibility === false) return; + let {mesh} = element; + + $('#preview').get(0).removeEventListener("mousemove", Vertexsnap.hoverCanvas) + $('#preview').get(0).addEventListener("mousemove", Vertexsnap.hoverCanvas) + + if (!mesh.vertex_points) { + mesh.updateMatrixWorld() + let vectors = []; + if (mesh.geometry) { + let positions = mesh.geometry.attributes.position.array; + for (let i = 0; i < positions.length; i += 3) { + let vec = [positions[i], positions[i+1], positions[i+2]]; + if (!vectors.find(vec2 => vec.equals(vec2))) { + vectors.push(vec); + } + } + } + vectors.push([0, 0, 0]); + + let points = new THREE.Points(new THREE.BufferGeometry(), new THREE.PointsMaterial().copy(Canvas.meshVertexMaterial)); + points.element_uuid = element.uuid; + points.vertices = vectors; + let vector_positions = []; + vectors.forEach(vector => vector_positions.push(...vector)); + let vector_colors = []; + vectors.forEach(vector => vector_colors.push(gizmo_colors.grid.r, gizmo_colors.grid.g, gizmo_colors.grid.b)); + points.geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vector_positions), 3)); + points.geometry.setAttribute('color', new THREE.Float32BufferAttribute(new Float32Array(vector_colors), 3)); + points.material.transparent = true; + mesh.vertex_points = points; + if (mesh.outline) { + mesh.outline.add(points); + } else { + mesh.add(points); + } + } + mesh.vertex_points.visible = true; + mesh.vertex_points.renderOrder = 900; + + Vertexsnap.elements_with_vertex_gizmos.push(element) + }, + clearVertexGizmos: function() { + Project.model_3d.remove(Vertexsnap.line); + Vertexsnap.elements_with_vertex_gizmos.forEach(element => { + if (element.mesh && element.mesh.vertex_points) { + element.mesh.vertex_points.visible = false; + if (element instanceof Mesh == false) { + element.mesh.vertex_points.parent.remove(element.mesh.vertex_points); + delete element.mesh.vertex_points; + } + } + + }) + Vertexsnap.elements_with_vertex_gizmos.empty(); + $('#preview').get(0).removeEventListener("mousemove", Vertexsnap.hoverCanvas) + }, + hoverCanvas: function(event) { + let data = Canvas.raycast(event) + + if (Vertexsnap.hovering) { + Project.model_3d.remove(Vertexsnap.line); + Vertexsnap.elements_with_vertex_gizmos.forEach(el => { + let points = el.mesh.vertex_points; + let colors = []; + for (let i = 0; i < points.geometry.attributes.position.count; i++) { + let color; + if (data && data.element == el && data.type == 'vertex' && data.vertex_index == i) { + color = gizmo_colors.outline; + } else { + color = gizmo_colors.grid; + } + colors.push(color.r, color.g, color.b); + } + points.material.depthTest = !(data.element == el); + points.geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)); + }) + } + if (!data || data.type !== 'vertex') { + Blockbench.setStatusBarText() + return; + } + Vertexsnap.hovering = true + + if (Vertexsnap.step1 === false) { + let {line} = Vertexsnap; + let {geometry} = line; + + let vertex_pos = Vertexsnap.getGlobalVertexPos(data.element, data.vertex); + geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array([...Vertexsnap.vertex_pos.toArray(), ...vertex_pos.toArray()]), 3)); + + line.renderOrder = 900 + Project.model_3d.add(Vertexsnap.line); + Vertexsnap.line.position.copy(scene.position).multiplyScalar(-1); + //Measure + var diff = new THREE.Vector3().copy(Vertexsnap.vertex_pos); + diff.sub(vertex_pos); + Blockbench.setStatusBarText(tl('status_bar.vertex_distance', [trimFloatNumber(diff.length())] )); + } + }, + select: function() { + Vertexsnap.clearVertexGizmos() + Outliner.selected.forEach(function(element) { + Vertexsnap.addVertices(element) + }) + if (Group.selected) { + Vertexsnap.addVertices(Group.selected) + } + if (Outliner.selected.length) { + $('#preview').css('cursor', (Vertexsnap.step1 ? 'copy' : 'alias')) + } + }, + canvasClick: function(data) { + if (!data || data.type !== 'vertex') return; + + if (Vertexsnap.step1) { + Vertexsnap.step1 = false + Vertexsnap.vertex_pos = Vertexsnap.getGlobalVertexPos(data.element, data.vertex); + Vertexsnap.vertex_index = data.vertex_index; + Vertexsnap.move_origin = typeof data.vertex !== 'string' && data.vertex.allEqual(0); + Vertexsnap.elements = Outliner.selected.slice(); + Vertexsnap.group = Group.selected; + Vertexsnap.selected_vertices = JSON.parse(JSON.stringify(Project.selected_vertices)); + Vertexsnap.clearVertexGizmos() + $('#preview').css('cursor', (Vertexsnap.step1 ? 'copy' : 'alias')) + + } else { + Vertexsnap.snap(data) + $('#preview').css('cursor', (Vertexsnap.step1 ? 'copy' : 'alias')) + } + Blockbench.setStatusBarText() + }, + getGlobalVertexPos(element, vertex) { + let vector = new THREE.Vector3(); + vector.fromArray(vertex instanceof Array ? vertex : element.vertices[vertex]); + element.mesh.localToWorld(vector); + return vector; + }, + snap: function(data) { + Undo.initEdit({elements: Vertexsnap.elements, outliner: !!Vertexsnap.group}); + + let mode = BarItems.vertex_snap_mode.get(); + + if (Vertexsnap.move_origin) { + if (Vertexsnap.group) { + let vec = Vertexsnap.getGlobalVertexPos(data.element, data.vertex); + + if (Format.bone_rig && Vertexsnap.group.parent instanceof Group && Vertexsnap.group.mesh.parent) { + Vertexsnap.group.mesh.parent.worldToLocal(vec); + } + let vec_array = vec.toArray() + vec_array.V3_add(Vertexsnap.group.parent.origin); + Vertexsnap.group.transferOrigin(vec_array) + + } else { + Vertexsnap.elements.forEach(function(element) { + let vec = Vertexsnap.getGlobalVertexPos(data.element, data.vertex); + + if (Format.bone_rig && element.parent instanceof Group && element.mesh.parent) { + element.mesh.parent.worldToLocal(vec); + } + let vec_array = vec.toArray() + vec_array.V3_add(element.parent.origin); + element.transferOrigin(vec_array) + }) + } + } else { + + var global_delta = Vertexsnap.getGlobalVertexPos(data.element, data.vertex); + global_delta.sub(Vertexsnap.vertex_pos) + + if (mode === 'scale' && !Format.integer_size && Vertexsnap.elements[0] instanceof Cube) { + //Scale + + var m; + switch (Vertexsnap.vertex_index) { + case 0: m=[ 1,1,1 ]; break; + case 1: m=[ 1,1,0 ]; break; + case 2: m=[ 1,0,1 ]; break; + case 3: m=[ 1,0,0 ]; break; + case 4: m=[ 0,1,0 ]; break; + case 5: m=[ 0,1,1 ]; break; + case 6: m=[ 0,0,0 ]; break; + case 7: m=[ 0,0,1 ]; break; + } + + Vertexsnap.elements.forEach(function(obj) { + if (obj instanceof Cube == false) return; + var q = obj.mesh.getWorldQuaternion(new THREE.Quaternion()).invert() + var cube_pos = new THREE.Vector3().copy(global_delta).applyQuaternion(q) + + for (i=0; i<3; i++) { + if (m[i] === 1) { + obj.to[i] = limitToBox(obj.to[i] + cube_pos.getComponent(i), obj.inflate) + } else { + obj.from[i] = limitToBox(obj.from[i] + cube_pos.getComponent(i), -obj.inflate) + } + } + if (Project.box_uv && obj.visibility) { + Canvas.updateUV(obj) + } + }) + } else if (mode === 'move') { + Vertexsnap.elements.forEach(function(obj) { + var cube_pos = new THREE.Vector3().copy(global_delta) + + if (obj instanceof Mesh && Vertexsnap.selected_vertices && Vertexsnap.selected_vertices[obj.uuid]) { + let vertices = Vertexsnap.selected_vertices[obj.uuid]; + var q = obj.mesh.getWorldQuaternion(Reusable.quat1).invert(); + cube_pos.applyQuaternion(q); + let cube_pos_array = cube_pos.toArray(); + vertices.forEach(vkey => { + if (obj.vertices[vkey]) obj.vertices[vkey].V3_add(cube_pos_array); + }) + + } else { + if (Format.bone_rig && obj.parent instanceof Group) { + var q = obj.mesh.parent.getWorldQuaternion(Reusable.quat1).invert(); + cube_pos.applyQuaternion(q); + } + if (obj instanceof Cube && Format.rotate_cubes) { + obj.origin.V3_add(cube_pos); + } + var in_box = obj.moveVector(cube_pos.toArray()); + if (!in_box && Format.canvas_limit && !settings.deactivate_size_limit.value) { + Blockbench.showMessageBox({translateKey: 'canvas_limit_error'}) + } + } + }) + } + + } + + Vertexsnap.clearVertexGizmos() + let update_options = { + elements: Vertexsnap.elements, + element_aspects: {transform: true, geometry: true}, + }; + if (Vertexsnap.group) { + update_options.elements = [...update_options.elements]; + Vertexsnap.group.forEachChild(child => { + update_options.elements.safePush(child); + }, OutlinerElement); + update_options.groups = [Vertexsnap.group]; + update_options.group_aspects = {transform: true}; + } + Canvas.updateView(update_options); + Undo.finishEdit('Use vertex snap'); + Vertexsnap.step1 = true; + } +} +//Scale +function getScaleAllGroups() { + let groups = []; + if (!Format.bone_rig) return groups; + if (Group.selected) { + Group.selected.forEachChild((g) => { + groups.push(g); + }, Group) + } else if (Outliner.selected.length == Outliner.elements.length && Group.all.length) { + groups = Group.all; + } + return groups; +} +function scaleAll(save, size) { + if (save === true) { + hideDialog() + } + if (size === undefined) { + size = $('#model_scale_label').val() + } + var origin = [ + parseFloat($('#scaling_origin_x').val())||0, + parseFloat($('#scaling_origin_y').val())||0, + parseFloat($('#scaling_origin_z').val())||0, + ] + var overflow = []; + Outliner.selected.forEach(function(obj) { + obj.autouv = 0; + origin.forEach(function(ogn, i) { + if ($('#model_scale_'+getAxisLetter(i)+'_axis').is(':checked')) { + + if (obj.from) { + obj.from[i] = (obj.before.from[i] - obj.inflate - ogn) * size; + if (obj.from[i] + ogn > 32 || obj.from[i] + ogn < -16) overflow.push(obj); + obj.from[i] = limitToBox(obj.from[i] + obj.inflate + ogn, -obj.inflate); + } + + if (obj.to) { + obj.to[i] = (obj.before.to[i] + obj.inflate - ogn) * size; + if (obj.to[i] + ogn > 32 || obj.to[i] + ogn < -16) overflow.push(obj); + obj.to[i] = limitToBox(obj.to[i] - obj.inflate + ogn, obj.inflate); + if (Format.integer_size) { + obj.to[i] = obj.from[i] + Math.round(obj.to[i] - obj.from[i]) + } + } + + if (obj.origin) { + obj.origin[i] = (obj.before.origin[i] - ogn) * size; + obj.origin[i] = obj.origin[i] + ogn; + } + + if (obj instanceof Mesh) { + for (let key in obj.vertices) { + obj.vertices[key][i] = (obj.before.vertices[key][i] - ogn) * size + ogn; + } + } + } else { + + if (obj.from) obj.from[i] = obj.before.from[i]; + if (obj.to) obj.to[i] = obj.before.to[i]; + + if (obj.origin) obj.origin[i] = obj.before.origin[i]; + + if (obj instanceof Mesh) { + for (let key in obj.vertices) { + obj.vertices[key][i] = obj.before.vertices[key][i]; + } + } + } + }) + if (save === true) { + delete obj.before + } + if (Project.box_uv) { + Canvas.updateUV(obj) + } + }) + getScaleAllGroups().forEach((g) => { + g.origin[0] = g.old_origin[0] * size + g.origin[1] = g.old_origin[1] * size + g.origin[2] = g.old_origin[2] * size + if (save === true) { + delete g.old_origin + } + }, Group) + if (overflow.length && Format.canvas_limit && !settings.deactivate_size_limit.value) { + scaleAll.overflow = overflow; + $('#scaling_clipping_warning').text('Model clipping: Your model is too large for the canvas') + $('#scale_overflow_btn').css('display', 'inline-block') + } else { + $('#scaling_clipping_warning').text('') + $('#scale_overflow_btn').hide() + } + Canvas.updateView({ + elements: Outliner.selected, + element_aspects: {geometry: true, transform: true}, + groups: getScaleAllGroups(), + group_aspects: {transform: true}, + }) + if (save === true) { + Undo.finishEdit('Scale model') + } +} +function modelScaleSync(label) { + if (label) { + var size = $('#model_scale_label').val() + $('#model_scale_range').val(size) + } else { + var size = $('#model_scale_range').val() + $('#model_scale_label').val(size) + } + scaleAll(false, size) +} +function cancelScaleAll() { + Outliner.selected.forEach(function(obj) { + if (obj === undefined) return; + if (obj.from) obj.from.V3_set(obj.before.from); + if (obj.to) obj.to.V3_set(obj.before.to); + if (obj.origin) obj.origin.V3_set(obj.before.origin); + if (obj instanceof Mesh) { + for (let key in obj.vertices) { + obj.vertices[key].V3_set(obj.before.vertices[key]); + } + } + delete obj.before + if (Project.box_uv) { + Canvas.updateUV(obj) + } + }) + getScaleAllGroups().forEach((g) => { + g.origin[0] = g.old_origin[0] + g.origin[1] = g.old_origin[1] + g.origin[2] = g.old_origin[2] + delete g.old_origin + }, Group) + Canvas.updateView({ + elements: Outliner.selected, + element_aspects: {geometry: true, transform: true}, + groups: getScaleAllGroups(), + group_aspects: {transform: true}, + }) + hideDialog() +} +function setScaleAllPivot(mode) { + if (mode === 'selection') { + var center = getSelectionCenter() + } else { + var center = Cube.selected[0] && Cube.selected[0].origin; + } + if (center) { + $('input#scaling_origin_x').val(center[0]); + $('input#scaling_origin_y').val(center[1]); + $('input#scaling_origin_z').val(center[2]); + } +} +function scaleAllSelectOverflow() { + cancelScaleAll() + selected.empty(); + scaleAll.overflow.forEach(obj => { + obj.selectLow() + }) + updateSelection(); +} +//Center +function centerElementsAll(axis) { + centerElements(0, false) + centerElements(1, false) + centerElements(2, false) +} +function centerElements(axis, update) { + if (!Outliner.selected.length) return; + let center = getSelectionCenter()[axis]; + var difference = (Format.centered_grid ? 0 : 8) - center + + Outliner.selected.forEach(function(obj) { + if (obj.movable) obj.origin[axis] += difference; + if (obj.to) obj.to[axis] = limitToBox(obj.to[axis] + difference, obj.inflate); + if (obj instanceof Cube) obj.from[axis] = limitToBox(obj.from[axis] + difference, obj.inflate); + }) + Group.all.forEach(group => { + if (!group.selected) return; + group.origin[axis] += difference; + }) + Canvas.updateView({ + elements: Outliner.selected, + groups: Group.all.filter(g => g.selected), + element_aspects: {transform: true}, + group_aspects: {transform: true}, + selection: true + }) +} + +//Move +function moveElementsInSpace(difference, axis) { + let space = Transformer.getTransformSpace() + let group = Format.bone_rig && Group.selected && Group.selected.matchesSelection() && Group.selected; + var group_m; + let quaternion = new THREE.Quaternion(); + let vector = new THREE.Vector3(); + + if (group) { + if (space === 0) { + group_m = vector.set(0, 0, 0); + group_m[getAxisLetter(axis)] = difference; + + var rotation = new THREE.Quaternion(); + group.mesh.parent.getWorldQuaternion(rotation); + group_m.applyQuaternion(rotation.invert()); + + group.forEachChild(g => { + g.origin.V3_add(group_m.x, group_m.y, group_m.z); + }, Group, true) + + } else if (space === 2) { + group_m = new THREE.Vector3(); + group_m[getAxisLetter(axis)] = difference; + + group_m.applyQuaternion(group.mesh.quaternion); + + group.forEachChild(g => { + g.origin.V3_add(group_m.x, group_m.y, group_m.z); + }, Group, true) + + } else { + group.forEachChild(g => { + g.origin[axis] += difference + }, Group, true) + } + Canvas.updateAllBones([Group.selected]); + } + + Outliner.selected.forEach(el => { + + if (!group_m && el instanceof Mesh && (el.getSelectedVertices().length > 0 || space >= 2)) { + + let selection_rotation = space == 3 && el.getSelectionRotation(); + let selected_vertices = el.getSelectedVertices(); + if (!selected_vertices.length) selected_vertices = Object.keys(el.vertices) + selected_vertices.forEach(key => { + + if (space == 2) { + el.vertices[key][axis] += difference; + + } else if (space == 3) { + let m = vector.set(0, 0, 0); + m[getAxisLetter(axis)] = difference; + m.applyEuler(selection_rotation); + el.vertices[key].V3_add(m.x, m.y, m.z); + + } else { + let m = vector.set(0, 0, 0); + m[getAxisLetter(axis)] = difference; + m.applyQuaternion(el.mesh.getWorldQuaternion(quaternion).invert()); + el.vertices[key].V3_add(m.x, m.y, m.z); + } + + }) + + } else { + + if (space == 2 && !group_m) { + if (el instanceof Locator) { + let m = vector.set(0, 0, 0); + m[getAxisLetter(axis)] = difference; + m.applyQuaternion(el.mesh.quaternion); + el.from.V3_add(m.x, m.y, m.z); + + } else if (el instanceof TextureMesh) { + el.local_pivot[axis] += difference; + + } else { + if (el.movable) el.from[axis] += difference; + if (el.resizable && el.to) el.to[axis] += difference; + } + + } else if (space instanceof Group) { + if (el.movable && el instanceof Mesh == false) el.from[axis] += difference; + if (el.resizable && el.to) el.to[axis] += difference; + if (el.rotatable && el instanceof Locator == false) el.origin[axis] += difference; + } else { + let move_origin = !!group; + if (group_m) { + var m = group_m + } else { + var m = vector.set(0, 0, 0); + m[getAxisLetter(axis)] = difference; + + let parent = el.parent; + while (parent instanceof Group) { + if (!parent.rotation.allEqual(0)) break; + parent = parent.parent; + } + + if (parent == 'root') { + // If none of the parent groups are rotated, move origin. + move_origin = true; + } else { + var rotation = new THREE.Quaternion(); + if (el.mesh && el instanceof Locator == false && el instanceof Mesh == false) { + el.mesh.getWorldQuaternion(rotation); + } else if (el.parent instanceof Group) { + el.parent.mesh.getWorldQuaternion(rotation); + } + m.applyQuaternion(rotation.invert()); + } + } + + if (el.movable && (el instanceof Mesh == false || !move_origin)) el.from.V3_add(m.x, m.y, m.z); + if (el.resizable && el.to) el.to.V3_add(m.x, m.y, m.z); + if (move_origin) { + if (el.rotatable && el instanceof Locator == false && el instanceof TextureMesh == false) el.origin.V3_add(m.x, m.y, m.z); + } + } + } + if (el instanceof Cube) { + el.mapAutoUV() + } + }) + Canvas.updateView({ + elements: Outliner.selected, + element_aspects: {transform: true, geometry: true}, + groups: Group.all.filter(g => g.selected), + group_aspects: {transform: true} + }) +} + +//Rotate +function getRotationInterval(event) { + if (Format.rotation_limit) { + return 22.5; + } else if ((event.shiftKey || Pressing.overrides.shift) && (event.ctrlOrCmd || Pressing.overrides.ctrl)) { + return 0.25; + } else if (event.shiftKey || Pressing.overrides.shift) { + return 22.5; + } else if (event.ctrlOrCmd || Pressing.overrides.ctrl) { + return 1; + } else { + return 2.5; + } +} +function getRotationObject() { + if (Format.bone_rig && Group.selected) return Group.selected; + let elements = Outliner.selected.filter(element => { + return element.rotatable && (element instanceof Cube == false || Format.rotate_cubes); + }) + if (elements.length) return elements; +} +function rotateOnAxis(modify, axis, slider) { + var things = getRotationObject(); + if (!things) return; + if (things instanceof Array == false) things = [things]; + /* + if (Format.bone_rig && Group.selected) { + if (!Group.selected) return; + let obj = Group.selected.mesh + + if (typeof space == 'object') { + let normal = axis == 0 ? THREE.NormalX : (axis == 1 ? THREE.NormalY : THREE.NormalZ) + let rotWorldMatrix = new THREE.Matrix4(); + rotWorldMatrix.makeRotationAxis(normal, Math.degToRad(modify(0))) + rotWorldMatrix.multiply(obj.matrix) + obj.matrix.copy(rotWorldMatrix) + obj.setRotationFromMatrix(rotWorldMatrix) + let e = obj.rotation; + Group.selected.rotation[0] = Math.radToDeg(e.x); + Group.selected.rotation[1] = Math.radToDeg(e.y); + Group.selected.rotation[2] = Math.radToDeg(e.z); + Canvas.updateAllBones() + + } else if (space == 0) { + let normal = axis == 0 ? THREE.NormalX : (axis == 1 ? THREE.NormalY : THREE.NormalZ) + let rotWorldMatrix = new THREE.Matrix4(); + rotWorldMatrix.makeRotationAxis(normal, Math.degToRad(modify(0))) + rotWorldMatrix.multiply(obj.matrixWorld) + + let inverse = new THREE.Matrix4().copy(obj.parent.matrixWorld).invert() + rotWorldMatrix.premultiply(inverse) + + obj.matrix.copy(rotWorldMatrix) + obj.setRotationFromMatrix(rotWorldMatrix) + let e = obj.rotation; + Group.selected.rotation[0] = Math.radToDeg(e.x); + Group.selected.rotation[1] = Math.radToDeg(e.y); + Group.selected.rotation[2] = Math.radToDeg(e.z); + Canvas.updateAllBones() + + } else { + var value = modify(Group.selected.rotation[axis]); + Group.selected.rotation[axis] = Math.trimDeg(value) + Canvas.updateAllBones() + } + return; + } + */ + //Warning + if (Format.rotation_limit && settings.dialog_rotation_limit.value) { + var i = 0; + while (i < Cube.selected.length) { + if (Cube.selected[i].rotation[(axis+1)%3] || + Cube.selected[i].rotation[(axis+2)%3] + ) { + i = Infinity + + Blockbench.showMessageBox({ + title: tl('message.rotation_limit.title'), + icon: 'rotate_right', + message: tl('message.rotation_limit.message'), + buttons: [tl('dialog.ok'), tl('dialog.dontshowagain')] + }, function(r) { + if (r === 1) { + settings.dialog_rotation_limit.value = false + Settings.save() + } + }) + return; + //Gotta stop the numslider here + } + i++; + } + } + var axis_letter = getAxisLetter(axis) + var origin = things[0].origin + things.forEach(function(obj, i) { + if (!obj.rotation.allEqual(0)) { + origin = obj.origin + } + }) + + let space = Transformer.getTransformSpace() + if (axis instanceof THREE.Vector3) space = 0; + things.forEach(obj => { + let mesh = obj.mesh; + if (obj instanceof Cube && !Format.bone_rig) { + if (obj.origin.allEqual(0)) { + obj.origin.V3_set(origin) + } + } + + if (!Group.selected && obj instanceof Mesh && Project.selected_vertices[obj.uuid] && Project.selected_vertices[obj.uuid].length > 0) { + + let normal = axis == 0 ? THREE.NormalX : (axis == 1 ? THREE.NormalY : THREE.NormalZ) + let rotWorldMatrix = new THREE.Matrix4(); + rotWorldMatrix.makeRotationAxis(normal, Math.degToRad(modify(0))) + if (space instanceof Group || space == 'root') { + rotWorldMatrix.multiply(mesh.matrix); + } else if (space == 0) { + rotWorldMatrix.multiply(mesh.matrixWorld); + } + let q = new THREE.Quaternion().setFromRotationMatrix(rotWorldMatrix); + if (space instanceof Group || space == 'root') { + q.premultiply(mesh.quaternion.invert()); + mesh.quaternion.invert(); + } else if (space == 0) { + let quat = mesh.getWorldQuaternion(new THREE.Quaternion()).invert(); + q.premultiply(quat); + } + + let vector = new THREE.Vector3(); + let local_pivot = obj.mesh.worldToLocal(new THREE.Vector3().copy(Transformer.position)) + + Project.selected_vertices[obj.uuid].forEach(key => { + vector.fromArray(obj.vertices[key]); + vector.sub(local_pivot); + vector.applyQuaternion(q); + vector.add(local_pivot); + obj.vertices[key].V3_set(vector.x, vector.y, vector.z); + }) + + } else if (slider || (space == 2 && Format.rotation_limit)) { + var obj_val = modify(obj.rotation[axis]); + obj_val = Math.trimDeg(obj_val) + if (Format.rotation_limit && obj instanceof Cube) { + //Limit To 1 Axis + obj.rotation[(axis+1)%3] = 0 + obj.rotation[(axis+2)%3] = 0 + //Limit Angle + obj_val = Math.round(obj_val/22.5)*22.5 + if (obj_val > 45 || obj_val < -45) { + + let f = obj_val > 45 + let can_roll = obj.roll(axis, f!=(axis==1) ? 1 : 3); + if (can_roll) { + obj_val = f ? -22.5 : 22.5; + } else { + obj_val = Math.clamp(obj_val, -45, 45); + } + } + } + obj.rotation[axis] = obj_val + if (obj instanceof Cube) { + obj.rotation_axis = axis_letter + } + } else if (space == 2) { + if ([0, 1, 2].find(axis2 => axis2 !== axis && Math.abs(obj.rotation[axis2]) > 0.1) !== undefined) { + let old_order = mesh.rotation.order; + mesh.rotation.reorder(axis == 0 ? 'ZYX' : (axis == 1 ? 'ZXY' : 'XYZ')) + var obj_val = modify(Math.radToDeg(mesh.rotation[axis_letter])); + obj_val = Math.trimDeg(obj_val) + mesh.rotation[axis_letter] = Math.degToRad(obj_val); + mesh.rotation.reorder(old_order); + + obj.rotation[0] = Math.radToDeg(mesh.rotation.x); + obj.rotation[1] = Math.radToDeg(mesh.rotation.y); + obj.rotation[2] = Math.radToDeg(mesh.rotation.z); + } else { + var obj_val = modify(Math.radToDeg(mesh.rotation[axis_letter])); + obj.rotation[axis] = Math.trimDeg(obj_val); + } + + } else if (space instanceof Group) { + let normal = axis == 0 ? THREE.NormalX : (axis == 1 ? THREE.NormalY : THREE.NormalZ) + let rotWorldMatrix = new THREE.Matrix4(); + rotWorldMatrix.makeRotationAxis(normal, Math.degToRad(modify(0))) + rotWorldMatrix.multiply(mesh.matrix) + mesh.matrix.copy(rotWorldMatrix) + mesh.setRotationFromMatrix(rotWorldMatrix) + let e = mesh.rotation; + obj.rotation[0] = Math.radToDeg(e.x); + obj.rotation[1] = Math.radToDeg(e.y); + obj.rotation[2] = Math.radToDeg(e.z); + + } else if (space == 0) { + let normal = axis instanceof THREE.Vector3 + ? axis + : axis == 0 ? THREE.NormalX : (axis == 1 ? THREE.NormalY : THREE.NormalZ) + let rotWorldMatrix = new THREE.Matrix4(); + rotWorldMatrix.makeRotationAxis(normal, Math.degToRad(modify(0))) + rotWorldMatrix.multiply(mesh.matrixWorld) + + let inverse = new THREE.Matrix4().copy(mesh.parent.matrixWorld).invert() + rotWorldMatrix.premultiply(inverse) + + mesh.matrix.copy(rotWorldMatrix) + mesh.setRotationFromMatrix(rotWorldMatrix) + let e = mesh.rotation; + obj.rotation[0] = Math.radToDeg(e.x); + obj.rotation[1] = Math.radToDeg(e.y); + obj.rotation[2] = Math.radToDeg(e.z); + + } + if (obj instanceof Group) { + Canvas.updateView({groups: [obj]}); + } + }) +} + +BARS.defineActions(function() { + + + new BarSelect('transform_space', { + condition: { + modes: ['edit', 'animate'], + tools: ['move_tool', 'pivot_tool', 'resize_tool'], + method: () => !(Toolbox && Toolbox.selected.id === 'resize_tool' && Mesh.all.length === 0) + }, + category: 'transform', + value: 'local', + options: { + global: true, + bone: {condition: () => Format.bone_rig, name: true}, + local: true, + normal: {condition: () => Mesh.selected.length, name: true} + }, + onChange() { + updateSelection(); + } + }) + new BarSelect('rotation_space', { + condition: {modes: ['edit', 'animate', 'pose'], tools: ['rotate_tool']}, + category: 'transform', + value: 'local', + options: { + global: 'action.transform_space.global', + bone: {condition: () => Format.bone_rig, name: true, name: 'action.transform_space.bone'}, + local: 'action.transform_space.local' + }, + onChange() { + updateSelection(); + } + }) + let grid_locked_interval = function(event) { + event = event||0; + return canvasGridSize(event.shiftKey || Pressing.overrides.shift, event.ctrlOrCmd || Pressing.overrides.ctrl); + } + + function moveOnAxis(modify, axis) { + selected.forEach(function(obj, i) { + if (obj instanceof Mesh && obj.getSelectedVertices().length) { + + let vertices = obj.getSelectedVertices(); + vertices.forEach(vkey => { + obj.vertices[vkey][axis] = modify(obj.vertices[vkey][axis]); + }) + obj.preview_controller.updateGeometry(obj); + + } else if (obj.movable) { + var val = modify(obj.from[axis]); + + if (Format.canvas_limit && !settings.deactivate_size_limit.value) { + var size = obj.to ? obj.size(axis) : 0; + val = limitToBox(limitToBox(val, -obj.inflate) + size, obj.inflate) - size + } + + var before = obj.from[axis]; + obj.from[axis] = val; + if (obj.to) { + obj.to[axis] += (val - before); + } + if (obj instanceof Cube) { + obj.mapAutoUV() + } + obj.preview_controller.updateTransform(obj); + if (obj.preview_controller.updateGeometry) obj.preview_controller.updateGeometry(obj); + } + }) + TickUpdates.selection = true; + } + function getPos(axis) { + let element = Outliner.selected[0]; + if (element instanceof Mesh && element.getSelectedVertices().length) { + let vertices = element.getSelectedVertices(); + let sum = 0; + vertices.forEach(vkey => sum += element.vertices[vkey][axis]); + return sum / vertices.length; + + } else if (element instanceof Cube) { + return element.from[axis]; + } else { + return element.origin[axis] + } + } + new NumSlider('slider_pos_x', { + name: tl('action.slider_pos', ['X']), + description: tl('action.slider_pos.desc', ['X']), + color: 'x', + category: 'transform', + condition: () => (selected.length && Modes.edit), + getInterval: grid_locked_interval, + get: function() { + return getPos(0); + }, + change: function(modify) { + moveOnAxis(modify, 0) + }, + onBefore: function() { + Undo.initEdit({elements: selected}) + }, + onAfter: function() { + Undo.finishEdit('Change element position') + } + }) + new NumSlider('slider_pos_y', { + name: tl('action.slider_pos', ['Y']), + description: tl('action.slider_pos.desc', ['Y']), + color: 'y', + category: 'transform', + condition: () => (selected.length && Modes.edit), + getInterval: grid_locked_interval, + get: function() { + return getPos(1); + }, + change: function(modify) { + moveOnAxis(modify, 1) + }, + onBefore: function() { + Undo.initEdit({elements: selected}) + }, + onAfter: function() { + Undo.finishEdit('Change element position') + } + }) + new NumSlider('slider_pos_z', { + name: tl('action.slider_pos', ['Z']), + description: tl('action.slider_pos.desc', ['Z']), + color: 'z', + category: 'transform', + condition: () => (selected.length && Modes.edit), + getInterval: grid_locked_interval, + get: function() { + return getPos(2); + }, + change: function(modify) { + moveOnAxis(modify, 2) + }, + onBefore: function() { + Undo.initEdit({elements: selected}) + }, + onAfter: function() { + Undo.finishEdit('Change element position') + } + }) + let slider_vector_pos = [BarItems.slider_pos_x, BarItems.slider_pos_y, BarItems.slider_pos_z]; + slider_vector_pos.forEach(slider => slider.slider_vector = slider_vector_pos); + + + function resizeOnAxis(modify, axis) { + selected.forEach(function(obj, i) { + if (obj.resizable) { + obj.resize(modify, axis, false, true) + } else if (obj.scalable) { + obj.scale[axis] = modify(obj.scale[axis]); + obj.preview_controller.updateTransform(obj); + if (obj.preview_controller.updateGeometry) obj.preview_controller.updateGeometry(obj); + } + }) + } + new NumSlider('slider_size_x', { + name: tl('action.slider_size', ['X']), + description: tl('action.slider_size.desc', ['X']), + color: 'x', + category: 'transform', + condition: () => (Outliner.selected[0] && (Outliner.selected[0].resizable || Outliner.selected[0].scalable) && Outliner.selected[0] instanceof Mesh == false && Modes.edit), + getInterval: grid_locked_interval, + get: function() { + if (Outliner.selected[0].scalable) { + return Outliner.selected[0].scale[0] + } else if (Outliner.selected[0].resizable) { + return Outliner.selected[0].to[0] - Outliner.selected[0].from[0] + } + }, + change: function(modify) { + resizeOnAxis(modify, 0) + }, + onBefore: function() { + Undo.initEdit({elements: Cube.selected}) + }, + onAfter: function() { + Undo.finishEdit('Change element size') + } + }) + new NumSlider('slider_size_y', { + name: tl('action.slider_size', ['Y']), + description: tl('action.slider_size.desc', ['Y']), + color: 'y', + category: 'transform', + condition: () => (Outliner.selected[0] && (Outliner.selected[0].resizable || Outliner.selected[0].scalable) && Outliner.selected[0] instanceof Mesh == false && Modes.edit), + getInterval: grid_locked_interval, + get: function() { + if (Outliner.selected[0].scalable) { + return Outliner.selected[0].scale[1] + } else if (Outliner.selected[0].resizable) { + return Outliner.selected[0].to[1] - Outliner.selected[0].from[1] + } + }, + change: function(modify) { + resizeOnAxis(modify, 1) + }, + onBefore: function() { + Undo.initEdit({elements: Cube.selected}) + }, + onAfter: function() { + Undo.finishEdit('Change element size') + } + }) + new NumSlider('slider_size_z', { + name: tl('action.slider_size', ['Z']), + description: tl('action.slider_size.desc', ['Z']), + color: 'z', + category: 'transform', + condition: () => (Outliner.selected[0] && (Outliner.selected[0].resizable || Outliner.selected[0].scalable)&& Outliner.selected[0] instanceof Mesh == false && Modes.edit), + getInterval: grid_locked_interval, + get: function() { + if (Outliner.selected[0].scalable) { + return Outliner.selected[0].scale[2] + } else if (Outliner.selected[0].resizable) { + return Outliner.selected[0].to[2] - Outliner.selected[0].from[2] + } + }, + change: function(modify) { + resizeOnAxis(modify, 2) + }, + onBefore: function() { + Undo.initEdit({elements: Cube.selected}) + }, + onAfter: function() { + Undo.finishEdit('Change element size') + } + }) + let slider_vector_size = [BarItems.slider_size_x, BarItems.slider_size_y, BarItems.slider_size_z]; + slider_vector_size.forEach(slider => slider.slider_vector = slider_vector_size); + //Inflate + new NumSlider('slider_inflate', { + category: 'transform', + condition: function() {return Cube.selected.length && Modes.edit}, + getInterval: grid_locked_interval, + get: function() { + return Cube.selected[0].inflate + }, + change: function(modify) { + Cube.selected.forEach(function(obj, i) { + var v = modify(obj.inflate) + if (Format.canvas_limit && !settings.deactivate_size_limit.value) { + v = obj.from[0] - Math.clamp(obj.from[0]-v, -16, 32); + v = obj.from[1] - Math.clamp(obj.from[1]-v, -16, 32); + v = obj.from[2] - Math.clamp(obj.from[2]-v, -16, 32); + v = Math.clamp(obj.to[0]+v, -16, 32) - obj.to[0]; + v = Math.clamp(obj.to[1]+v, -16, 32) - obj.to[1]; + v = Math.clamp(obj.to[2]+v, -16, 32) - obj.to[2]; + } + obj.inflate = v + }) + Canvas.updatePositions() + }, + onBefore: function() { + Undo.initEdit({elements: Cube.selected}) + }, + onAfter: function() { + Undo.finishEdit('Inflate elements') + } + }) + + //Rotation + new NumSlider('slider_rotation_x', { + name: tl('action.slider_rotation', ['X']), + description: tl('action.slider_rotation.desc', ['X']), + color: 'x', + category: 'transform', + condition: () => ((Modes.edit || Modes.pose) && getRotationObject()), + get: function() { + if (Format.bone_rig && Group.selected) { + return Group.selected.rotation[0]; + } + let ref = Outliner.selected.find(el => { + return el.rotatable && (Format.rotate_cubes || el instanceof Cube == false) + }) + if (ref) return ref.rotation[0]; + }, + change: function(modify) { + rotateOnAxis(modify, 0, true) + Canvas.updatePositions() + }, + onBefore: function() { + Undo.initEdit({elements: Outliner.selected.filter(el => el.rotatable), group: Group.selected}) + }, + onAfter: function() { + Undo.finishEdit(getRotationObject() instanceof Group ? 'Rotate group' : 'Rotate elements'); + }, + getInterval: getRotationInterval + }) + new NumSlider('slider_rotation_y', { + name: tl('action.slider_rotation', ['Y']), + description: tl('action.slider_rotation.desc', ['Y']), + color: 'y', + category: 'transform', + condition: () => ((Modes.edit || Modes.pose) && getRotationObject()), + get: function() { + if (Format.bone_rig && Group.selected) { + return Group.selected.rotation[1]; + } + let ref = Outliner.selected.find(el => { + return el.rotatable && (Format.rotate_cubes || el instanceof Cube == false) + }) + if (ref) return ref.rotation[1]; + }, + change: function(modify) { + rotateOnAxis(modify, 1, true) + Canvas.updatePositions() + }, + onBefore: function() { + Undo.initEdit({elements: Outliner.selected.filter(el => el.rotatable), group: Group.selected}) + }, + onAfter: function() { + Undo.finishEdit(getRotationObject() instanceof Group ? 'Rotate group' : 'Rotate elements'); + }, + getInterval: getRotationInterval + }) + new NumSlider('slider_rotation_z', { + name: tl('action.slider_rotation', ['Z']), + description: tl('action.slider_rotation.desc', ['Z']), + color: 'z', + category: 'transform', + condition: () => ((Modes.edit || Modes.pose) && getRotationObject()), + get: function() { + if (Format.bone_rig && Group.selected) { + return Group.selected.rotation[2]; + } + let ref = Outliner.selected.find(el => { + return el.rotatable && (Format.rotate_cubes || el instanceof Cube == false) + }) + if (ref) return ref.rotation[2]; + }, + change: function(modify) { + rotateOnAxis(modify, 2, true) + Canvas.updatePositions() + }, + onBefore: function() { + Undo.initEdit({elements: Outliner.selected.filter(el => el.rotatable), group: Group.selected}) + }, + onAfter: function() { + Undo.finishEdit(getRotationObject() instanceof Group ? 'Rotate group' : 'Rotate elements'); + }, + getInterval: getRotationInterval + }) + let slider_vector_rotation = [BarItems.slider_rotation_x, BarItems.slider_rotation_y, BarItems.slider_rotation_z]; + slider_vector_rotation.forEach(slider => slider.slider_vector = slider_vector_rotation); + + //Origin + function moveOriginOnAxis(modify, axis) { + var rotation_object = getRotationObject() + + if (rotation_object instanceof Group) { + var val = modify(rotation_object.origin[axis]); + rotation_object.origin[axis] = val; + let elements_to_update = []; + rotation_object.forEachChild(element => elements_to_update.push(element), OutlinerElement); + Canvas.updateView({ + groups: [rotation_object], + group_aspects: {transform: true}, + elements: elements_to_update, + element_aspects: {transform: true}, + selection: true + }); + if (Format.bone_rig) { + Canvas.updateAllBones(); + } + } else { + rotation_object.forEach(function(obj, i) { + var val = modify(obj.origin[axis]); + obj.origin[axis] = val; + }) + Canvas.updateView({elements: rotation_object, element_aspects: {transform: true, geometry: true}, selection: true}) + } + if (Modes.animate) { + Animator.preview(); + } + } + new NumSlider('slider_origin_x', { + name: tl('action.slider_origin', ['X']), + description: tl('action.slider_origin.desc', ['X']), + color: 'x', + category: 'transform', + condition: () => (Modes.edit || Modes.animate) && getRotationObject() && (Group.selected || Outliner.selected.length > Locator.selected.length), + getInterval: grid_locked_interval, + get: function() { + if (Format.bone_rig && Group.selected) { + return Group.selected.origin[0]; + } + let ref = Outliner.selected.find(el => { + return el.rotatable && el.origin && (Format.rotate_cubes || el instanceof Cube == false) + }) + if (ref) return ref.origin[0]; + }, + change: function(modify) { + moveOriginOnAxis(modify, 0) + }, + onBefore: function() { + Undo.initEdit({elements: selected, group: Group.selected}) + }, + onAfter: function() { + Undo.finishEdit('Change pivot point') + } + }) + new NumSlider('slider_origin_y', { + name: tl('action.slider_origin', ['Y']), + description: tl('action.slider_origin.desc', ['Y']), + color: 'y', + category: 'transform', + condition: () => (Modes.edit || Modes.animate) && getRotationObject() && (Group.selected || Outliner.selected.length > Locator.selected.length), + getInterval: grid_locked_interval, + get: function() { + if (Format.bone_rig && Group.selected) { + return Group.selected.origin[1]; + } + let ref = Outliner.selected.find(el => { + return el.rotatable && el.origin && (Format.rotate_cubes || el instanceof Cube == false) + }) + if (ref) return ref.origin[1]; + }, + change: function(modify) { + moveOriginOnAxis(modify, 1) + }, + onBefore: function() { + Undo.initEdit({elements: selected, group: Group.selected}) + }, + onAfter: function() { + Undo.finishEdit('Change pivot point') + } + }) + new NumSlider('slider_origin_z', { + name: tl('action.slider_origin', ['Z']), + description: tl('action.slider_origin.desc', ['Z']), + color: 'z', + category: 'transform', + condition: () => (Modes.edit || Modes.animate) && getRotationObject() && (Group.selected || Outliner.selected.length > Locator.selected.length), + getInterval: grid_locked_interval, + get: function() { + if (Format.bone_rig && Group.selected) { + return Group.selected.origin[2]; + } + let ref = Outliner.selected.find(el => { + return el.rotatable && el.origin && (Format.rotate_cubes || el instanceof Cube == false) + }) + if (ref) return ref.origin[2]; + }, + change: function(modify) { + moveOriginOnAxis(modify, 2) + }, + onBefore: function() { + Undo.initEdit({elements: selected, group: Group.selected}) + }, + onAfter: function() { + Undo.finishEdit('Change pivot point') + } + }) + let slider_vector_origin = [BarItems.slider_origin_x, BarItems.slider_origin_y, BarItems.slider_origin_z]; + slider_vector_origin.forEach(slider => slider.slider_vector = slider_vector_origin); + + new Action('scale', { + icon: 'settings_overscan', + category: 'transform', + condition: () => (Modes.edit && selected.length), + click: function () { + $('#model_scale_range, #model_scale_label').val(1) + $('#scaling_clipping_warning').text('') + + Undo.initEdit({elements: Outliner.selected, outliner: Format.bone_rig}) + + Outliner.selected.forEach(function(obj) { + obj.before = { + from: obj.from ? obj.from.slice() : undefined, + to: obj.to ? obj.to.slice() : undefined, + origin: obj.origin ? obj.origin.slice() : undefined + } + if (obj instanceof Mesh) { + obj.before.vertices = {}; + for (let key in obj.vertices) { + obj.before.vertices[key] = obj.vertices[key].slice(); + } + } + }) + getScaleAllGroups().forEach((g) => { + g.old_origin = g.origin.slice(); + }, Group, true) + showDialog('scaling') + var v = Format.centered_grid ? 0 : 8; + var origin = Group.selected ? Group.selected.origin : [v, 0, v]; + $('#scaling_origin_x').val(origin[0]) + $('#scaling_origin_y').val(origin[1]) + $('#scaling_origin_z').val(origin[2]) + scaleAll(false, 1) + } + }) + new Action('rotate_x_cw', { + name: tl('action.rotate_cw', 'X'), + icon: 'rotate_right', + color: 'x', + category: 'transform', + click: function () { + rotateSelected(0, 1); + } + }) + new Action('rotate_x_ccw', { + name: tl('action.rotate_ccw', 'X'), + icon: 'rotate_left', + color: 'x', + category: 'transform', + click: function () { + rotateSelected(0, 3); + } + }) + new Action('rotate_y_cw', { + name: tl('action.rotate_cw', 'Y'), + icon: 'rotate_right', + color: 'y', + category: 'transform', + click: function () { + rotateSelected(1, 1); + } + }) + new Action('rotate_y_ccw', { + name: tl('action.rotate_ccw', 'Y'), + icon: 'rotate_left', + color: 'y', + category: 'transform', + click: function () { + rotateSelected(1, 3); + } + }) + new Action('rotate_z_cw', { + name: tl('action.rotate_cw', 'Z'), + icon: 'rotate_right', + color: 'z', + category: 'transform', + click: function () { + rotateSelected(2, 1); + } + }) + new Action('rotate_z_ccw', { + name: tl('action.rotate_ccw', 'Z'), + icon: 'rotate_left', + color: 'z', + category: 'transform', + click: function () { + rotateSelected(2, 3); + } + }) + + new Action('flip_x', { + name: tl('action.flip', 'X'), + icon: 'icon-mirror_x', + color: 'x', + category: 'transform', + click: function () { + mirrorSelected(0); + } + }) + new Action('flip_y', { + name: tl('action.flip', 'Y'), + icon: 'icon-mirror_y', + color: 'y', + category: 'transform', + click: function () { + mirrorSelected(1); + } + }) + new Action('flip_z', { + name: tl('action.flip', 'Z'), + icon: 'icon-mirror_z', + color: 'z', + category: 'transform', + click: function () { + mirrorSelected(2); + } + }) + + new Action('center_x', { + name: tl('action.center', 'X'), + icon: 'vertical_align_center', + color: 'x', + category: 'transform', + click: function () { + Undo.initEdit({elements: Outliner.selected, outliner: true}); + centerElements(0); + Undo.finishEdit('Center selection on X axis') + } + }) + new Action('center_y', { + name: tl('action.center', 'Y'), + icon: 'vertical_align_center', + color: 'y', + category: 'transform', + click: function () { + Undo.initEdit({elements: Outliner.selected, outliner: true}); + centerElements(1); + Undo.finishEdit('Center selection on Y axis') + } + }) + new Action('center_z', { + name: tl('action.center', 'Z'), + icon: 'vertical_align_center', + color: 'z', + category: 'transform', + click: function () { + Undo.initEdit({elements: Outliner.selected, outliner: true}); + centerElements(2); + Undo.finishEdit('Center selection on Z axis') + } + }) + new Action('center_all', { + icon: 'filter_center_focus', + category: 'transform', + click: function () { + Undo.initEdit({elements: Outliner.selected, outliner: true}); + centerElementsAll(); + Undo.finishEdit('Center selection') + } + }) + + //Move Cube Keys + new Action('move_up', { + icon: 'arrow_upward', + category: 'transform', + condition: {modes: ['edit'], method: () => (!open_menu && selected.length)}, + keybind: new Keybind({key: 38, ctrl: null, shift: null}), + click: function (e) { + if (Prop.active_panel === 'uv') { + UVEditor.moveSelection([0, -1], e) + } else { + moveElementsRelative(-1, 2, e) + } + } + }) + new Action('move_down', { + icon: 'arrow_downward', + category: 'transform', + condition: {modes: ['edit'], method: () => (!open_menu && selected.length)}, + keybind: new Keybind({key: 40, ctrl: null, shift: null}), + click: function (e) { + if (Prop.active_panel === 'uv') { + UVEditor.moveSelection([0, 1], e) + } else { + moveElementsRelative(1, 2, e) + } + } + }) + new Action('move_left', { + icon: 'arrow_back', + category: 'transform', + condition: {modes: ['edit'], method: () => (!open_menu && selected.length)}, + keybind: new Keybind({key: 37, ctrl: null, shift: null}), + click: function (e) { + if (Prop.active_panel === 'uv') { + UVEditor.moveSelection([-1, 0], e) + } else { + moveElementsRelative(-1, 0, e) + } + } + }) + new Action('move_right', { + icon: 'arrow_forward', + category: 'transform', + condition: {modes: ['edit'], method: () => (!open_menu && selected.length)}, + keybind: new Keybind({key: 39, ctrl: null, shift: null}), + click: function (e) { + if (Prop.active_panel === 'uv') { + UVEditor.moveSelection([1, 0], e) + } else { + moveElementsRelative(1, 0, e) + } + } + }) + new Action('move_forth', { + icon: 'keyboard_arrow_up', + category: 'transform', + condition: {modes: ['edit'], method: () => (!open_menu && selected.length)}, + keybind: new Keybind({key: 33, ctrl: null, shift: null}), + click: function (e) {moveElementsRelative(-1, 1, e)} + }) + new Action('move_back', { + icon: 'keyboard_arrow_down', + category: 'transform', + condition: {modes: ['edit'], method: () => (!open_menu && selected.length)}, + keybind: new Keybind({key: 34, ctrl: null, shift: null}), + click: function (e) {moveElementsRelative(1, 1, e)} + }) + + new Action('toggle_visibility', { + icon: 'visibility', + category: 'transform', + click: function () {toggleCubeProperty('visibility')} + }) + new Action('toggle_locked', { + icon: 'fas.fa-lock', + category: 'transform', + click: function () {toggleCubeProperty('locked')} + }) + new Action('toggle_export', { + icon: 'save', + category: 'transform', + click: function () {toggleCubeProperty('export')} + }) + new Action('toggle_autouv', { + icon: 'fullscreen_exit', + category: 'transform', + condition: {modes: ['edit']}, + click: function () {toggleCubeProperty('autouv')} + }) + new Action('toggle_shade', { + icon: 'wb_sunny', + category: 'transform', + condition: () => !Project.box_uv && Modes.edit, + click: function () {toggleCubeProperty('shade')} + }) + new Action('toggle_mirror_uv', { + icon: 'icon-mirror_x', + category: 'transform', + condition: () => Project.box_uv && (Modes.edit || Modes.paint), + click: function () {toggleCubeProperty('shade')} + }) + new Action('update_autouv', { + icon: 'brightness_auto', + category: 'transform', + condition: () => !Project.box_uv && Modes.edit, + click: function () { + if (Cube.selected.length) { + Undo.initEdit({elements: Cube.selected[0].forSelected(), selection: true}) + Cube.selected[0].forSelected(function(cube) { + cube.mapAutoUV() + }) + Undo.finishEdit('Update auto UV') + } + } + }) + new Action('origin_to_geometry', { + icon: 'filter_center_focus', + category: 'transform', + condition: {modes: ['edit', 'animate']}, + click: function () {origin2geometry()} + }) + new Action('rescale_toggle', { + icon: 'check_box_outline_blank', + category: 'transform', + condition: function() {return Format.rotation_limit && Cube.selected.length;}, + click: function () { + Undo.initEdit({elements: Cube.selected}) + var value = !Cube.selected[0].rescale + Cube.selected.forEach(function(cube) { + cube.rescale = value + }) + Canvas.updatePositions() + updateNslideValues() + Undo.finishEdit('Toggle cube rescale') + } + }) + new Action('bone_reset_toggle', { + icon: 'check_box_outline_blank', + category: 'transform', + condition: function() {return Format.bone_rig && Group.selected;}, + click: function () { + Undo.initEdit({group: Group.selected}) + Group.selected.reset = !Group.selected.reset + updateNslideValues() + Undo.finishEdit('Toggle bone reset') + } + }) + + new Action('remove_blank_faces', { + icon: 'cancel_presentation', + condition: () => !Format.box_uv, + click: function () { + let elements = Outliner.selected.filter(el => el.faces); + Undo.initEdit({elements}) + var arr = elements.slice() + var empty_elements = []; + var cleared_total = 0; + unselectAll() + arr.forEach(element => { + var clear_count = 0; + var original_face_count = Object.keys(element.faces).length + for (var face in element.faces) { + var face_tag = element.faces[face]; + if (face_tag.texture == false) { + if (element instanceof Cube) { + face_tag.texture = null; + } else { + delete element.faces[face]; + } + clear_count++; + cleared_total++; + } + } + if (clear_count == original_face_count) { + empty_elements.push(element); + } + }) + updateSelection(); + Blockbench.showQuickMessage(tl('message.removed_faces', [cleared_total])) + if (empty_elements.length) { + Blockbench.showMessageBox({ + title: tl('message.cleared_blank_faces.title'), + icon: 'rotate_right', + message: tl('message.cleared_blank_faces.message', [empty_elements.length]), + buttons: ['generic.remove', 'dialog.cancel'], + confirm: 0, + cancel: 1, + }, function(r) { + empty_elements.forEach(element => { + if (r == 0) { + element.remove(); + elements.remove(element) + } else { + for (var face in element.faces) { + element.faces[face].texture = false; + } + } + }) + updateSelection(); + Canvas.updateView({elements, element_aspects: {geometry: true, faces: true, uv: true}}) + Undo.finishEdit('Remove blank faces'); + }) + } else { + Canvas.updateView({elements, element_aspects: {geometry: true, faces: true, uv: true}}) + Undo.finishEdit('Remove blank faces'); + } + } + }) +}) diff --git a/js/preview/transformer.js b/js/modeling/transform_gizmo.js similarity index 100% rename from js/preview/transformer.js rename to js/modeling/transform_gizmo.js diff --git a/js/outliner/mesh.js b/js/outliner/mesh.js index d950ad42..435ef9a4 100644 --- a/js/outliner/mesh.js +++ b/js/outliner/mesh.js @@ -1122,1581 +1122,3 @@ new NodePreviewController(Mesh, { this.dispatchEvent('update_painting_grid', {element}); } }) - -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: { - cube: 'dialog.add_primitive.shape.cube', - 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}) => !['cube', 'pyramid', 'plane'].includes(shape)}, - height: {label: 'dialog.add_primitive.height', type: 'number', value: 8, condition: ({shape}) => ['cylinder', 'cone', 'cube', '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)}, - }, - 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: {} - }); - var group = getCurrentGroup(); - mesh.addTo(group) - 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 == 'cube') { - 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 == '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)); - UVEditor.selected_faces.empty(); - 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}, - height: {label: 'dialog.add_primitive.height', type: 'number', value: result.height, condition: ['cylinder', 'cone', 'cube', 'pyramid', 'tube'].includes(result.shape)}, - 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)}, - minor_sides: {label: 'dialog.add_primitive.minor_sides', type: 'number', value: result.minor_sides, min: 2, max: 32, condition: ['torus'].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(); - } - }) - new BarSelect('selection_mode', { - options: { - object: {name: true, icon: 'far.fa-gem'}, - face: {name: true, icon: 'crop_portrait'}, - edge: {name: true, icon: 'fa-grip-lines-vertical'}, - vertex: {name: true, icon: 'fiber_manual_record'}, - }, - icon_mode: true, - condition: () => Modes.edit && Mesh.all.length, - onChange({value}) { - if (value === 'object') { - Mesh.selected.forEach(mesh => { - delete Project.selected_vertices[mesh.uuid]; - }) - } else if (value === 'face') { - UVEditor.vue.selected_faces.empty(); - Mesh.selected.forEach(mesh => { - for (let fkey in mesh.faces) { - let face = mesh.faces[fkey]; - if (face.isSelected()) { - UVEditor.vue.selected_faces.safePush(fkey); - } - } - }) - } - updateSelection(); - } - }) - - 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.all.length, - 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 BarSelect('select_seam', { - options: { - auto: true, - divide: true, - join: true, - }, - condition: () => Modes.edit && Mesh.all.length, - onChange({value}) { - if (value == 'auto') value = null; - Undo.initEdit({elements: Mesh.selected}); - Mesh.selected.forEach(mesh => { - let selected_vertices = mesh.getSelectedVertices(); - mesh.forAllFaces((face) => { - let vertices = face.getSortedVertices(); - vertices.forEach((vkey_a, i) => { - let vkey_b = vertices[i+1] || vertices[0]; - if (selected_vertices.includes(vkey_a) && selected_vertices.includes(vkey_b)) { - mesh.setSeam([vkey_a, vkey_b], 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 => { - UVEditor.selected_faces.empty(); - let selected_vertices = mesh.getSelectedVertices(); - 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 (face.isSelected()) { - delete mesh.faces[key]; - } - } - // Split face - if ( - (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); - UVEditor.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); - UVEditor.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); - UVEditor.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}); - - let new_meshes = []; - Cube.selected.forEach(cube => { - - let mesh = new Mesh({ - name: cube.name, - color: cube.color, - origin: cube.origin, - rotation: cube.rotation, - vertices: [ - [cube.to[0] + cube.inflate - cube.origin[0], cube.to[1] + cube.inflate - cube.origin[1], cube.to[2] + cube.inflate - cube.origin[2]], - [cube.to[0] + cube.inflate - cube.origin[0], cube.to[1] + cube.inflate - cube.origin[1], cube.from[2] - cube.inflate - cube.origin[2]], - [cube.to[0] + cube.inflate - cube.origin[0], cube.from[1] - cube.inflate - cube.origin[1], cube.to[2] + cube.inflate - cube.origin[2]], - [cube.to[0] + cube.inflate - cube.origin[0], cube.from[1] - cube.inflate - cube.origin[1], cube.from[2] - cube.inflate - cube.origin[2]], - [cube.from[0] - cube.inflate - cube.origin[0], cube.to[1] + cube.inflate - cube.origin[1], cube.to[2] + cube.inflate - cube.origin[2]], - [cube.from[0] - cube.inflate - cube.origin[0], cube.to[1] + cube.inflate - cube.origin[1], cube.from[2] - cube.inflate - cube.origin[2]], - [cube.from[0] - cube.inflate - cube.origin[0], cube.from[1] - cube.inflate - cube.origin[1], cube.to[2] + cube.inflate - cube.origin[2]], - [cube.from[0] - cube.inflate - cube.origin[0], cube.from[1] - cube.inflate - cube.origin[1], cube.from[2] - cube.inflate - cube.origin[2]], - ], - }) - - let vertex_keys = Object.keys(mesh.vertices); - let unused_vkeys = vertex_keys.slice(); - function addFace(direction, vertices) { - let cube_face = cube.faces[direction]; - if (cube_face.texture === null) return; - let uv = { - [vertices[0]]: [cube_face.uv[2], cube_face.uv[1]], - [vertices[1]]: [cube_face.uv[0], cube_face.uv[1]], - [vertices[2]]: [cube_face.uv[2], cube_face.uv[3]], - [vertices[3]]: [cube_face.uv[0], cube_face.uv[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); - cube.remove(); - }) - Undo.finishEdit('Convert cubes to meshes', {elements: new_meshes}); - } - }) - 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 key in mesh.faces) { - let face = mesh.faces[key]; - if (face.isSelected()) { - face.invert(); - } - } - }) - Undo.finishEdit('Invert mesh faces'); - 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 = Project.selected_vertices[mesh.uuid].slice(); - let new_vertices; - let new_face_keys = []; - let selected_faces = []; - let selected_face_keys = []; - let combined_direction; - for (let fkey in mesh.faces) { - let face = mesh.faces[fkey]; - if (face.isSelected()) { - selected_faces.push(face); - selected_face_keys.push(fkey); - } - } - - 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) { - if (mesh.faces[fkey].vertices.filter(vkey => original_vertices.includes(vkey)).length >= 2 && mesh.faces[fkey].vertices.length > 2) { - face = mesh.faces[fkey]; - 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) { - direction = match.getNormal(true); - } - } - - vector.V3_add(direction.map(v => v * extend)); - return vector; - })) - Project.selected_vertices[mesh.uuid].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 line - let line_vertices = remaining_vertices.slice(); - let covered_edges = []; - let new_faces = []; - for (let fkey in mesh.faces) { - let face = mesh.faces[fkey]; - let sorted_vertices = face.getSortedVertices(); - let matched_vertices = sorted_vertices.filter(vkey => line_vertices.includes(new_vertices[original_vertices.indexOf(vkey)])); - if (matched_vertices.length >= 2) { - let already_handled_edge = covered_edges.find(edge => edge.includes(matched_vertices[0]) && edge.includes(matched_vertices[1])) - if (already_handled_edge) { - let handled_face = new_faces[covered_edges.indexOf(already_handled_edge)] - if (handled_face) handled_face.invert(); - continue; - } - covered_edges.push(matched_vertices.slice(0, 2)); - - if (sorted_vertices[0] == matched_vertices[0] && sorted_vertices[1] != matched_vertices[1]) { - matched_vertices.reverse(); - } - let [a, b] = matched_vertices.map(vkey => new_vertices[original_vertices.indexOf(vkey)]); - let [c, d] = matched_vertices; - 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); - } - }) - - 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'}, - }, form => { - runEdit(true, form.extend); - }) - } - }) - 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 = Project.selected_vertices[mesh.uuid].slice(); - if (original_vertices.length < 3) return; - let new_vertices; - let selected_faces = []; - let selected_face_keys = []; - for (let key in mesh.faces) { - let face = mesh.faces[key]; - if (face.isSelected()) { - selected_faces.push(face); - selected_face_keys.push(key); - } - } - - 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.selected_vertices[mesh.uuid].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)], - ] - }); - mesh.addFaces(new_face); - 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]; - }) - - }) - 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}, - }, 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() { - function runEdit(amended, offset = 50, direction = 0) { - Undo.initEdit({elements: Mesh.selected, selection: true}, amended); - 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 = {}; - - function getCenterVertex(vertices) { - let existing_key = center_vertices[vertices[0]] || center_vertices[vertices[1]]; - if (existing_key) return existing_key; - - let vector = mesh.vertices[vertices[0]].map((v, i) => Math.lerp(v, mesh.vertices[vertices[1]][i], offset/100)) - let [vkey] = mesh.addVertices(vector); - center_vertices[vertices[0]] = center_vertices[vertices[1]] = vkey; - return vkey; - } - - function splitFace(face, side_vertices, double_side) { - 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 center_vertices = [ - getCenterVertex(side_vertices), - getCenterVertex(opposite_vertices) - ] - - let c1_uv_coords = [ - Math.lerp(face.uv[side_vertices[0]][0], face.uv[side_vertices[1]][0], offset/100), - Math.lerp(face.uv[side_vertices[0]][1], face.uv[side_vertices[1]][1], offset/100), - ]; - let c2_uv_coords = [ - Math.lerp(face.uv[opposite_vertices[0]][0], face.uv[opposite_vertices[1]][0], offset/100), - Math.lerp(face.uv[opposite_vertices[0]][1], face.uv[opposite_vertices[1]][1], offset/100), - ]; - - 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); - - // 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); - 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) { - splitFace(ref_face, side_vertices); - break; - } - } - } - - } else { - let opposite_vertex = sorted_vertices.find(vkey => !side_vertices.includes(vkey)); - - let center_vertex = getCenterVertex(side_vertices); - - let c1_uv_coords = [ - (face.uv[side_vertices[0]][0] + face.uv[side_vertices[1]][0]) / 2, - (face.uv[side_vertices[0]][1] + face.uv[side_vertices[1]][1]) / 2, - ]; - - 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]], - } - }) - mesh.addFaces(new_face); - } - } - - let start_vertices = start_face.getSortedVertices().filter((vkey, i) => selected_vertices.includes(vkey)); - let start_offset = direction % start_vertices.length; - let start_edge = start_vertices.slice(start_offset, start_offset+2); - if (start_edge.length == 1) start_edge.splice(0, 0, start_vertices[0]); - - splitFace(start_face, start_edge, start_face.vertices.length == 4); - - selected_vertices.empty(); - for (let key in center_vertices) { - selected_vertices.safePush(center_vertices[key]); - } - }) - Undo.finishEdit('Create loop cut') - Canvas.updateView({elements: Mesh.selected, element_aspects: {geometry: true, uv: true, faces: true}, selection: true}) - } - - let selected_face; - Mesh.selected.forEach(mesh => { - if (!selected_face) { - selected_face = mesh.getSelectedFaces()[0]; - } - }) - - 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: 50, label: 'edit.loop_cut.offset', min: 0, max: 100}, - }, form => { - runEdit(true, form.offset, form.direction); - }) - } - }) - 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 selected_vertices = mesh.getSelectedVertices(); - let faces = Object.keys(mesh.faces); - for (let fkey in mesh.faces) { - let face = mesh.faces[fkey]; - let sorted_vertices = face.getSortedVertices(); - let side_vertices = faces.includes(fkey) && sorted_vertices.filter(vkey => selected_vertices.includes(vkey)); - if (side_vertices && side_vertices.length == 2) { - if (side_vertices[0] == sorted_vertices[0] && side_vertices[1] == sorted_vertices.last()) { - side_vertices.reverse(); - } - let original_face_normal = face.getNormal(true); - let index_difference = sorted_vertices.indexOf(side_vertices[1]) - sorted_vertices.indexOf(side_vertices[0]); - if (index_difference == -1 || index_difference > 2) side_vertices.reverse(); - let other_face = face.getAdjacentFace(sorted_vertices.indexOf(side_vertices[0])); - face.vertices.remove(...side_vertices); - delete face.uv[side_vertices[0]]; - delete face.uv[side_vertices[1]]; - if (other_face) { - let new_vertices = other_face.face.getSortedVertices().filter(vkey => !side_vertices.includes(vkey)); - face.vertices.push(...new_vertices); - new_vertices.forEach(vkey => { - face.uv[vkey] = other_face.face.uv[vkey]; - }) - delete mesh.faces[other_face.key]; - } - faces.remove(fkey); - if (Reusable.vec1.fromArray(face.getNormal(true)).angleTo(Reusable.vec2.fromArray(original_face_normal)) > Math.PI/2) { - face.invert(); - } - side_vertices.forEach(vkey => { - let is_used; - for (let fkey2 in mesh.faces) { - if (mesh.faces[fkey2].vertices.includes(vkey)) { - is_used = true; - break; - } - } - if (!is_used) { - delete mesh.vertices[vkey]; - selected_vertices.remove(vkey); - } - }) - } - } - }) - 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}); - 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 copy = new Mesh(mesh); - elements.push(copy); - - for (let fkey in mesh.faces) { - let face = mesh.faces[fkey]; - if (face.isSelected()) { - 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.selected_vertices[mesh.uuid]; - Project.selected_vertices[copy.uuid] = selected_vertices; - 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}) - } - }) - new Action('import_obj', { - icon: 'fa-gem', - category: 'file', - condition: {modes: ['edit'], method: () => (Format.meshes)}, - click: function () { - - - Blockbench.import({ - resource_id: 'obj', - extensions: ['obj'], - name: 'OBJ Wavefront Model', - }, function(files) { - let {content} = files[0]; - 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(); - - 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 (cmd == 'o' || cmd == 'g') { - mesh = new Mesh({ - name: args[0], - vertices: {} - }) - vertex_keys = {}; - meshes.push(mesh); - } - if (cmd == 'v') { - vertices.push(toVector(args, 3).map(v => v * 16)); - } - 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 => { - 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 - }) - 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(); - } - } - } - }) - meshes.forEach(mesh => { - mesh.init(); - }) - - Undo.finishEdit('Import OBJ'); - }) - } - }) -}) \ No newline at end of file