Add Merge Vertices tool

Fix issue with vertex snapping on cubes
Fix skin models opening two tabs
Fix issue with closing projects
Fix #1048 Rescaling with face tool selection creates flickering
Fix ##1051 Arrow keys do not work with meshes (and other non-cubes)
Fix issues with null objects
Fix area select in UV editor selecting faces twice
Finish implementing theme borders option
master
JannisX11 2021-09-17 22:32:53 +02:00
parent ee5fdb2be7
commit fb8d8ba67b
13 changed files with 189 additions and 114 deletions

View File

@ -573,3 +573,55 @@
flex-shrink: 0;
}
/* Theme Borders */
body.theme_borders .contextMenu,
body.theme_borders dialog,
body.theme_borders .dialog_close_button,
body.theme_borders #start_screen content,
body.theme_borders #quick_message_box,
body.theme_borders action_selector > #action_selector_list
{
border: 1px solid var(--color-border);
}
body.theme_borders #start_screen section {
border-bottom: 1px solid var(--color-border);
}
body.theme_borders .panel {
margin-top: -1px;
border-top: 01px solid var(--color-border);
}
body.theme_borders #right_bar {
border-left: 1px solid var(--color-border);
}
body.theme_borders #left_bar {
border-right: 1px solid var(--color-border);
}
body.theme_borders .preview .preview_menu {
right: 0;
}
body.theme_borders .dialog_sidebar {
border-right: 1px solid var(--color-border);
}
body.theme_borders .dialog_handle {
border-bottom: 1px solid var(--color-border);
}
body.theme_borders .dialog_close_button {
right: -1px;
top: -1px;
height: 31px;
}
body.theme_borders li.animation_file {
border-top: 1px solid var(--color-border);
}
body.theme_borders #main_toolbar, body.theme_borders #tab_bar {
border-bottom: 1px solid var(--color-border);
}
body.theme_borders #status_bar {
border-top: 1px solid var(--color-border);
}
body.theme_borders .contextMenu li.menu_separator {
background-color: var(--color-border);
height: 1px;
padding: 0;
opacity: 1;
}

View File

@ -93,15 +93,17 @@ function updateSelection(options = {}) {
} else if ((!selected.includes(obj) || obj.locked) && obj.selected) {
obj.unselect()
}
if (Project.selected_vertices[obj.uuid]) {
Project.selected_vertices[obj.uuid].forEachReverse(vkey => {
if (vkey in obj.vertices == false) {
Project.selected_vertices[obj.uuid].remove(vkey);
}
})
}
if (Project.selected_vertices[obj.uuid] && (Project.selected_vertices[obj.uuid].length == 0 || !obj.selected)) {
delete Project.selected_vertices[obj.uuid];
if (obj instanceof Mesh) {
if (Project.selected_vertices[obj.uuid]) {
Project.selected_vertices[obj.uuid].forEachReverse(vkey => {
if (vkey in obj.vertices == false) {
Project.selected_vertices[obj.uuid].remove(vkey);
}
})
}
if (Project.selected_vertices[obj.uuid] && (Project.selected_vertices[obj.uuid].length == 0 || !obj.selected)) {
delete Project.selected_vertices[obj.uuid];
}
}
})
if (Group.selected && Group.selected.locked) Group.selected.unselect()

View File

@ -133,6 +133,10 @@ const CustomTheme = {
CustomTheme.updateSettings();
saveChanges();
},
'data.borders'() {
CustomTheme.updateSettings();
saveChanges();
},
'data.css'() {
CustomTheme.updateSettings();
saveChanges();
@ -254,8 +258,12 @@ const CustomTheme = {
<input @input="customizeTheme($event)" style="font-family: var(--font-headline)" type="text" class="half dark_bordered" id="layout_font_headline" v-model="data.headline_font">
</div>
<div class="dialog_bar">
<label class="name_space_left" for="layout_font_cpde">${tl('layout.font.code')}</label>
<input @input="customizeTheme($event)" style="font-family: var(--font-code)" type="text" class="half dark_bordered" id="layout_font_cpde" v-model="data.code_font">
<label class="name_space_left" for="layout_font_code">${tl('layout.font.code')}</label>
<input @input="customizeTheme($event)" style="font-family: var(--font-code)" type="text" class="half dark_bordered" id="layout_font_code" v-model="data.code_font">
</div>
<div class="dialog_bar">
<label class="name_space_left" for="layout_borders">${tl('layout.borders')}</label>
<input @input="customizeTheme($event)" type="checkbox" id="layout_borders" v-model="data.borders">
</div>
</div>
@ -361,10 +369,18 @@ const CustomTheme = {
document.body.style.setProperty('--font-custom-main', CustomTheme.data.main_font);
document.body.style.setProperty('--font-custom-headline', CustomTheme.data.headline_font);
document.body.style.setProperty('--font-custom-code', CustomTheme.data.code_font);
document.body.classList.toggle('theme_borders', !!CustomTheme.data.borders);
$('style#theme_css').text(CustomTheme.data.css);
},
loadTheme(theme) {
var app = CustomTheme.data;
app.id = '';
app.name = '';
app.author = '';
app.main_font = '';
app.headline_font = '';
app.code_font = '';
app.borders = false;
Merge.string(app, theme, 'id')
Merge.string(app, theme, 'name')
Merge.string(app, theme, 'author')

View File

@ -169,10 +169,8 @@ const format = new ModelFormat({
}
})
format.new = function() {
if (newProject(this)) {
skin_dialog.show();
return true;
}
skin_dialog.show();
return true;
}
function generateTemplate(width = 64, height = 64, cubes, name = 'name', eyes, layer_template) {

View File

@ -269,10 +269,14 @@ class ModelProject {
}
async close(force) {
let last_selected = Project;
this.select();
await new Promise(resolve => setTimeout(resolve, 50));
try {
this.select();
} catch (err) {
console.error(err);
}
function saveWarning() {
async function saveWarning() {
await new Promise(resolve => setTimeout(resolve, 4));
if (Project.saved) {
return true;
} else {
@ -295,7 +299,7 @@ class ModelProject {
}
}
if (force || saveWarning()) {
if (force || await saveWarning()) {
if (isApp) await updateRecentProjectThumbnail();
Blockbench.dispatchEvent('close_project');

View File

@ -437,6 +437,7 @@ class Mesh extends OutlinerElement {
'loop_cut',
'create_face',
'invert_face',
'merge_vertices',
'_',
'split_mesh',
'merge_meshes',
@ -622,6 +623,7 @@ new NodePreviewController(Mesh, {
mesh.vertex_points.geometry.computeBoundingSphere();
mesh.outline.geometry.computeBoundingSphere();
updateCubeHighlights()
},
updateFaces(element) {
let {mesh} = element;
@ -1593,6 +1595,46 @@ BARS.defineActions(function() {
Canvas.updateView({elements: Mesh.selected, element_aspects: {geometry: true, uv: true, faces: true}, selection: true})
}
})
new Action('merge_vertices', {
icon: 'close_fullscreen',
category: 'edit',
keybind: new Keybind({key: 'm', shift: true}),
condition: () => (Modes.edit && Format.meshes && Mesh.selected[0] && Mesh.selected[0].getSelectedVertices().length > 1),
click() {
Undo.initEdit({elements: Mesh.selected});
Mesh.selected.forEach(mesh => {
let selected_vertices = mesh.getSelectedVertices();
if (selected_vertices.length < 2) return;
let first_vertex = selected_vertices[0];
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);
})
Undo.finishEdit('Merge vertices')
Canvas.updateView({elements: Mesh.selected, element_aspects: {geometry: true, uv: true, faces: true}, selection: true})
}
})
new Action('merge_meshes', {
icon: 'upload',
category: 'edit',

View File

@ -13,6 +13,9 @@ class NullObject extends OutlinerElement {
this.extend(data);
}
}
get origin() {
return this.from;
}
extend(object) {
for (var key in NullObject.properties) {
NullObject.properties[key].merge(this, object)
@ -103,6 +106,8 @@ class NullObject extends OutlinerElement {
OutlinerElement.registerType(NullObject, 'null_object');
new NodePreviewController(NullObject)
BARS.defineActions(function() {
new Action('add_null_object', {
icon: 'far.fa-circle',

View File

@ -752,8 +752,8 @@ class Preview {
}
let face_test = start_face.getAdjacentFace(0);
let index = (face_test && face_test.face.isSelected()) ? 1 : 0;
let face_test = start_face.getAdjacentFace(1);
let index = (face_test && face_test.face.isSelected()) ? 2 : 1;
selectFace(start_face, index);
if (!(event.ctrlOrCmd || Pressing.overrides.ctrl || event.shiftKey || Pressing.overrides.shift)) {

View File

@ -617,6 +617,15 @@ const UVEditor = {
this.vue.selected_faces.empty();
}
},
moveSelection(offset, event) {
Undo.initEdit({elements: UVEditor.getMappableElements()})
let step = canvasGridSize(event.shiftKey || Pressing.overrides.shift, event.ctrlOrCmd || Pressing.overrides.ctrl) / UVEditor.grid;
UVEditor.slidePos((old_val) => {
let sign = offset[offset[0] ? 0 : 1];
return old_val + step * sign;
}, offset[0] ? 0 : 1);
Undo.finishEdit('Move UV')
},
disableAutoUV() {
this.forCubes(obj => {
obj.autouv = 0
@ -1678,7 +1687,7 @@ Interface.definePanels(function() {
for (let fkey in element.faces) {
let face_rect = getRectangle(...element.faces[fkey].uv);
if (doRectanglesOverlap(rect, face_rect)) {
scope.selected_faces.push(fkey);
scope.selected_faces.safePush(fkey);
}
}
} else if (element instanceof Cube) {
@ -1693,7 +1702,7 @@ Interface.definePanels(function() {
i++;
let vkey2 = vertices[i] || vertices[0];
if (lineIntersectsReactangle(face.uv[vkey], face.uv[vkey2], [rect.ax, rect.ay], [rect.bx, rect.by])) {
scope.selected_faces.push(fkey);
scope.selected_faces.safePush(fkey);
break;
}
}
@ -1701,86 +1710,6 @@ Interface.definePanels(function() {
}
}
})
/*
<template v-if="mode == 'uv'" v-for="element in (showing_overlays ? all_mappable_elements : mappable_elements)" :key="element.uuid">
<template v-if="element.type == 'cube' && !box_uv">
<div class="cube_uv_face"
v-for="(face, key) in element.faces" :key="key"
v-if="face.getTexture() == texture || texture == 0"
:title="face_names[key]"
:class="{selected: selected_faces.includes(key), unselected: showing_overlays && !mappable_elements.includes(element)}"
@mousedown.prevent="dragFace(key, $event)"
:style="{
left: toPixels(Math.min(face.uv[0], face.uv[2]), -1),
top: toPixels(Math.min(face.uv[1], face.uv[3]), -1),
'--width': toPixels(Math.abs(face.uv_size[0]), 2),
'--height': toPixels(Math.abs(face.uv_size[1]), 2),
}"
>
<template v-if="selected_faces.includes(key) && !(showing_overlays && !mappable_elements.includes(element))">
{{ face_names[key] || '' }}
<div class="uv_resize_side horizontal" @mousedown="resizeFace(key, $event, 0, -1)" style="width: var(--width)"></div>
<div class="uv_resize_side horizontal" @mousedown="resizeFace(key, $event, 0, 1)" style="top: var(--height); width: var(--width)"></div>
<div class="uv_resize_side vertical" @mousedown="resizeFace(key, $event, -1, 0)" style="height: var(--height)"></div>
<div class="uv_resize_side vertical" @mousedown="resizeFace(key, $event, 1, 0)" style="left: var(--width); height: var(--height)"></div>
<div class="uv_resize_corner uv_c_nw" @mousedown="resizeFace(key, $event, -1, -1)" style="left: 0; top: 0"></div>
<div class="uv_resize_corner uv_c_ne" @mousedown="resizeFace(key, $event, 1, -1)" style="left: var(--width); top: 0"></div>
<div class="uv_resize_corner uv_c_sw" @mousedown="resizeFace(key, $event, -1, 1)" style="left: 0; top: var(--height)"></div>
<div class="uv_resize_corner uv_c_se" @mousedown="resizeFace(key, $event, 1, 1)" style="left: var(--width); top: var(--height)"></div>
</template>
</div>
</template>
<div v-else-if="element.type == 'cube'" class="cube_box_uv"
@mousedown.prevent="dragFace(null, $event)"
@click.prevent="selectCube(element, $event)"
:class="{unselected: showing_overlays && !mappable_elements.includes(element)}"
:style="{left: toPixels(element.uv_offset[0]), top: toPixels(element.uv_offset[1])}"
>
<div class="uv_fill" :style="{left: '-1px', top: toPixels(element.size(2, true), -1), width: toPixels(element.size(2, true)*2 + element.size(0, true)*2, 2), height: toPixels(element.size(1, true), 2)}" />
<div class="uv_fill" :style="{left: toPixels(element.size(2, true), -1), top: '-1px', width: toPixels(element.size(0, true)*2, 2), height: toPixels(element.size(2, true), 2), borderBottom: 'none'}" />
<div :style="{left: toPixels(element.size(2, true), -1), top: '-1px', width: toPixels(element.size(0, true), 2), height: toPixels(element.size(2, true) + element.size(1, true), 2)}" />
<div :style="{left: toPixels(element.size(2, true)*2 + element.size(0, true), -1), top: toPixels(element.size(2, true), -1), width: toPixels(element.size(0, true), 2), height: toPixels(element.size(1, true), 2)}" />
</div>
<template v-if="element.type == 'mesh'">
<div class="mesh_uv_face"
v-for="(face, key) in element.faces" :key="key"
v-if="face.vertices.length > 2 && face.getTexture() == texture"
:class="{selected: selected_faces.includes(key)}"
@mousedown.prevent="dragFace(key, $event)"
:style="{
left: toPixels(getMeshFaceCorner(face, 0), -1),
top: toPixels(getMeshFaceCorner(face, 1), -1),
width: toPixels(getMeshFaceWidth(face, 0), 2),
height: toPixels(getMeshFaceWidth(face, 1), 2),
}"
>
<svg>
<polygon :points="getMeshFaceOutline(face)" />
</svg>
<template v-if="selected_faces.includes(key)">
<div class="uv_mesh_vertex" v-for="key in face.vertices"
:class="{selected: selected_vertices[element.uuid] && selected_vertices[element.uuid].includes(key)}"
@mousedown.prevent.stop="dragVertices(element, key, $event)"
:style="{left: toPixels( face.uv[key][0] - getMeshFaceCorner(face, 0) ), top: toPixels( face.uv[key][1] - getMeshFaceCorner(face, 1) )}"
></div>
</template>
</div>
</template>
</template>*/
}
function stop() {
removeEventListeners(document, 'mousemove touchmove', drag);

View File

@ -90,13 +90,13 @@ function limitToBox(val, inflate) {
}
}
//Movement
function moveCubesRelative(difference, index, event) { //Multiple
if (!quad_previews.current || !Cube.selected.length) {
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: Cube.selected, outliner: _has_groups})
Undo.initEdit({elements: Outliner.selected, outliner: _has_groups})
var axes = []
// < >
// PageUpDown
@ -1544,42 +1544,66 @@ BARS.defineActions(function() {
category: 'transform',
condition: {modes: ['edit'], method: () => (!open_menu && selected.length)},
keybind: new Keybind({key: 38, ctrl: null, shift: null}),
click: function (e) {moveCubesRelative(-1, 2, e)}
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) {moveCubesRelative(1, 2, e)}
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) {moveCubesRelative(-1, 0, e)}
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) {moveCubesRelative(1, 0, e)}
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) {moveCubesRelative(-1, 1, e)}
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) {moveCubesRelative(1, 1, e)}
click: function (e) {moveElementsRelative(1, 1, e)}
})
new Action('toggle_visibility', {

File diff suppressed because one or more lines are too long

View File

@ -524,6 +524,7 @@
"layout.font.main": "Main Font",
"layout.font.headline": "Headline Font",
"layout.font.code": "Code Font",
"layout.borders": "Borders",
"about.version": "Version:",
"about.version.up_to_date": "Up to date",
@ -1032,6 +1033,8 @@
"action.loop_cut.desc": "Split the mesh in a loop across the selected line",
"action.split_mesh": "Split Mesh",
"action.split_mesh.desc": "Split the selected faces of the mesh into a new mesh",
"action.merge_vertices": "Merge Vertices",
"action.merge_vertices.desc": "Merge the selected vertices into the position of the first selected verted",
"action.merge_meshes": "Merge Meshes",
"action.merge_meshes.desc": "Merge multiple meshes into one",

View File

@ -6,7 +6,7 @@
"main_font": "",
"headline_font": "",
"code_font": "",
"css": ".contextMenu,\ndialog,\n.dialog_close_button,\n#start_screen content,\n#quick_message_box,\naction_selector > #action_selector_list\n{\n border: 1px solid var(--color-border);\n}\n#start_screen section {\n border-bottom: 1px solid var(--color-border);\n}\n.panel {\n margin-top: -1px;\n border-top: 01px solid var(--color-border);\n}\n#right_bar {\n border-left: 1px solid var(--color-border);\n}\n#left_bar {\n border-right: 1px solid var(--color-border);\n}\n.preview .preview_menu {\n right: 0;\n}\n.dialog_sidebar {\n border-right: 1px solid var(--color-border);\n}\n.dialog_handle {\n background: transparent;\n border-bottom: 1px solid var(--color-border);\n}\n.dialog_close_button {\n right: -1px;\n top: -1px;\n height: 31px;\n}\nli.animation_file {\n border-top: 1px solid var(--color-border);\n}\n#work_screen {\n grid-template-rows: 31px minmax(200px, 5000px) 26px;\n}\n#main_toolbar, #tab_bar {\n border-bottom: 01px solid var(--color-border);\n}\n#status_bar {\n border-top: 01px solid var(--color-border);\n}\n\n#mode_selector li {\n padding: 2px 10px;\n}\n#mode_selector li.selected {\n background-color: var(--color-accent);\n color: var(--color-accent_text);\n}\n\n#tab_bar .project_tab.selected,\n.dialog_sidebar_pages li.selected,\n.tool.enabled\n{\n background-color: var(--color-accent) !important;\n color: var(--color-accent_text);\n}\n.contextMenu li {\n height: 34px;\n padding-top: 6px;\n padding-bottom: 6px;\n}\n.contextMenu li.menu_separator {\n height: 2px;\n padding: 0;\n background-color: var(--color-border);\n}\n.dialog_sidebar_pages li.selected {\n background-color: var(--color-accent) !important;\n color: var(--color-accent_text);\n}",
"css": "#mode_selector li {\n\tpadding: 2px 10px;\n}\n#mode_selector li.selected {\n\tbackground-color: var(--color-accent);\n\tcolor: var(--color-accent_text);\n}\n\n#tab_bar .project_tab.selected,\n.dialog_sidebar_pages li.selected,\n.tool.enabled\n{\n\tbackground-color: var(--color-accent) !important;\n\tcolor: var(--color-accent_text);\n}\n.contextMenu li {\n\theight: 34px;\n\tpadding-top: 6px;\n\tpadding-bottom: 6px;\n}\n.dialog_sidebar_pages li.selected {\n\tbackground-color: var(--color-accent) !important;\n\tcolor: var(--color-accent_text);\n}\n.dialog_handle {\n\tbackground: transparent;\n}\n#work_screen {\n\tgrid-template-rows: 31px minmax(200px, 5000px) 26px;\n}\n",
"colors": {
"ui": "#17191d",
"back": "#17191d",