Add Solid Color Template generator

Add menu to import project directly from open tab
Add "Snap UV To Pixels" action
Fill tool element mode support for meshes (Closes #1065)
This commit is contained in:
JannisX11 2021-09-22 13:47:46 +02:00
parent 4c9d9acd5d
commit 2948152fe2
9 changed files with 272 additions and 49 deletions

View File

@ -378,7 +378,7 @@
@mouseleave="mouseLeave(project, $event)"
>
<label class="project_tab_session_badge" v-if="project.EditSession"><i class="material-icons">group</i>{{ project.EditSession.client_count }}</label>
<label>{{ project.name || project.geometry_name || project.format.name }}</label>
<label>{{ project.getDisplayName() }}</label>
<div class="project_tab_close_button" :class="{unsaved: !project.saved}" :title="close_tab_label" @click="project.close()">
<i class="material-icons close_icon">clear</i>
<i class="material-icons unsaved_icon" v-if="!project.saved">fiber_manual_record</i>

View File

@ -2358,7 +2358,7 @@ const BARS = {
project.geometry_name.toLowerCase().includes(search_input)
) {
list.push({
name: project.name || project.geometry_name || project.format.name,
name: project.getDisplayName(),
icon: project.format.icon,
description: project.path,
keybind_label: Modes.options[project.mode].name,

View File

@ -539,6 +539,31 @@ const MenuBar = {
'close_project',
'_',
{name: 'menu.file.import', id: 'import', icon: 'insert_drive_file', children: [
{
id: 'import_open_project',
name: 'menu.file.import.import_open_project',
icon: 'input',
condition: () => Project && ModelProject.all.length > 1,
children() {
let projects = [];
ModelProject.all.forEach(project => {
if (project == Project) return;
projects.push({
name: project.getDisplayName(),
icon: project.format.icon,
description: project.path,
click() {
let current_project = Project;
project.select();
let bbmodel = Codecs.project.compile();
current_project.select();
Codecs.project.merge(JSON.parse(bbmodel));
}
})
})
return projects;
}
},
'import_project',
'import_java_block_model',
'import_optifine_part',

View File

@ -95,6 +95,9 @@ class ModelProject {
get nodes_3d() {
return ProjectData[this.uuid].nodes_3d;
}
getDisplayName() {
return this.name || this.geometry_name || this.format.name;
}
reset() {
return;
//if (isApp) updateRecentProjectThumbnail();
@ -466,6 +469,34 @@ function setStartScreen(state) {
}
onVueSetup(() => {
const new_tab = {
name: tl('projects.new_tab'),
saved: true,
selected: true,
uuid: guid(),
visible: true,
is_new_tab: true,
getDisplayName() {return this.name},
close: () => {
if (ModelProject.all.length) {
Interface.tab_bar.new_tab.visible = false;
let project = ModelProject.all.find(project => project.uuid == Interface.tab_bar.last_opened_project) ||
ModelProject.all.last();
if (project) project.select();
} else {
window.close();
}
},
select() {
if (Project) {
Project.unselect()
}
Project = 0;
Interface.tab_bar.new_tab.selected = true;
setProjectTitle(tl('projects.new_tab'));
},
openSettings() {}
}
Interface.tab_bar = new Vue({
el: '#tab_bar',
data: {
@ -475,33 +506,7 @@ onVueSetup(() => {
close_tab_label: tl('projects.close_tab'),
search_tabs_label: tl('generic.search'),
last_opened_project: '',
new_tab: {
name: tl('projects.new_tab'),
saved: true,
selected: true,
uuid: guid(),
visible: true,
is_new_tab: true,
close: () => {
if (ModelProject.all.length) {
Interface.tab_bar.new_tab.visible = false;
let project = ModelProject.all.find(project => project.uuid == Interface.tab_bar.last_opened_project) ||
ModelProject.all.last();
if (project) project.select();
} else {
window.close();
}
},
select() {
if (Project) {
Project.unselect()
}
Project = 0;
Interface.tab_bar.new_tab.selected = true;
setProjectTitle(tl('projects.new_tab'));
},
openSettings() {}
}
new_tab
},
computed: {
tabs() {

View File

@ -370,7 +370,7 @@ const Painter = {
var element = Painter.current.element;
let {rect, uvFactorX, uvFactorY, w, h} = area;
if (Painter.erase_mode && (fill_mode === 'cube' || fill_mode === 'face')) {
if (Painter.erase_mode && (fill_mode === 'element' || fill_mode === 'face')) {
ctx.globalAlpha = b_opacity;
ctx.fillStyle = 'white';
ctx.globalCompositeOperation = 'destination-out';
@ -378,7 +378,7 @@ const Painter = {
ctx.fillStyle = tinycolor(ColorPanel.get()).setAlpha(b_opacity).toRgbString();
}
if (element instanceof Cube && fill_mode === 'cube') {
if (element instanceof Cube && fill_mode === 'element') {
for (var face in element.faces) {
var tag = element.faces[face]
ctx.beginPath();
@ -394,6 +394,32 @@ const Painter = {
}
}
} else if (element instanceof Mesh && fill_mode === 'element') {
for (var fkey in element.faces) {
var face = element.faces[fkey];
if (face.vertices.length <= 2 || face.getTexture() !== texture) continue;
ctx.beginPath();
let min_x = Project.texture_width;
let min_y = Project.texture_height;
let max_x = 0;
let max_y = 0;
face.vertices.forEach(vkey => {
if (!face.uv[vkey]) return;
min_x = Math.min(min_x, face.uv[vkey][0]);
min_y = Math.min(min_y, face.uv[vkey][1]);
max_x = Math.max(max_x, face.uv[vkey][0]);
max_y = Math.max(max_y, face.uv[vkey][1]);
})
ctx.rect(
Math.floor(min_x) * uvFactorX,
Math.floor(min_y) * uvFactorY,
(Math.ceil(max_x) - Math.floor(min_x)) * uvFactorX,
(Math.ceil(max_y) - Math.floor(min_y)) * uvFactorY,
)
ctx.fill()
}
} else if (fill_mode === 'face') {
ctx.fill()
} else {
@ -1138,7 +1164,7 @@ BARS.defineActions(function() {
condition: () => Toolbox && Toolbox.selected.id === 'fill_tool',
options: {
face: true,
cube: true,
element: true,
color_connected: true,
color: true,
}

View File

@ -13,6 +13,13 @@ const TextureGenerator = {
south: {c1: '#f8dd72', c2: '#FFF899', place: t => {return {x: t.posx+t.z+t.x+t.z,y: t.posy+t.z, w: t.x, h: t.y}}},
},
addBitmapDialog() {
let type_options = {
blank: 'dialog.create_texture.type.blank',
template: 'dialog.create_texture.type.template',
}
if (!Project.box_uv) {
type_options.color_map = 'dialog.create_texture.type.color_map';
}
var dialog = new Dialog({
id: 'add_bitmap',
title: tl('action.create_texture'),
@ -22,7 +29,7 @@ const TextureGenerator = {
folder: {label: 'dialog.create_texture.folder', condition: Format.id == 'java_block'},
color: {label: 'data.color', type: 'color', colorpicker: TextureGenerator.background_color},
resolution: {label: 'dialog.create_texture.resolution', description: 'dialog.create_texture.resolution.desc', type: 'select', value: 16, condition: (form) => (form.template), options: {
resolution: {label: 'dialog.create_texture.resolution', description: 'dialog.create_texture.resolution.desc', type: 'select', value: 16, condition: (form) => (form.type == 'template'), options: {
16: '16',
32: '32',
64: '64',
@ -30,28 +37,28 @@ const TextureGenerator = {
256: '256',
512: '512',
}},
resolution_vec: {label: 'dialog.create_texture.resolution', type: 'vector', condition: (form) => (!form.template), dimensions: 2, value: [16, 16], min: 16, max: 2048},
resolution_vec: {label: 'dialog.create_texture.resolution', type: 'vector', condition: (form) => (form.type == 'blank'), dimensions: 2, value: [16, 16], min: 16, max: 2048},
section2: "_",
template: {label: 'dialog.create_texture.template', type: 'checkbox', condition: Cube.all.length || Mesh.all.length},
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.template)},
compress: {label: 'dialog.create_texture.compress', description: 'dialog.create_texture.compress.desc', type: 'checkbox', value: true, condition: (form) => (form.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.template && form.rearrange_uv)},
double_use: {label: 'dialog.create_texture.double_use', description: 'dialog.create_texture.double_use.desc', type: 'checkbox', value: true, condition: (form) => (form.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.template && form.rearrange_uv && Mesh.selected.length)},
box_uv: {label: 'dialog.project.uv_mode.box_uv', type: 'checkbox', value: false, condition: (form) => (form.template && !Project.box_uv)},
padding: {label: 'dialog.create_texture.padding', description: 'dialog.create_texture.padding.desc', type: 'checkbox', value: false, condition: (form) => (form.template && form.rearrange_uv)},
rearrange_uv:{label: 'dialog.create_texture.rearrange_uv', description: 'dialog.create_texture.rearrange_uv.desc', type: 'checkbox', value: true, condition: (form) => (form.type == 'template')},
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)},
},
onFormChange(form) {
if (form.template && TextureGenerator.background_color.get().toHex8() === 'ffffffff') {
if (form.type == 'template' && TextureGenerator.background_color.get().toHex8() === 'ffffffff') {
TextureGenerator.background_color.set('#00000000')
}
},
onConfirm: function(results) {
results.particle = 'auto';
if (!results.template) {
if (results.type == 'blank') {
results.resolution = results.resolution_vec;
}
dialog.hide()
@ -91,12 +98,12 @@ const TextureGenerator = {
if (typeof after === 'function') {
after(texture)
}
if (!options.template) {
if (options.type == 'blank') {
Undo.finishEdit('Create blank texture', {textures: [texture], selected_texture: true, bitmap: true})
}
return texture;
}
if (options.template === true) {
if (options.type == 'template') {
if (Project.box_uv || options.box_uv) {
if (Mesh.selected[0]) {
Blockbench.showQuickMessage('message.box_uv_for_meshes', 1600);
@ -105,6 +112,8 @@ const TextureGenerator = {
} else {
TextureGenerator.generateFaceTemplate(options, makeTexture);
}
} else if (options.type == 'color_map') {
TextureGenerator.generateColorMapTemplate(options, makeTexture);
} else {
Undo.initEdit({textures: [], selected_texture: true})
TextureGenerator.generateBlank(options.resolution[1], options.resolution[0], options.color, makeTexture)
@ -995,6 +1004,125 @@ const TextureGenerator = {
uv_mode: true
})
},
generateColorMapTemplate(options, cb) {
var background_color = options.color;
var texture = options.texture;
var new_resolution = [];
var face_list = [];
var element_list = (Format.single_texture ? Outliner.elements : Outliner.selected).filter(el => {
return (el instanceof Cube || el instanceof Mesh) && el.visibility;
});
Undo.initEdit({
textures: [],
elements: element_list,
uv_only: true,
selected_texture: true,
uv_mode: true
})
element_list.forEach(element => {
for (let fkey in element.faces) {
let face = element.faces[fkey];
if (element instanceof Mesh && face.vertices.length <= 2) continue;
if (element instanceof Cube && face.texture === null) continue;
face_list.push({element, fkey, face});
}
})
if (face_list.length == 0) {
Blockbench.showMessage('No valid cubes', 'center')
return;
}
var max_size = Math.ceil(Math.sqrt(face_list.length));
if (options.power) {
max_size = Math.getNextPower(max_size, 16);
} else {
max_size = Math.ceil(max_size/16)*16;
}
new_resolution = [max_size, max_size];
if (background_color.getAlpha() != 0) {
background_color = background_color.toRgbString()
}
var canvas = document.createElement('canvas')
canvas.width = new_resolution[0];
canvas.height = new_resolution[1];
var ctx = canvas.getContext('2d');
ctx.fillStyle = typeof background_color == 'string' ? background_color : 'white';
ctx.imageSmoothingEnabled = false;
let texture_ctxs = {};
//Drawing
face_list.forEach(({face, fkey}, i) => {
let x = i % max_size;
let y = Math.floor(i / max_size);
let texture;
if (!Format.single_texture) {
if (face.texture !== undefined && face.texture !== null) {
texture = face.getTexture()
}
} else {
texture = Texture.getDefault();
}
if (texture && texture.img) {
if (!texture_ctxs[texture.uuid]) {
texture_ctxs[texture.uuid] = new CanvasFrame(texture.img).ctx;
}
let color = Painter.getPixelColor(
texture_ctxs[texture.uuid],
Math.floor((face instanceof CubeFace ? face.uv : face.uv[face.vertices[0]])[0] / Project.texture_width * texture.width),
Math.floor((face instanceof CubeFace ? face.uv : face.uv[face.vertices[0]])[1] / Project.texture_height * texture.height),
);
ctx.fillStyle = color ? color.toHexString() : 'white';
} else {
ctx.fillStyle = typeof background_color == 'string' ? background_color : 'white';
}
ctx.fillRect(x, y, 1, 1);
if (face instanceof CubeFace) {
face.uv = [x+0.25, y+0.25, x+0.75, y+0.75];
} else if (face instanceof MeshFace) {
let vertices = face.getSortedVertices();
face.uv[vertices[0]] = [x+0.75, y+0.25];
face.uv[vertices[1]] = [x+0.25, y+0.25];
face.uv[vertices[2]] = [x+0.25, y+0.75];
if (vertices[3]) face.uv[vertices[3]] = [x+0.75, y+0.75];
console.log(vertices, face.uv)
}
})
var dataUrl = canvas.toDataURL()
var texture = cb(dataUrl)
TextureGenerator.changeProjectResolution(new_resolution[0], new_resolution[1]);
if (texture) {
face_list.forEach(({face, fkey}, i) => {
face.texture = texture.uuid;
})
element_list.forEach(function(element) {
element.preview_controller.updateFaces(element);
element.preview_controller.updateUV(element);
if (typeof element.autouv !== 'undefined') {
element.autouv = 0;
}
})
}
updateSelection()
Undo.finishEdit('Create template', {
textures: [texture],
bitmap: true,
elements: element_list,
selected_texture: true,
uv_only: true,
uv_mode: true
})
},
//Misc
changeProjectResolution(width, height) {
let factor_x = width / Project.texture_width;

View File

@ -1214,6 +1214,7 @@ const UVEditor = {
'uv_maximize',
'uv_auto',
'uv_rel_auto',
'snap_uv_to_pixels',
{icon: 'rotate_90_degrees_ccw', condition: () => Format.uv_rotation, name: 'menu.uv.mapping.rotation', children: function() {
var off = 'radio_button_unchecked'
var on = 'radio_button_checked'
@ -1516,6 +1517,38 @@ BARS.defineActions(function() {
Undo.finishEdit('Set face tint')
}
})
new Action('snap_uv_to_pixels', {
icon: 'grid_goldenratio',
category: 'uv',
condition: () => UVEditor.getMappableElements().length,
click: function (event) {
let elements = UVEditor.getMappableElements();
Undo.initEdit({elements, uv_only: true})
elements.forEach(element => {
let selected_vertices = element instanceof Mesh && element.getSelectedVertices();
UVEditor.selected_faces.forEach(fkey => {
if (!element.faces[fkey]) return;
let face = element.faces[fkey];
if (element instanceof Mesh) {
face.vertices.forEach(vkey => {
if ((!selected_vertices.length || selected_vertices.includes(vkey)) && face.uv[vkey]) {
face.uv[vkey][0] = Math.clamp(Math.round(face.uv[vkey][0]), 0, Project.texture_width);
face.uv[vkey][1] = Math.clamp(Math.round(face.uv[vkey][1]), 0, Project.texture_height);
}
})
} else if (element instanceof Cube) {
face.uv[0] = Math.clamp(Math.round(face.uv[0]), 0, Project.texture_width);
face.uv[1] = Math.clamp(Math.round(face.uv[1]), 0, Project.texture_height);
face.uv[2] = Math.clamp(Math.round(face.uv[2]), 0, Project.texture_width);
face.uv[3] = Math.clamp(Math.round(face.uv[3]), 0, Project.texture_height);
}
})
element.preview_controller.updateUV(element);
})
UVEditor.loadData();
Undo.finishEdit('Set automatic cullface')
}
})
new Toggle('toggle_uv_overlay', {

File diff suppressed because one or more lines are too long

View File

@ -386,7 +386,10 @@
"dialog.animation_export.title": "Select Animations to Export",
"dialog.create_texture.folder": "Folder",
"dialog.create_texture.template": "Template",
"dialog.create_texture.type": "Type",
"dialog.create_texture.type.blank": "Blank",
"dialog.create_texture.type.template": "Texture Template",
"dialog.create_texture.type.color_map": "Solid Color Template",
"dialog.create_texture.rearrange_uv": "Rearrange UV",
"dialog.create_texture.rearrange_uv.desc": "Create a new UV arrangement to give every face its own spot on the texture",
"dialog.create_texture.compress": "Compress Template",
@ -732,7 +735,7 @@
"action.fill_mode.face": "Face",
"action.fill_mode.color_connected": "Connected Colors",
"action.fill_mode.color": "Colors",
"action.fill_mode.cube": "Cube",
"action.fill_mode.element": "Element",
"action.draw_shape_type": "Shape Type",
"action.draw_shape_type.rectangle": "Rectangle",
"action.draw_shape_type.rectangle_h": "Rectangle (Hollow)",
@ -1156,6 +1159,8 @@
"action.slider_face_tint.desc": "Set the tint index of the current face. -1 means unset.",
"action.toggle_uv_overlay": "Toggle UV Overlay",
"action.toggle_uv_overlay.desc": "When enabled, displays all UV mapping overlays above the texture.",
"action.snap_uv_to_pixels": "Snap UV to Pixels",
"action.snap_uv_to_pixels.desc": "Snaps the selected UV vertices to the pixel grid",
"action.remove_blank_faces": "Remove Blank Faces",
"action.remove_blank_faces.desc": "Deletes all untextured faces of the selection",
@ -1252,6 +1257,7 @@
"menu.file.recent": "Recent",
"menu.file.recent.clear": "Clear Recent Files",
"menu.file.import": "Import",
"menu.file.import.import_open_project": "Import Open Project",
"menu.file.export": "Export",
"menu.file.preferences": "Preferences",
"menu.transform.rotate": "Rotate",