Implement mesh section copy pasting

Add Sketchfab tag suggestions
Add dialog form button type
Copying in preview now copies both element and face
Fix copy pasting elements not copying texture
Rename "Create Face", closes #1060
This commit is contained in:
JannisX11 2021-09-22 22:56:54 +02:00
parent 2948152fe2
commit 4ba2e4a33a
13 changed files with 191 additions and 46 deletions

View File

@ -735,14 +735,13 @@
width: 73px;
border: none;
}
dialog a.button_select_all,
dialog a.button_select_none {
dialog div.dialog_form_buttons a {
display: inline-block;
text-decoration: underline;
cursor: pointer;
margin-right: 8px;
}
dialog a.button_select_all:hover,
dialog a.button_select_none:hover {
dialog div.dialog_form_buttons a:hover {
color: var(--color-accent);
}
/*Scale*/

View File

@ -440,6 +440,7 @@
#tab_bar #tab_bar_list {
display: flex;
flex-direction: row;
flex-grow: 1;
position: relative;
white-space: nowrap;
overflow-x: auto;

View File

@ -1674,12 +1674,6 @@ const Animator = {
id: 'animation_import',
title: 'dialog.animation_import.title',
form,
lines: [
`<div>
<a class="button_select_all">${tl('generic.select_all')}</a>
<a class="button_select_none">${tl('generic.select_none')}</a>
</div>`
],
onConfirm(form_result) {
this.hide();
let names = [];
@ -1692,15 +1686,17 @@ const Animator = {
let new_animations = Animator.loadFile(file, names);
Undo.finishEdit('Import animations', {animations: new_animations})
}
}).show();
function setAll(value) {
let values = {};
keys.forEach(key => values[key.hashCode()] = value);
dialog.setFormValues(values);
});
form.select_all_none = {
type: 'buttons',
buttons: ['generic.select_all', 'generic.select_none'],
click(index) {
let values = {};
keys.forEach(key => values[key.hashCode()] = (index == 0));
dialog.setFormValues(values);
}
}
dialog.object.querySelector('a.button_select_all').addEventListener('click', e => setAll(true));
dialog.object.querySelector('a.button_select_none').addEventListener('click', e => setAll(false));
dialog.show();
}
},
exportAnimationFile(path) {

View File

@ -6,6 +6,7 @@ const Clipbench = {
display_slot: 'display_slot',
keyframe: 'keyframe',
face: 'face',
mesh_selection: 'mesh_selection',
texture: 'texture',
outliner: 'outliner',
texture_selection: 'texture_selection',
@ -29,6 +30,12 @@ const Clipbench = {
if (Animator.open && Timeline.animators.length && (Timeline.selected.length || mode === 2) && ['keyframe', 'timeline', 'preview'].includes(p)) {
return Clipbench.types.keyframe
}
if (Modes.edit && p == 'preview' && Mesh.selected[0] && Mesh.selected[0].getSelectedVertices().length) {
return Clipbench.types.mesh_selection;
}
if (mode == 2 && Modes.edit && Format.meshes && Clipbench.last_copied == 'mesh_selection' && (p == 'preview' || p == 'outliner')) {
return Clipbench.types.mesh_selection;
}
if ((p == 'uv' || p == 'preview') && Modes.edit) {
return Clipbench.types.face;
}
@ -40,7 +47,9 @@ const Clipbench = {
}
},
copy(event, cut) {
switch (Clipbench.getCopyType(1)) {
let copy_type = Clipbench.getCopyType(1);
Clipbench.last_copied = copy_type;
switch (copy_type) {
case 'text':
Clipbench.setText(window.getSelection()+'');
break;
@ -58,24 +67,27 @@ const Clipbench = {
case 'face':
UVEditor.copy(event);
break;
case 'mesh_selection':
Clipbench.setMeshSelection(Mesh.selected[0], event);
break;
case 'texture':
Clipbench.setTexture(Texture.selected);
if (cut) {
BarItems.delete.trigger();
}
break;
case 'outliner':
Clipbench.setElements();
Clipbench.setGroup();
if (Group.selected) {
Clipbench.setGroup(Group.selected);
} else {
Clipbench.setElements(selected);
}
if (cut) {
BarItems.delete.trigger();
}
break;
}
if (copy_type == 'outliner' || (copy_type == 'face' && Prop.active_panel == 'preview')) {
Clipbench.setElements();
Clipbench.setGroup();
if (Group.selected) {
Clipbench.setGroup(Group.selected);
} else {
Clipbench.setElements(selected);
}
if (cut) {
BarItems.delete.trigger();
}
}
},
paste(event) {
@ -95,6 +107,9 @@ const Clipbench = {
case 'face':
UVEditor.paste(event);
break;
case 'mesh_selection':
Clipbench.pasteMeshSelection();
break;
case 'texture':
Clipbench.pasteTextures();
break;
@ -134,6 +149,53 @@ const Clipbench = {
document.execCommand('copy')
}
},
setMeshSelection(mesh) {
this.vertices = {};
this.faces = {};
mesh.getSelectedVertices().forEach(vkey => {
this.vertices[vkey] = mesh.vertices[vkey].slice();
})
for (let fkey in mesh.faces) {
let face = mesh.faces[fkey];
if (face.isSelected()) {
this.faces[fkey] = new MeshFace(null, face);
}
}
},
pasteMeshSelection() {
let elements = Mesh.selected.slice();
Undo.initEdit({elements});
let new_mesh;
if (!elements.length) {
new_mesh = new Mesh({name: 'pasted', vertices: []});
elements.push(new_mesh);
}
elements.forEach(mesh => {
let old_vertices = Object.keys(this.vertices);
let vertices_positions = old_vertices.map(vkey => this.vertices[vkey]);
let new_vertices = mesh.addVertices(...vertices_positions);
for (let old_fkey in this.faces) {
let old_face = this.faces[old_fkey];
let new_face = new MeshFace(mesh, old_face);
let new_face_vertices = new_face.vertices.map(old_vkey => {
let new_vkey = new_vertices[old_vertices.indexOf(old_vkey)];
new_face.uv[new_vkey] = new_face.uv[old_vkey];
delete new_face.uv[old_vkey];
console.log(old_vertices.indexOf(old_vkey), new_vkey)
return new_vkey;
})
new_face.vertices.replace(new_face_vertices);
mesh.addFaces(new_face);
}
mesh.getSelectedVertices(true).replace(new_vertices);
})
if (new_mesh) {
new_mesh.init().select();
}
Undo.finishEdit('Paste mesh selection');
Canvas.updateView({elements: Mesh.selected, selection: true})
},
pasteOutliner(event) {
Undo.initEdit({outliner: true, elements: [], selection: true});
//Group

View File

@ -131,6 +131,21 @@ function buildForm(dialog) {
break;
case 'buttons':
let list = document.createElement('div');
list.className = 'dialog_form_buttons';
data.buttons.forEach((button_text, index) => {
let button = document.createElement('a');
button.innerText = tl(button_text);
button.addEventListener('click', e => {
data.click(index, e);
})
list.append(button);
})
bar.append(list);
break;
case 'number':
input_element = $(`<input class="dark_bordered half focusable_input" type="number" id="${form_id}"
value="${parseFloat(data.value)||0}" min="${data.min}" max="${data.max}" step="${data.step||1}">`)

View File

@ -286,19 +286,35 @@ var Extruder = {
}
//Export
function uploadSketchfabModel() {
if (elements.length === 0) {
if (elements.length === 0 || !Format) {
return;
}
let tag_suggestions = ['lowpoly', 'pixelart'];
if (Format.id !== 'free') tag_suggestions.push('minecraft');
if (Format.id === 'skin') tag_suggestions.push('skin');
if (!Mesh.all.length) tag_suggestions.push('voxel');
let clean_project_name = Project.name.toLowerCase().replace(/[_.-]+/g, '-').replace(/[^a-z0-9-]+/, '')
if (Project.name) tag_suggestions.push(clean_project_name);
if (clean_project_name.includes('-')) tag_suggestions.safePush(...clean_project_name.split('-').filter(s => s.length > 2 && s != 'geo').reverse());
var dialog = new Dialog({
id: 'sketchfab_uploader',
title: 'dialog.sketchfab_uploader.title',
width: 540,
width: 640,
form: {
token: {label: 'dialog.sketchfab_uploader.token', value: settings.sketchfab_token.value, type: 'password'},
about_token: {type: 'info', text: tl('dialog.sketchfab_uploader.about_token', ['[sketchfab.com/settings/password](https://sketchfab.com/settings/password)'])},
name: {label: 'dialog.sketchfab_uploader.name'},
description: {label: 'dialog.sketchfab_uploader.description', type: 'textarea'},
tags: {label: 'dialog.sketchfab_uploader.tags', placeholder: 'Tag1 Tag2'},
tag_suggestions: {label: 'dialog.sketchfab_uploader.suggested_tags', type: 'buttons', buttons: tag_suggestions, click(index) {
let {tags} = dialog.getFormResult();
let new_tag = tag_suggestions[index];
if (!tags.split(/\s/g).includes(new_tag)) {
tags += ' ' + new_tag;
dialog.setFormValues({tags});
}
}},
animations: {label: 'dialog.sketchfab_uploader.animations', value: true, type: 'checkbox', condition: (Format.animation_mode && Animator.animations.length)},
//color: {type: 'color', label: 'dialog.sketchfab_uploader.color'},
draft: {label: 'dialog.sketchfab_uploader.draft', type: 'checkbox'},

View File

@ -223,7 +223,7 @@ class Cube extends OutlinerElement {
delete copy.parent;
return copy;
}
getSaveCopy() {
getSaveCopy(project) {
var el = {}
for (var key in Cube.properties) {
@ -245,7 +245,7 @@ class Cube extends OutlinerElement {
if (!this.uv_offset.allEqual(0)) el.uv_offset = this.uv_offset;
el.faces = {}
for (var face in this.faces) {
el.faces[face] = this.faces[face].getSaveCopy()
el.faces[face] = this.faces[face].getSaveCopy(project)
}
el.uuid = this.uuid
return el;

View File

@ -324,10 +324,10 @@ class Group extends OutlinerNode {
Canvas.updatePositions();
return copy;
}
getSaveCopy() {
getSaveCopy(project) {
var base_group = this.getChildlessCopy(true);
for (var child of this.children) {
base_group.children.push(child.getSaveCopy());
base_group.children.push(child.getSaveCopy(project));
}
delete base_group.parent;
return base_group;

View File

@ -287,7 +287,7 @@ class Mesh extends OutlinerElement {
delete copy.parent;
return copy;
}
getSaveCopy() {
getSaveCopy(project) {
var el = {}
for (var key in Mesh.properties) {
Mesh.properties[key].copy(this, el)
@ -300,7 +300,7 @@ class Mesh extends OutlinerElement {
el.faces = {};
for (let key in this.faces) {
el.faces[key] = this.faces[key].getSaveCopy();
el.faces[key] = this.faces[key].getSaveCopy(project);
}
el.type = 'mesh';
@ -1233,10 +1233,14 @@ BARS.defineActions(function() {
keybind: new Keybind({key: 'f', shift: true}),
condition: () => (Modes.edit && Format.meshes && 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});
Mesh.selected.forEach(mesh => {
let selected_vertices = Project.selected_vertices[mesh.uuid];
if (selected_vertices && selected_vertices.length >= 2 && selected_vertices.length <= 4) {
let selected_vertices = mesh.getSelectedVertices();
if (selected_vertices.length >= 2 && selected_vertices.length <= 4) {
let reference_face;
for (let key in mesh.faces) {
let face = mesh.faces[key];
@ -1288,6 +1292,55 @@ BARS.defineActions(function() {
}
}
}
} 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 face = new MeshFace(mesh, {vertices: face_vertices, texture: reference_face.texture});
mesh.addFaces(face);
if (face_vertices.length < 4) break;
start_index += 3;
}
}
})
Undo.finishEdit('Create mesh face')

View File

@ -1521,7 +1521,7 @@ class Face {
this.texture = false;
return this;
}
getSaveCopy() {
getSaveCopy(project) {
var copy = {
uv: this.uv,
}
@ -1531,8 +1531,10 @@ class Face {
var tex = this.getTexture()
if (tex === null) {
copy.texture = null;
} else if (tex instanceof Texture) {
} else if (tex instanceof Texture && project) {
copy.texture = Texture.all.indexOf(tex)
} else if (tex instanceof Texture) {
copy.texture = tex.uuid;
}
return copy;
}

View File

@ -43,11 +43,11 @@ const TextureGenerator = {
type: {label: 'dialog.create_texture.type', type: 'select', condition: Cube.all.length || Mesh.all.length, options: type_options},
rearrange_uv:{label: 'dialog.create_texture.rearrange_uv', description: 'dialog.create_texture.rearrange_uv.desc', type: 'checkbox', value: true, condition: (form) => (form.type == 'template')},
box_uv: {label: 'dialog.project.uv_mode.box_uv', type: 'checkbox', value: false, condition: (form) => (form.type == 'template' && !Project.box_uv)},
compress: {label: 'dialog.create_texture.compress', description: 'dialog.create_texture.compress.desc', type: 'checkbox', value: true, condition: (form) => (form.type == 'template' && Project.box_uv && form.rearrange_uv)},
power: {label: 'dialog.create_texture.power', description: 'dialog.create_texture.power.desc', type: 'checkbox', value: true, condition: (form) => (form.type !== 'blank' && (form.rearrange_uv || form.type == 'color_map'))},
double_use: {label: 'dialog.create_texture.double_use', description: 'dialog.create_texture.double_use.desc', type: 'checkbox', value: true, condition: (form) => (form.type == 'template' && Project.box_uv && form.rearrange_uv)},
combine_polys: {label: 'dialog.create_texture.combine_polys', description: 'dialog.create_texture.combine_polys.desc', type: 'checkbox', value: true, condition: (form) => (form.type == 'template' && form.rearrange_uv && Mesh.selected.length)},
box_uv: {label: 'dialog.project.uv_mode.box_uv', type: 'checkbox', value: false, condition: (form) => (form.type == 'template' && !Project.box_uv)},
padding: {label: 'dialog.create_texture.padding', description: 'dialog.create_texture.padding.desc', type: 'checkbox', value: false, condition: (form) => (form.type == 'template' && form.rearrange_uv)},
},

File diff suppressed because one or more lines are too long

View File

@ -440,6 +440,7 @@
"dialog.sketchfab_uploader.description": "Description",
"dialog.sketchfab_uploader.animations": "Animations",
"dialog.sketchfab_uploader.tags": "Tags",
"dialog.sketchfab_uploader.suggested_tags": "Suggested Tags",
"dialog.sketchfab_uploader.draft": "Draft",
"dialog.sketchfab_uploader.private": "Private (Pro)",
"dialog.sketchfab_uploader.password": "Password (Pro)",
@ -1022,7 +1023,7 @@
"action.selection_mode.face": "Face",
"action.selection_mode.line": "Line",
"action.selection_mode.vertex": "Vertex",
"action.create_face": "Create Face",
"action.create_face": "Create Face or Line",
"action.create_face.desc": "Creates a new face or line between the selected vertices",
"action.convert_to_mesh": "Convert to Mesh",
"action.convert_to_mesh.desc": "Convert the selected elements into meshes",