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:
parent
2948152fe2
commit
4ba2e4a33a
@ -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*/
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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}">`)
|
||||
|
20
js/io/io.js
20
js/io/io.js
@ -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'},
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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')
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user