Merge branch 'texture-groups' into next

This commit is contained in:
JannisX11 2024-06-08 15:19:20 +02:00
commit b740c56376
16 changed files with 767 additions and 215 deletions

View File

@ -47,8 +47,10 @@
font-weight: normal;
display: inline-block;
}
.code {
code, .code {
font-family: var(--font-code);
}
.code {
font-size: 16px;
}
.small_text {

View File

@ -471,8 +471,7 @@
bottom: 4px;
width: 4px;
margin-left: 10px;
border-left: 2px solid var(--color-text);
opacity: 0.2;
border-left: 2px solid var(--color-guidelines);
pointer-events: none;
}
.drag_hover[order]::before {
@ -639,6 +638,21 @@
z-index: 100;
border: 2px solid var(--color-accent);
box-shadow: 0 0 16px black;
height: 48px;
width: 48px;
position: absolute;
pointer-events: none;
}
.texture_group_drag_helper {
position: absolute;
z-index: 100;
min-height: 24px;
min-width: 120px;
padding: 4px;
border: 2px solid var(--color-accent);
background-color: var(--color-ui);
box-shadow: 0 0 16px black;
pointer-events: none;
}
.icon_placeholder {
width: 48px;
@ -710,6 +724,63 @@
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
.texture_group {
padding-bottom: 4px;
}
.texture_group_head {
height: 32px;
padding: 4px;
padding-right: 8px;
display: flex;
gap: 5px;
color: var(--color-subtle_text);
}
.texture_group_head:hover {
color: var(--color-text);
}
.texture_group_head > .icon-open-state {
text-align: center;
width: 21px;
margin-top: 4px;
flex-shrink: 0;
}
.texture_group_head > label {
flex-shrink: 1;
overflow: hidden;
white-space: nowrap;
}
.texture_group_head.folded > label {
max-width: calc(60% - 50px);
min-width: 30px;
}
.texture_group_head > .in_list_button {
margin-left: auto;
}
.texture_group_mini_icon_list {
display: flex;
gap: 2px;
margin-right: auto;
margin-left: 4px;
max-width: 36%;
overflow: hidden;
}
.texture_group_mini_icon_list > .texture_mini_icon {
width: 24px;
height: 24px;
border-radius: 50%;
flex-shrink: 0;
overflow: hidden;
background-color: var(--color-ui);
flex-shrink: 0;
margin-right: -8px;
border: 1px solid var(--color-border);
}
.texture_group_list {
margin-left: 14px;
padding-left: 6px;
border-left: 2px solid var(--color-guidelines);
}
#texture_animation_playback {
display: flex;

View File

@ -343,6 +343,7 @@
--color-checkerboard: #1c2026;
--color-menu_separator: #b0afba;
--color-guidelines: rgba(136, 150, 157, 0.35);
--color-close: #d62e3f;
--color-confirm: #90ee90;

View File

@ -146,6 +146,7 @@
<script src="js/modeling/mirror_modeling.js"></script>
<script src="js/texturing/layers.js"></script>
<script src="js/texturing/textures.js"></script>
<script src="js/texturing/texture_groups.js"></script>
<script src="js/texturing/texture_flipbook.js"></script>
<script src="js/texturing/uv.js"></script>
<script src="js/texturing/painter.js"></script>

View File

@ -538,8 +538,6 @@ function setupInterface() {
reference.select();
});
document.getElementById('texture_list').addEventListener('click', e => unselectTextures());
$(Panels.timeline.node).mousedown((event) => {
setActivePanel('timeline');
})

View File

@ -189,6 +189,11 @@ var codec = new Codec('project', {
if (options.absolute_paths == false) delete t.path;
model.textures.push(t);
})
for (let texture_group of TextureGroup.all) {
if (!model.texture_groups) model.texture_groups = [];
let copy = texture_group.getSaveCopy();
model.texture_groups.push(copy);
}
if (Animation.all.length) {
model.animations = [];
@ -328,6 +333,11 @@ var codec = new Codec('project', {
Project.texture_height = model.resolution.height;
}
if (model.texture_groups) {
model.texture_groups.forEach(tex_group => {
new TextureGroup(tex_group, tex_group.uuid).add(false);
})
}
if (model.textures) {
model.textures.forEach(tex => {
var tex_copy = new Texture(tex, tex.uuid).add(false);
@ -533,6 +543,11 @@ var codec = new Codec('project', {
}
}
if (model.texture_groups) {
model.texture_groups.forEach(tex_group => {
new TextureGroup(tex_group, tex_group.uuid).add(false);
})
}
if (model.textures && (!Format.single_texture || Texture.all.length == 0)) {
new_textures.replace(model.textures.map(loadTexture))
}

View File

@ -730,7 +730,6 @@ function calculateVisibleBox() {
codec.dispatchEvent('parsed', {model: data.object});
loadTextureDraggable()
Canvas.updateAllBones()
setProjectTitle()
if (isApp && Project.geometry_name) {

View File

@ -114,7 +114,6 @@ function parseGeometry(data) {
codec.dispatchEvent('parsed', {model: data.object});
loadTextureDraggable()
Canvas.updateAllBones()
setProjectTitle()
if (isApp && Project.geometry_name && Project.BedrockEntityManager) {

View File

@ -149,7 +149,6 @@ const codec = new Codec('skin_model', {
if (data.camera_angle) {
main_preview.loadAnglePreset(DefaultCameraPresets.find(p => p.id == data.camera_angle))
}
loadTextureDraggable()
Canvas.updateAllBones()
Canvas.updateVisibility()
setProjectTitle()

View File

@ -59,6 +59,7 @@ class ModelProject {
this.mesh_selection = {};
this.textures = [];
this.selected_texture = null;
this.texture_groups = [];
this.outliner = [];
this.animations = [];
this.animation_controllers = [];
@ -207,6 +208,7 @@ class ModelProject {
BarItems.edit_mode_uv_overlay.updateEnabledState();
Panels.textures.inside_vue.textures = Texture.all;
Panels.textures.inside_vue.texture_groups = TextureGroup.all;
Panels.layers.inside_vue.layers = Texture.selected ? Texture.selected.layers : [];
scene.add(this.model_3d);
@ -279,8 +281,6 @@ class ModelProject {
updateProjectResolution();
Validator.validate();
Vue.nextTick(() => {
loadTextureDraggable();
if (this.on_next_upen instanceof Array) {
this.on_next_upen.forEach(callback => callback());
delete this.on_next_upen;
@ -607,6 +607,7 @@ function selectNoProject() {
UVEditor.vue.all_elements = [];
Interface.Panels.textures.inside_vue.textures = [];
Interface.Panels.textures.inside_vue.texture_groups = [];
Panels.animations.inside_vue.animations = [];
Panels.animations.inside_vue.animation_controllers = [];

View File

@ -395,10 +395,6 @@ const TickUpdates = {
delete TickUpdates.UVEditor;
UVEditor.loadData()
}
if (TickUpdates.texture_list) {
delete TickUpdates.texture_list;
loadTextureDraggable();
}
if (TickUpdates.keyframe_selection) {
delete TickUpdates.keyframe_selection;
Vue.nextTick(updateKeyframeSelection)

View File

@ -1433,7 +1433,8 @@ Interface.definePanels(function() {
depth: Number
},
data() {return {
outliner_colors: settings.outliner_colors
outliner_colors: settings.outliner_colors,
markerColors
}},
computed: {
indentation() {

View File

@ -0,0 +1,135 @@
class TextureGroup {
constructor(data, uuid) {
this.uuid = uuid ?? guid();
this.folded = false;
if (data) this.extend(data);
}
extend(data) {
for (let key in TextureGroup.properties) {
TextureGroup.properties[key].merge(this, data)
}
return this;
}
add() {
TextureGroup.all.push(this);
return this;
}
select() {
let textures = this.getTextures();
if (textures[0]) textures[0].select();
for (let texture of textures) {
if (!texture.selected) texture.multi_selected = true;
}
return this;
}
remove() {
TextureGroup.all.remove(this);
}
showContextMenu(event) {
Prop.active_panel = 'textures';
TextureGroup.active_menu_group = this;
this.menu.open(event, this);
}
rename() {
Blockbench.textPrompt('generic.rename', this.name, (name) => {
if (name && name !== this.name) {
Undo.initEdit({texture_groups: [this]});
this.name = name;
Undo.finishEdit('Rename texture group');
}
})
return this;
}
getTextures() {
return Texture.all.filter(texture => texture.group == this.uuid);
}
getUndoCopy() {
let copy = {
uuid: this.uuid,
index: TextureGroup.all.indexOf(this)
};
for (let key in TextureGroup.properties) {
TextureGroup.properties[key].copy(this, copy)
}
return copy;
}
getSaveCopy() {
let copy = {
uuid: this.uuid
};
for (let key in TextureGroup.properties) {
TextureGroup.properties[key].copy(this, copy)
}
return copy;
}
}
Object.defineProperty(TextureGroup, 'all', {
get() {
return Project.texture_groups || [];
},
set(arr) {
Project.texture_groups.replace(arr);
}
})
new Property(TextureGroup, 'string', 'name', {default: tl('data.texture_group')});
TextureGroup.prototype.menu = new Menu('texture_group', [
new MenuSeparator('manage'),
'rename',
{
icon: 'fa-leaf',
name: 'menu.texture_group.resolve',
click(texture_group) {
let textures = texture_group.getTextures();
Undo.initEdit({textures, texture_groups: [texture_group]});
texture_group.remove();
textures.forEach(texture => {
texture.group = '';
})
Undo.finishEdit('Resolve texture group', {textures, texture_groups: []});
}
},
], {
onClose() {
setTimeout(() => {
TextureGroup.active_menu_group = null;
}, 10);
}
})
/**
ToDo:
- Auto-generate groups
- Grid view?
- Search
*/
SharedActions.add('rename', {
condition: () => Prop.active_panel == 'textures' && TextureGroup.active_menu_group,
run() {
TextureGroup.active_menu_group.rename();
}
})
BARS.defineActions(function() {
new Action('create_texture_group', {
icon: 'perm_media',
category: 'textures',
click() {
let texture_group = new TextureGroup();
texture_group.name = 'Texture Group ' + (TextureGroup.all.length+1);
let textures_to_add = Texture.all.filter(tex => tex.selected || tex.multi_selected);
Undo.initEdit({texture_groups: [], textures: textures_to_add});
if (textures_to_add.length) {
for (let texture of textures_to_add) {
texture.group = texture_group.uuid;
}
let first = Texture.selected || textures_to_add[0];
texture_group.name = first.name.replace(/\.\w+$/, '') + ' Group';
}
texture_group.add(false);
Undo.finishEdit('Add texture group', {texture_groups: [texture_group], textures: textures_to_add});
}
})
});

View File

@ -2,9 +2,9 @@
//Textures
class Texture {
constructor(data, uuid) {
var scope = this;
let scope = this;
//Info
for (var key in Texture.properties) {
for (let key in Texture.properties) {
Texture.properties[key].reset(this);
}
//meta
@ -342,6 +342,15 @@ class Texture {
case 3: return tl('texture.error.parent'); break;
}
}
getGroup() {
if (!this.group) return;
let group = TextureGroup.all.find(group => group.uuid == this.group);
if (group) {
return group;
} else {
this.group = '';
}
}
getUndoCopy(bitmap) {
var copy = {};
for (var key in Texture.properties) {
@ -900,6 +909,10 @@ class Texture {
if (this.layers_enabled && !this.selected_layer && this.layers[0]) {
this.layers[0].select();
}
if (this.group) {
let group = this.getGroup();
if (group) group.folded = false;
}
this.scrollTo();
if (this.render_mode == 'layered') {
Canvas.updatePixelGrid()
@ -935,7 +948,6 @@ class Texture {
Project.textures.push(this)
}
Blockbench.dispatchEvent( 'add_texture', {texture: this})
loadTextureDraggable()
if ((Format.single_texture || Format.single_texture_default) && Cube.all.length) {
Canvas.updateAllFaces()
@ -1400,7 +1412,7 @@ class Texture {
return this;
}
scrollTo() {
var el = $(`#texture_list > li[texid=${this.uuid}]`)
var el = $(`#texture_list li.texture[texid=${this.uuid}]`)
if (el.length === 0 || Texture.all.length < 2) return;
var outliner_pos = $('#texture_list').offset().top
@ -2052,6 +2064,7 @@ class Texture {
new Property(Texture, 'string', 'folder')
new Property(Texture, 'string', 'namespace')
new Property(Texture, 'string', 'id')
new Property(Texture, 'string', 'group')
new Property(Texture, 'number', 'width')
new Property(Texture, 'number', 'height')
new Property(Texture, 'number', 'uv_width')
@ -2094,142 +2107,7 @@ function saveTextures(lazy = false) {
})
}
function loadTextureDraggable() {
Vue.nextTick(function() {
setTimeout(function() {
$('li.texture:not(.ui-draggable)').draggable({
revertDuration: 0,
cursorAt: { left: 2, top: -5 },
revert: 'invalid',
appendTo: 'body',
zIndex: 19,
distance: 12,
delay: 120,
helper: function(e) {
var t = $(e.target)
if (!t.hasClass('texture')) t = t.parent()
if (!t.hasClass('texture')) t = t.parent()
return t.find('.texture_icon_wrapper').clone().addClass('texture_drag_helper').attr('texid', t.attr('texid'))
},
drag: function(event, ui) {
$('.outliner_node[order]').attr('order', null);
$('.drag_hover').removeClass('drag_hover');
$('.texture[order]').attr('order', null)
if ($('#cubes_list li.outliner_node:hover').length) {
var tar = $('#cubes_list li.outliner_node:hover').last()
tar.addClass('drag_hover').attr('order', '0');
/*
var element = Outliner.root.findRecursive('uuid', tar.attr('id'))
if (element) {
tar.attr('order', '0')
}*/
} else if ($('#texture_list li:hover').length) {
let node = $('#texture_list > .texture:hover')
if (node.length) {
var target_tex = Texture.all.findInArray('uuid', node.attr('texid'));
index = Texture.all.indexOf(target_tex);
let offset = event.clientY - node[0].offsetTop;
if (offset > 24) {
node.attr('order', '1')
} else {
node.attr('order', '-1')
}
}
}
},
stop: function(event, ui) {
setTimeout(function() {
$('.texture[order]').attr('order', null);
$('.outliner_node[order]').attr('order', null);
var tex = Texture.all.findInArray('uuid', ui.helper.attr('texid'));
if (!tex) return;
if ($('.preview:hover').length > 0) {
var data = Canvas.raycast(event)
if (data.element && data.face) {
var elements = data.element.selected ? UVEditor.getMappableElements() : [data.element];
if (Format.per_group_texture) {
elements = [];
let groups = Group.selected ? [Group.selected] : [];
Outliner.selected.forEach(el => {
if (el.faces && el.parent instanceof Group) groups.safePush(el.parent);
});
Undo.initEdit({outliner: true});
groups.forEach(group => {
group.texture = '';
group.forEachChild(child => {
if (child.preview_controller?.updateFaces) child.preview_controller.updateFaces(child);
})
})
} else {
Undo.initEdit({elements})
elements.forEach(element => {
element.applyTexture(tex, event.shiftKey || Pressing.overrides.shift || [data.face])
})
}
Undo.finishEdit('Apply texture')
}
} else if ($('#texture_list:hover').length > 0) {
let index = Texture.all.length-1
let node = $('#texture_list > .texture:hover')
if (node.length) {
var target_tex = Texture.all.findInArray('uuid', node.attr('texid'));
index = Texture.all.indexOf(target_tex);
let own_index = Texture.all.indexOf(tex)
if (own_index == index) return;
if (own_index < index) index--;
if (event.clientY - node[0].offsetTop > 24) index++;
}
Undo.initEdit({texture_order: true})
Texture.all.remove(tex)
Texture.all.splice(index, 0, tex)
Canvas.updateLayeredTextures()
Undo.finishEdit('Reorder textures')
} else if ($('#cubes_list:hover').length) {
let target_node = $('#cubes_list li.outliner_node.drag_hover').last().get(0);
$('.drag_hover').removeClass('drag_hover');
if (!target_node) return;
let uuid = target_node.id;
let target = OutlinerNode.uuids[uuid];
let array = [];
if (target.type === 'group') {
target.forEachChild((element) => {
array.push(element);
})
} else {
array = selected.includes(target) ? selected.slice() : [target];
}
array = array.filter(element => element.applyTexture);
if (Format.per_group_texture) {
let group = target.type === 'group' ? target : null;
if (!group) group = target.parent;
array = [];
Undo.initEdit({group});
group.texture = tex.uuid;
group.forEachChild(child => {
if (child.preview_controller?.updateFaces) child.preview_controller.updateFaces(child);
})
} else {
Undo.initEdit({elements: array, uv_only: true})
array.forEach(element => {
element.applyTexture(tex, true);
});
}
Undo.finishEdit('Apply texture');
UVEditor.loadData();
} else if ($('#uv_viewport:hover').length) {
UVEditor.applyTexture(tex);
}
}, 10)
}
})
}, 42)
})
console.warn('loadTextureDraggable no longer exists');
}
function unselectTextures() {
Texture.all.forEach(function(s) {
@ -2330,15 +2208,15 @@ BARS.defineActions(function() {
icon: 'library_add',
category: 'textures',
keybind: new Keybind({key: 't', ctrl: true}),
click() {
var start_path;
click(event, context) {
let start_path;
if (!isApp) {} else
if (Texture.all.length > 0) {
var arr = Texture.all[0].path.split(osfs)
let arr = Texture.all[0].path.split(osfs)
arr.splice(-1)
start_path = arr.join(osfs)
} else if (Project.export_path) {
var arr = Project.export_path.split(osfs)
let arr = Project.export_path.split(osfs)
arr.splice(-3)
arr.push('textures')
start_path = arr.join(osfs)
@ -2351,11 +2229,15 @@ BARS.defineActions(function() {
multiple: true,
startpath: start_path
}, function(results) {
var new_textures = []
Undo.initEdit({textures: new_textures})
let new_textures = [];
let texture_group = context instanceof TextureGroup ? context : Texture.selected?.getGroup();
Undo.initEdit({textures: new_textures});
results.forEach(function(f) {
var t = new Texture({name: f.name}).fromFile(f).add(false).fillParticle()
new_textures.push(t)
let t = new Texture({name: f.name}).fromFile(f).add(false).fillParticle();
new_textures.push(t);
if (texture_group) {
t.group = texture_group.uuid;
}
})
Undo.finishEdit('Add texture')
})
@ -2421,6 +2303,298 @@ BARS.defineActions(function() {
Interface.definePanels(function() {
let texture_component = Vue.extend({
props: {
texture: Texture
},
methods: {
getDescription(texture) {
if (texture.error) {
return texture.getErrorMessage()
} else {
let message = texture.width + ' x ' + texture.height + 'px';
if (!Format.image_editor) {
let uv_size = texture.width / texture.getUVWidth() * 16;
message += ` (${trimFloatNumber(uv_size, 2)}x)`;
}
if (texture.frameCount > 1) {
message += ` - ${texture.currentFrame+1}/${texture.frameCount}`
}
return message;
}
},
getTextureIconOffset(texture) {
if (!texture.currentFrame) return;
let val = texture.currentFrame * -48 * (texture.display_height / texture.width);
return `${val}px`;
},
dragTexture(e1) {
if (e1.button == 1) return;
if (getFocusedTextInput()) return;
convertTouchEvent(e1);
let texture = this.texture;
let active = false;
let helper;
let timeout;
let last_event = e1;
let vue_scope = this;
// scrolling
let list = document.getElementById('texture_list');
let list_offset = $(list).offset();
let scrollInterval = function() {
if (!active) return;
if (mouse_pos.y < list_offset.top) {
list.scrollTop += (mouse_pos.y - list_offset.top) / 7 - 3;
} else if (mouse_pos.y > list_offset.top + list.clientHeight) {
list.scrollTop += (mouse_pos.y - (list_offset.top + list.clientHeight)) / 6 + 3;
}
}
let scrollIntervalID;
function move(e2) {
convertTouchEvent(e2);
let offset = [
e2.clientX - e1.clientX,
e2.clientY - e1.clientY,
]
if (!active) {
let distance = Math.sqrt(Math.pow(offset[0], 2) + Math.pow(offset[1], 2))
if (Blockbench.isTouch) {
if (distance > 20 && timeout) {
clearTimeout(timeout);
timeout = null;
} else {
document.getElementById('texture_list').scrollTop += last_event.clientY - e2.clientY;
}
} else if (distance > 6) {
active = true;
}
}
if (!active) return;
if (e2) e2.preventDefault();
if (open_menu) open_menu.hide();
if (!helper) {
helper = vue_scope.$el.cloneNode();
helper.classList.add('texture_drag_helper');
helper.setAttribute('texid', texture.uuid);
document.body.append(helper);
scrollIntervalID = setInterval(scrollInterval, 1000/60)
Blockbench.addFlag('dragging_textures');
}
helper.style.left = `${e2.clientX}px`;
helper.style.top = `${e2.clientY}px`;
// drag
$('.outliner_node[order]').attr('order', null);
$('.drag_hover').removeClass('drag_hover');
$('.texture[order]').attr('order', null)
let target = $('#cubes_list li.outliner_node:hover').last();
if (target.length) {
tar.addClass('drag_hover').attr('order', '0');
return;
}
target = document.querySelector('#texture_list li.texture:hover');
if (target) {
let offset = e2.clientY - $(target).offset().top;
target.setAttribute('order', offset > 24 ? '1' : '-1');
return;
}
target = document.querySelector('#texture_list .texture_group_head:hover');
if (target) {
target.classList.add('drag_hover');
target.setAttribute('order', '0');
return;
}
if (document.querySelector('#texture_list:hover')) {
let nodes = document.querySelectorAll('#texture_list > li');
if (nodes.length) {
let target = nodes[nodes.length-1];
target.setAttribute('order', '1');
target.classList.add('drag_hover');
}
}
last_event = e2;
}
async function off(e2) {
if (helper) helper.remove();
clearInterval(scrollIntervalID);
removeEventListeners(document, 'mousemove touchmove', move);
removeEventListeners(document, 'mouseup touchend', off);
e2.stopPropagation();
$('.outliner_node[order]').attr('order', null);
$('.drag_hover').removeClass('drag_hover');
$('.texture[order]').attr('order', null)
if (Blockbench.isTouch) clearTimeout(timeout);
if (!active || Menu.open) return;
await new Promise(r => setTimeout(r, 10));
Blockbench.removeFlag('dragging_textures');
if ($('.preview:hover').length > 0) {
var data = Canvas.raycast(e2)
if (data.element && data.face) {
var elements = data.element.selected ? UVEditor.getMappableElements() : [data.element];
if (Format.per_group_texture) {
elements = [];
let groups = Group.selected ? [Group.selected] : [];
Outliner.selected.forEach(el => {
if (el.faces && el.parent instanceof Group) groups.safePush(el.parent);
});
Undo.initEdit({outliner: true});
groups.forEach(group => {
group.texture = texture.uuid;
group.forEachChild(child => {
if (child.preview_controller?.updateFaces) child.preview_controller.updateFaces(child);
})
})
} else {
Undo.initEdit({elements});
elements.forEach(element => {
element.applyTexture(texture, e2.shiftKey || Pressing.overrides.shift || [data.face])
})
}
Undo.finishEdit('Apply texture')
}
} else if ($('#texture_list:hover').length > 0) {
let index = Texture.all.length-1;
let texture_node = document.querySelector('#texture_list li.texture:hover');
let target_group_head = document.querySelector('#texture_list .texture_group_head:hover');
let new_group = '';
if (target_group_head) {
new_group = target_group_head.parentNode.id;
} else if (texture_node) {
let target_tex = Texture.all.findInArray('uuid', texture_node.getAttribute('texid'));
index = Texture.all.indexOf(target_tex);
let own_index = Texture.all.indexOf(texture)
if (own_index == index) return;
let offset = e2.clientY - $(texture_node).offset().top;
if (own_index < index) index--;
if (offset > 24) index++;
new_group = target_tex.group;
}
Undo.initEdit({texture_order: true, textures: texture.group != new_group ? [texture] : null});
Texture.all.remove(texture);
Texture.all.splice(index, 0, texture);
texture.group = new_group;
Canvas.updateLayeredTextures();
Undo.finishEdit('Rearrange textures');
} else if ($('#cubes_list:hover').length) {
let target_node = $('#cubes_list li.outliner_node.drag_hover').last().get(0);
$('.drag_hover').removeClass('drag_hover');
if (!target_node) return;
let uuid = target_node.id;
let target = OutlinerNode.uuids[uuid];
let array = [];
if (target.type === 'group') {
target.forEachChild((element) => {
array.push(element);
})
} else {
array = selected.includes(target) ? selected.slice() : [target];
}
array = array.filter(element => element.applyTexture);
if (Format.per_group_texture) {
let group = target.type === 'group' ? target : null;
if (!group) group = target.parent;
array = [];
Undo.initEdit({group});
group.texture = texture.uuid;
group.forEachChild(child => {
if (child.preview_controller?.updateFaces) child.preview_controller.updateFaces(child);
})
} else {
Undo.initEdit({elements: array, uv_only: true})
array.forEach(element => {
element.applyTexture(texture, true);
});
}
Undo.finishEdit('Apply texture');
UVEditor.loadData();
} else if ($('#uv_viewport:hover').length) {
UVEditor.applyTexture(texture);
}
/*convertTouchEvent(e2);
let target = document.elementFromPoint(e2.clientX, e2.clientY);
[drop_target] = eventTargetToNode(target);
if (drop_target) {
moveOutlinerSelectionTo(item, drop_target, e2, order);
} else if ($('#texture_list').is(':hover')) {
moveOutlinerSelectionTo(item, undefined, e2);*/
}
if (Blockbench.isTouch) {
timeout = setTimeout(() => {
active = true;
move(e1);
}, 320)
}
addEventListeners(document, 'mousemove touchmove', move, {passive: false});
addEventListeners(document, 'mouseup touchend', off, {passive: false});
}
},
template: `
<li
v-bind:class="{ selected: texture.selected, multi_selected: texture.multi_selected, particle: texture.particle, use_as_default: texture.use_as_default}"
v-bind:texid="texture.uuid"
class="texture"
@click.stop="texture.select($event)"
@dblclick="texture.openMenu($event)"
@mousedown.stop="dragTexture($event)" @touchstart.stop="dragTexture($event)"
@contextmenu.prevent.stop="texture.showContextMenu($event)"
>
<div class="texture_icon_wrapper">
<img v-bind:texid="texture.id" v-bind:src="texture.source" class="texture_icon" width="48px" alt="" v-if="texture.show_icon" :style="{marginTop: getTextureIconOffset(texture)}" />
<i class="material-icons texture_error" v-bind:title="texture.getErrorMessage()" v-if="texture.error">error_outline</i>
<i class="texture_movie fa fa_big fa-film" title="Animated Texture" v-if="texture.frameCount > 1"></i>
</div>
<div class="texture_description_wrapper">
<div class="texture_name">{{ texture.name }}</div>
<div class="texture_res">{{ getDescription(texture) }}</div>
</div>
<i class="material-icons texture_multi_select_icon" v-if="texture.multi_selected">check</i>
<template v-else>
<i class="material-icons texture_particle_icon" v-if="texture.particle">bubble_chart</i>
<i class="material-icons texture_particle_icon" v-if="texture.use_as_default">star</i>
<i class="material-icons texture_visibility_icon clickable"
v-bind:class="{icon_off: !texture.visible}"
v-if="texture.render_mode == 'layered'"
@click.stop="texture.toggleVisibility()"
@dblclick.stop
>
{{ texture.visible ? 'visibility' : 'visibility_off' }}
</i>
<i class="material-icons texture_save_icon" v-bind:class="{clickable: !texture.saved}" @click.stop="texture.save()">
<template v-if="texture.saved">check_circle</template>
<template v-else>save</template>
</i>
</template>
</li>
`
})
new Panel('textures', {
icon: 'fas.fa-images',
growable: true,
@ -2437,6 +2611,7 @@ Interface.definePanels(function() {
children: [
'import_texture',
'create_texture',
'create_texture_group',
'append_to_template',
]
})
@ -2449,26 +2624,16 @@ Interface.definePanels(function() {
name: 'panel-textures',
data() { return {
textures: Texture.all,
texture_groups: TextureGroup.all,
currentFrame: 0,
}},
components: {'Texture': texture_component},
methods: {
openMenu(event) {
Interface.Panels.textures.menu.show(event)
},
getDescription(texture) {
if (texture.error) {
return texture.getErrorMessage()
} else {
let message = texture.width + ' x ' + texture.height + 'px';
if (!Format.image_editor) {
let uv_size = texture.width / texture.getUVWidth() * 16;
message += ` (${trimFloatNumber(uv_size, 2)}x)`;
}
if (texture.frameCount > 1) {
message += ` - ${texture.currentFrame+1}/${texture.frameCount}`
}
return message;
}
addTextureToGroup(texture_group) {
BarItems.import_texture.click(0, texture_group);
},
slideTimelinePointer(e1) {
let scope = this;
@ -2510,52 +2675,182 @@ Interface.definePanels(function() {
});
return count;
},
getTextureIconOffset(texture) {
if (!texture.currentFrame) return;
let val = texture.currentFrame * -48 * (texture.display_height / texture.width);
return `${val}px`;
unselect(event) {
if (Blockbench.hasFlag('dragging_textures')) return;
unselectTextures();
},
getUngroupedTextures() {
return this.textures.filter(tex => !(tex.group && TextureGroup.all.find(g => g.uuid == tex.group)));
},
dragTextureGroup(texture_group, e1) {
if (e1.button == 1) return;
convertTouchEvent(e1);
let active = false;
let helper;
let timeout;
let last_event = e1;
let texture_group_target_node;
let order = 0;
// scrolling
let list = document.getElementById('texture_list');
let list_offset = $(list).offset();
let scrollInterval = function() {
if (!active) return;
if (mouse_pos.y < list_offset.top) {
list.scrollTop += (mouse_pos.y - list_offset.top) / 7 - 3;
} else if (mouse_pos.y > list_offset.top + list.clientHeight) {
list.scrollTop += (mouse_pos.y - (list_offset.top + list.clientHeight)) / 6 + 3;
}
}
let scrollIntervalID;
function move(e2) {
convertTouchEvent(e2);
let offset = [
e2.clientX - e1.clientX,
e2.clientY - e1.clientY,
]
if (!active) {
let distance = Math.sqrt(Math.pow(offset[0], 2) + Math.pow(offset[1], 2))
if (Blockbench.isTouch) {
if (distance > 20 && timeout) {
clearTimeout(timeout);
timeout = null;
} else {
document.getElementById('texture_list').scrollTop += last_event.clientY - e2.clientY;
}
} else if (distance > 6) {
active = true;
}
}
if (!active) return;
if (e2) e2.preventDefault();
if (open_menu) open_menu.hide();
if (!helper) {
helper = Interface.createElement('div', {class: 'texture_group_drag_helper'}, texture_group.name);
document.body.append(helper);
scrollIntervalID = setInterval(scrollInterval, 1000/60)
}
helper.style.left = `${e2.clientX}px`;
helper.style.top = `${e2.clientY}px`;
// drag
$('.drag_hover').removeClass('drag_hover');
$('.texture_group[order]').attr('order', null);
let target = document.querySelector('#texture_list .texture_group:hover');
if (target) {
target.classList.add('drag_hover');
let offset = e2.clientY - $(target).offset().top;
order = offset > (target.clientHeight/2) ? 1 : -1;
target.setAttribute('order', order.toString());
texture_group_target_node = target;
} else if (document.querySelector('#texture_list:hover')) {
let nodes = document.querySelectorAll('#texture_list > li.texture_group');
if (nodes.length) {
let target = nodes[nodes.length-1];
order = 1;
target.setAttribute('order', '1');
target.classList.add('drag_hover');
texture_group_target_node = target;
}
}
last_event = e2;
}
async function off(e2) {
if (helper) helper.remove();
clearInterval(scrollIntervalID);
removeEventListeners(document, 'mousemove touchmove', move);
removeEventListeners(document, 'mouseup touchend', off);
e2.stopPropagation();
$('.drag_hover').removeClass('drag_hover');
$('.texture_group[order]').attr('order', null);
if (Blockbench.isTouch) clearTimeout(timeout);
if (!active || Menu.open) return;
if (texture_group_target_node) {
let index = TextureGroup.all.length-1;
let texture_group_target = TextureGroup.all.find(tg => tg.uuid == texture_group_target_node.id);
if (texture_group_target) {
index = TextureGroup.all.indexOf(texture_group_target)
let own_index = TextureGroup.all.indexOf(texture_group)
if (own_index == index) return;
if (own_index < index) index--;
if (order == 1) index++;
}
Undo.initEdit({texture_groups: [texture_group]});
TextureGroup.all.remove(texture_group);
TextureGroup.all.splice(index, 0, texture_group);
Undo.finishEdit('Rearrange texture groups');
}
}
if (Blockbench.isTouch) {
timeout = setTimeout(() => {
active = true;
move(e1);
}, 320)
}
addEventListeners(document, 'mousemove touchmove', move, {passive: false});
addEventListeners(document, 'mouseup touchend', off, {passive: false});
}
},
template: `
<div>
<ul id="texture_list" class="list mobile_scrollbar" @contextmenu.stop.prevent="openMenu($event)">
<ul id="texture_list" class="list mobile_scrollbar" @contextmenu.stop.prevent="openMenu($event)" @click.stop="unselect($event)">
<li
v-for="texture in textures"
v-bind:class="{ selected: texture.selected, multi_selected: texture.multi_selected, particle: texture.particle, use_as_default: texture.use_as_default}"
v-bind:texid="texture.uuid"
:key="texture.uuid"
class="texture"
v-on:click.stop="texture.select($event)"
v-on:dblclick="texture.openMenu($event)"
@contextmenu.prevent.stop="texture.showContextMenu($event)"
v-for="texture_group in texture_groups" :key="texture_group.uuid" :id="texture_group.uuid"
class="texture_group"
>
<div class="texture_icon_wrapper">
<img v-bind:texid="texture.id" v-bind:src="texture.source" class="texture_icon" width="48px" alt="" v-if="texture.show_icon" :style="{marginTop: getTextureIconOffset(texture)}" />
<i class="material-icons texture_error" v-bind:title="texture.getErrorMessage()" v-if="texture.error">error_outline</i>
<i class="texture_movie fa fa_big fa-film" title="Animated Texture" v-if="texture.frameCount > 1"></i>
<div class="texture_group_head" :class="{folded: texture_group.folded}"
@dblclick.stop="texture_group.select()"
@click.stop="texture_group.folded = !texture_group.folded"
@contextmenu.prevent.stop="texture_group.showContextMenu($event)"
@mousedown.stop="dragTextureGroup(texture_group, $event)" @touchstart.stop="dragTextureGroup(texture_group, $event)"
>
<i
@click.stop="texture_group.folded = !texture_group.folded"
class="icon-open-state fa"
:class=\'{"fa-angle-right": texture_group.folded, "fa-angle-down": !texture_group.folded}\'
></i>
<label :title="texture_group.name">{{ texture_group.name }}</label>
<ul class="texture_group_mini_icon_list" v-if="texture_group.folded">
<li
v-for="texture in texture_group.getTextures()"
:key="texture.uuid"
class="texture_mini_icon"
:title="texture.name"
>
<img :src="texture.source" class="texture_icon" width="24px" height="24px" alt="" v-if="texture.show_icon" />
</li>
</ul>
<div class="in_list_button" @click.stop="addTextureToGroup(texture_group)" v-if="!texture_group.folded">
<i class="material-icons">add</i>
</div>
</div>
<div class="texture_description_wrapper">
<div class="texture_name">{{ texture.name }}</div>
<div class="texture_res">{{ getDescription(texture) }}</div>
</div>
<i class="material-icons texture_multi_select_icon" v-if="texture.multi_selected">check</i>
<template v-else>
<i class="material-icons texture_particle_icon" v-if="texture.particle">bubble_chart</i>
<i class="material-icons texture_particle_icon" v-if="texture.use_as_default">star</i>
<i class="material-icons texture_visibility_icon clickable"
v-bind:class="{icon_off: !texture.visible}"
v-if="texture.render_mode == 'layered'"
@click.stop="texture.toggleVisibility()"
@dblclick.stop
>
{{ texture.visible ? 'visibility' : 'visibility_off' }}
</i>
<i class="material-icons texture_save_icon" v-bind:class="{clickable: !texture.saved}" @click="texture.save()">
<template v-if="texture.saved">check_circle</template>
<template v-else>save</template>
</i>
</template>
<ul class="texture_group_list" v-if="!texture_group.folded">
<Texture
v-for="texture in texture_group.getTextures()"
:key="texture.uuid"
:texture="texture"
></Texture>
</ul>
</li>
<Texture
v-for="texture in getUngroupedTextures()"
:key="texture.uuid"
:texture="texture"
></Texture>
</ul>
<div id="texture_animation_playback" class="bar" v-show="maxFrameCount()">
<div class="tool_wrapper"></div>

View File

@ -323,6 +323,34 @@ class UndoSystem {
}
}
if (save.texture_groups) {
for (let uuid in save.texture_groups) {
let group;
let data = save.texture_groups[uuid];
if (reference.texture_groups[uuid]) {
group = TextureGroup.all.find(tg => tg.uuid == uuid);
if (group) {
group.extend(data);
}
} else {
group = new TextureGroup(data, uuid).add(false);
}
//order
let index = TextureGroup.all.indexOf(group);
if (index != -1 && index != data.index && typeof data.index == 'number') {
TextureGroup.all.remove(group);
TextureGroup.all.splice(data.index, 0, group);
}
}
for (let uuid in reference.texture_groups) {
if (!save.texture_groups[uuid]) {
let group = TextureGroup.all.find(tg => tg.uuid == uuid);
if (group) {
TextureGroup.all.remove(group);
}
}
}
}
if (save.textures) {
Painter.current = {}
for (var uuid in save.textures) {
@ -591,6 +619,14 @@ UndoSystem.save = class {
})
}
if (aspects.texture_groups) {
this.texture_groups = {};
aspects.texture_groups.forEach(tg => {
let copy = tg.getUndoCopy()
this.texture_groups[tg.uuid] = copy;
})
}
if (aspects.layers) {
this.layers = {};
aspects.layers.forEach(layer => {

View File

@ -26,6 +26,7 @@
"data.texture_mesh": "Texture Mesh",
"data.group": "Group",
"data.texture": "Texture",
"data.texture_group": "Texture Group",
"data.layer": "Layer",
"data.origin": "Pivot",
"data.plugin": "Plugin",
@ -2020,6 +2021,8 @@
"menu.texture.save": "Save",
"menu.texture.properties": "Properties...",
"menu.texture_group.resolve": "Resolve",
"menu.preview.background": "Background",
"menu.preview.background.load": "Load",
"menu.preview.background.clipboard": "Load from Clipboard",