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