Add mesh inset action

Implement normal transform space
Turn name of locked elements gray in outliner
Fix face selection issue
Improve performance by hiding unselected faces on large meshes
Fix issue where imported bbmodels would switch to generic model
This commit is contained in:
JannisX11 2021-09-16 16:52:21 +02:00
parent 8aa0a48034
commit 2c98c43754
10 changed files with 166 additions and 17 deletions

View File

@ -286,6 +286,9 @@
.outliner_object > input.renaming {
pointer-events: auto;
}
.outliner_object input.cube_name.locked {
color: var(--color-subtle_text);
}
i.outliner_toggle {
font-size: 15px;
}

View File

@ -61,8 +61,8 @@ var codec = new Codec('project', {
},
load(model, file, add) {
newProject(Formats[model.meta.type] || Formats.free);
console.log(model)
newProject(Formats[model.meta.model_format] || Formats.free);
var name = pathToName(file.path, true);
if (file.path && isApp && !file.no_file ) {
Project.save_path = file.path;

View File

@ -308,7 +308,7 @@ class ModelProject {
delete ProjectData[this.uuid];
Project = 0;
if (last_selected !== this) {
if (last_selected && last_selected !== this) {
last_selected.select();
} else if (ModelProject.all.length) {
ModelProject.all[Math.clamp(index, 0, ModelProject.all.length-1)].select();

View File

@ -277,6 +277,16 @@ class Mesh extends OutlinerElement {
}
return faces;
}
getSelectionRotation() {
let [face] = this.getSelectedFaces().map(fkey => this.faces[fkey]);
if (face) {
let normal = face.getNormal(true)
var y = Math.atan2(normal[0], normal[2]);
var x = Math.atan2(normal[1], Math.sqrt(Math.pow(normal[0], 2) + Math.pow(normal[2], 2)));
return new THREE.Euler(-x, y, 0, 'YXZ');
}
}
transferOrigin(origin, update = true) {
if (!this.mesh) return;
var q = new THREE.Quaternion().copy(this.mesh.quaternion);
@ -423,6 +433,7 @@ class Mesh extends OutlinerElement {
Mesh.prototype.needsUniqueName = false;
Mesh.prototype.menu = new Menu([
'extrude_mesh_selection',
'inset_mesh_selection',
'loop_cut',
'create_face',
'invert_face',
@ -708,12 +719,13 @@ new NodePreviewController(Mesh, {
let mesh = element.mesh;
let white = new THREE.Color(0xffffff);
let selected_vertices = element.getSelectedVertices();
if (BarItems.selection_mode.value == 'vertex') {
let colors = [];
for (let key in element.vertices) {
let color;
if (Project.selected_vertices[element.uuid] && Project.selected_vertices[element.uuid].includes(key)) {
if (selected_vertices.includes(key)) {
color = white;
} else {
color = gizmo_colors.grid;
@ -729,7 +741,7 @@ new NodePreviewController(Mesh, {
let color;
if (!Modes.edit || BarItems.selection_mode.value == 'object') {
color = gizmo_colors.outline;
} else if (Project.selected_vertices[element.uuid] && Project.selected_vertices[element.uuid].includes(key)) {
} else if (selected_vertices.includes(key)) {
color = white;
} else {
color = gizmo_colors.grid;
@ -1133,7 +1145,7 @@ BARS.defineActions(function() {
}
})
Undo.finishEdit('Create mesh face')
Canvas.updateView({elements: Mesh.selected, element_aspects: {geometry: true, uv: true, faces: true}})
Canvas.updateView({elements: Mesh.selected, element_aspects: {geometry: true, uv: true, faces: true}, selection: true})
}
})
new Action('convert_to_mesh', {
@ -1196,7 +1208,7 @@ BARS.defineActions(function() {
new Action('invert_face', {
icon: 'flip_to_back',
category: 'edit',
keybind: new Keybind({key: 'i', shift: true}),
keybind: new Keybind({key: 'i', shift: true, ctrl: true}),
condition: () => (Modes.edit && Format.meshes && Mesh.selected[0] && Mesh.selected[0].getSelectedFaces().length),
click() {
Undo.initEdit({elements: Mesh.selected});
@ -1336,6 +1348,107 @@ BARS.defineActions(function() {
Canvas.updateView({elements: Mesh.selected, element_aspects: {geometry: true, uv: true, faces: true}, selection: true})
}
})
new Action('inset_mesh_selection', {
icon: 'fa-compress-arrows-alt',
category: 'edit',
keybind: new Keybind({key: 'i', shift: true}),
condition: () => (Modes.edit && Format.meshes && Mesh.selected[0] && Mesh.selected[0].getSelectedVertices().length),
click() {
Undo.initEdit({elements: Mesh.selected});
Mesh.selected.forEach(mesh => {
let original_vertices = Project.selected_vertices[mesh.uuid].slice();
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)
})
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.V3_add(inset).V3_divide(2);
}
if (affected_faces.length == 2) {
let vkey2 = affected_faces[0].vertices.find(_vkey => _vkey != vkey && affected_faces[1].vertices.includes(_vkey));
inset.V3_set(mesh.vertices[vkey2]).V3_multiply(1/4);
vector.V3_multiply(3/4);
vector.V3_add(inset);
}
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)],
]
});
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)];
delete mesh.vertices[b];
})
})
Undo.finishEdit('Extrude mesh selection')
Canvas.updateView({elements: Mesh.selected, element_aspects: {geometry: true, uv: true, faces: true}, selection: true})
}
})
new Action('loop_cut', {
icon: 'carpenter',
category: 'edit',

View File

@ -1135,7 +1135,7 @@ Interface.definePanels(function() {
'<i v-else class="outliner_opener_placeholder"></i>' +
//Main
'<i :class="node.icon + ((settings.outliner_colors.value && node.color >= 0) ? \' ec_\'+node.color : \'\')" v-on:dblclick.stop="if (node.children && node.children.length) {node.isOpen = !node.isOpen;}"></i>' +
'<input type="text" class="cube_name tab_target" v-model="node.name" disabled>' +
'<input type="text" class="cube_name tab_target" :class="{locked: node.locked}" v-model="node.name" disabled>' +
`<i v-for="btn in node.buttons"

View File

@ -389,6 +389,7 @@ class Preview {
let index = intersect.faceIndex;
for (let key in element.faces) {
let {vertices} = element.faces[key];
if (vertices.length < 3) continue;
if (index == 0 || (index == 1 && vertices.length == 4)) {
face = key;

View File

@ -524,6 +524,7 @@
this.hoverAxis = null;
this.direction = true;
this.last_valid_position = new THREE.Vector3();
this.rotation_selection = new THREE.Euler();
this.firstLocation = [0,0,0]
@ -674,6 +675,11 @@
} else {
object.getWorldQuaternion(this.rotation)
}
if (this.rotation_selection) {
let q = Reusable.quat1.setFromEuler(this.rotation_selection);
this.quaternion.multiply(q);
worldRotation.setFromQuaternion(this.quaternion);
}
} else {
worldRotation.set(0, 0, 0);
@ -818,6 +824,7 @@
}
this.center = function() {
delete Transformer.rotation_ref;
Transformer.rotation_selection.set(0, 0, 0);
if (Modes.edit || Toolbox.selected.id == 'pivot_tool') {
if (Transformer.visible) {
var rotation_tool = Toolbox.selected.id === 'rotate_tool' || Toolbox.selected.id === 'pivot_tool'
@ -855,11 +862,15 @@
let space = Transformer.getTransformSpace();
//Rotation
if (space === 2 || Toolbox.selected.id == 'resize_tool') {
if (space >= 2 || Toolbox.selected.id == 'resize_tool') {
Transformer.rotation_ref = Group.selected ? Group.selected.mesh : (selected[0] && selected[0].mesh);
if (Toolbox.selected.id == 'rotate_tool' && Group.selected) {
Transformer.rotation_ref = Group.selected.mesh;
}
if (space === 3 && Mesh.selected[0]) {
let rotation = Mesh.selected[0].getSelectionRotation();
if (rotation) Transformer.rotation_selection.copy(rotation);
}
} else if (space instanceof Group) {
Transformer.rotation_ref = space.mesh;

View File

@ -2064,6 +2064,18 @@ Interface.definePanels(function() {
})
return max - min;
},
filterMeshFaces(faces) {
let keys = Object.keys(faces);
if (keys.length > 800) {
let result = {};
this.selected_faces.forEach(key => {
if (faces[key]) result[key] = faces[key];
})
return result;
} else {
return faces;
}
},
getBrushOutlineStyle() {
if (Toolbox.selected.brushTool) {
var pixel_size = this.inner_width / (this.texture ? this.texture.width : Project.texture_width);
@ -2107,7 +2119,7 @@ Interface.definePanels(function() {
@mouseleave="if (mode == 'paint') mouse_coords.x = -1"
class="checkerboard_target"
ref="viewport"
v-show="!hidden"
v-if="!hidden"
:style="{width: (width+8) + 'px', height: (height+8) + 'px', overflowX: (zoom > 1) ? 'scroll' : 'hidden', overflowY: (inner_height > height) ? 'scroll' : 'hidden'}"
>
@ -2157,7 +2169,7 @@ Interface.definePanels(function() {
<template v-if="element.type == 'mesh'">
<div class="mesh_uv_face"
v-for="(face, key) in element.faces" :key="key"
v-for="(face, key) in filterMeshFaces(element.faces)" :key="key"
v-if="face.vertices.length > 2 && face.getTexture() == texture"
:class="{selected: selected_faces.includes(key)}"
@mousedown.prevent="dragFace(key, $event)"

View File

@ -590,10 +590,12 @@ 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 = new THREE.Vector3();
group_m = vector.set(0, 0, 0);
group_m[getAxisLetter(axis)] = difference;
var rotation = new THREE.Quaternion();
@ -626,15 +628,20 @@ function moveElementsInSpace(difference, axis) {
if (!group_m && el instanceof Mesh && Project.selected_vertices[el.uuid] && Project.selected_vertices[el.uuid].length > 0) {
let quaternion = new THREE.Quaternion();
let selection_rotation = space == 3 && el.getSelectionRotation();
Project.selected_vertices[el.uuid].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 = new THREE.Vector3();
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);
@ -646,7 +653,7 @@ function moveElementsInSpace(difference, axis) {
if (space == 2 && !group_m) {
if (el instanceof Locator) {
let m = new THREE.Vector3();
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);
@ -668,7 +675,7 @@ function moveElementsInSpace(difference, axis) {
if (group_m) {
var m = group_m
} else {
var m = new THREE.Vector3();
var m = vector.set(0, 0, 0);
m[getAxisLetter(axis)] = difference;
let parent = el.parent;

View File

@ -1026,6 +1026,8 @@
"action.invert_face.desc": "Invert the selected faces to make them face the opposite direction",
"action.extrude_mesh_selection": "Extrude Selection",
"action.extrude_mesh_selection.desc": "Extrude the selected parts of the mesh",
"action.inset_mesh_selection": "Inset Selection",
"action.inset_mesh_selection.desc": "Inset the selected parts of the mesh",
"action.loop_cut": "Loop Cut",
"action.loop_cut.desc": "Split the mesh in a loop across the selected line",
"action.split_mesh": "Split Mesh",