blockbench/js/texturing/textures.js
JannisX11 e7019b642d Improve action control interface
Fix mesh UV mapping preview issue
Automatically import bedrock entity textures as emissive
Use Shift key in UV editor to snap vertices to grid
2021-09-08 11:00:17 +02:00

1670 lines
45 KiB
JavaScript

//Textures
class Texture {
constructor(data, uuid) {
var scope = this;
//Info
for (var key in Texture.properties) {
Texture.properties[key].reset(this);
}
//meta
this.source = ''
this.selected = false
this.show_icon = true
this.error = 0;
this.visible = true;
//Data
this.img = 0;
this.width = 0;
this.height = 0;
this.currentFrame = 0;
this.saved = true;
this.mode = isApp ? 'link' : 'bitmap';
this.uuid = uuid || guid()
if (typeof data === 'object') {
this.extend(data)
}
if (!this.id) {
var i = Texture.all.length;
while (true) {
var c = 0
var duplicates = false;
while (c < Texture.all.length) {
if (Texture.all[c].id == i) {
duplicates = true;
}
c++;
}
if (duplicates === true) {
i++;
} else {
this.id = i.toString();
break;
}
}
}
//Setup Img/Mat
var img = this.img = new Image()
img.src = 'assets/missing.png'
var tex = new THREE.Texture(img)
img.tex = tex;
img.tex.magFilter = THREE.NearestFilter
img.tex.minFilter = THREE.NearestFilter
img.tex.name = this.name;
var vertShader = `
attribute float highlight;
uniform bool SHADE;
varying vec2 vUv;
varying float light;
varying float lift;
float AMBIENT = 0.5;
float XFAC = -0.15;
float ZFAC = 0.05;
void main()
{
if (SHADE) {
vec3 N = normalize( vec3( modelMatrix * vec4(normal, 0.0) ) );
float yLight = (1.0+N.y) * 0.5;
light = yLight * (1.0-AMBIENT) + N.x*N.x * XFAC + N.z*N.z * ZFAC + AMBIENT;
} else {
light = 1.0;
}
if (highlight == 1.0) {
lift = 0.1;
} else {
lift = 0.0;
}
vUv = uv;
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
gl_Position = projectionMatrix * mvPosition;
}`
var fragShader = `
#ifdef GL_ES
precision ${isApp ? 'highp' : 'mediump'} float;
#endif
uniform sampler2D map;
uniform bool SHADE;
uniform bool EMISSIVE;
uniform float BRIGHTNESS;
varying vec2 vUv;
varying float light;
varying float lift;
void main(void)
{
vec4 color = texture2D(map, vUv);
if (color.a < 0.01) discard;
if (EMISSIVE == false) {
gl_FragColor = vec4(lift + color.rgb * light * BRIGHTNESS, color.a);
} else {
float light2 = (light * BRIGHTNESS) + (1.0 - light * BRIGHTNESS) * (1.0 - color.a);
gl_FragColor = vec4(lift + color.rgb * light2, 1.0);
}
}`
var mat = new THREE.ShaderMaterial({
uniforms: {
map: {type: 't', value: tex},
SHADE: {type: 'bool', value: settings.shading.value},
BRIGHTNESS: {type: 'bool', value: settings.brightness.value / 50},
EMISSIVE: {type: 'bool', value: this.render_mode == 'emissive'}
},
vertexShader: vertShader,
fragmentShader: fragShader,
side: Canvas.getRenderSide(),
transparent: true,
});
mat.map = tex;
mat.name = this.name;
Project.materials[this.uuid] = mat;
var size_control = {};
this.img.onload = function() {
if (!this.src) return;
this.tex.needsUpdate = true;
scope.width = img.naturalWidth;
scope.height = img.naturalHeight;
if (scope.isDefault) {
console.log('Successfully loaded '+scope.name+' from default pack')
}
//Width / Animation
if (img.naturalWidth !== img.naturalHeight && Format.id == 'java_block') {
BARS.updateConditions()
}
if (Project.box_uv && Format.single_texture && !scope.error) {
if (!scope.keep_size) {
let pw = Project.texture_width;
let ph = Project.texture_height;
let nw = img.naturalWidth;
let nh = img.naturalHeight;
//texture is unlike project
var unlike = (pw != nw || ph != nh);
//Resolution of this texture has changed
var changed = size_control.old_width && (size_control.old_width != nw || size_control.old_height != nh);
//Resolution could be a multiple of project size
var multi = (
(pw%nw == 0 || nw%pw == 0) &&
(ph%nh == 0 || nh%ph == 0)
)
if (unlike && changed && !multi) {
Blockbench.showMessageBox({
translateKey: 'update_res',
icon: 'photo_size_select_small',
buttons: [tl('message.update_res.update'), tl('dialog.cancel')],
confirm: 0,
cancel: 1
}, function(result) {
if (result === 0) {
setProjectResolution(img.naturalWidth, img.naturalHeight)
if (selected.length) {
UVEditor.loadData()
}
}
})
}
}
delete scope.keep_size;
size_control.old_width = img.naturalWidth
size_control.old_height = img.naturalHeight
}
TextureAnimator.updateButton()
Canvas.updateAllFaces(scope)
if (typeof scope.load_callback === 'function') {
scope.load_callback(scope);
delete scope.load_callback;
}
}
this.img.onerror = function(error) {
if (isApp &&
!scope.isDefault &&
scope.mode !== 'bitmap' &&
scope.fromDefaultPack()
) {
return true;
} else {
scope.loadEmpty()
}
}
}
get frameCount() {
if (Format.animated_textures && this.ratio !== 1 && this.ratio !== (Project.texture_width / Project.texture_height) && 1/this.ratio % 1 === 0) {
return 1/this.ratio
}
}
get display_height() {
return this.height / (this.frameCount || 1);
}
get ratio() {
return this.width / this.height;
}
getErrorMessage() {
switch (this.error) {
case 0: return ''; break;
case 1: return tl('texture.error.file'); break;
//case 1: return tl('texture.error.invalid'); break;
//case 2: return tl('texture.error.ratio'); break;
case 3: return tl('texture.error.parent'); break;
}
}
getUndoCopy(bitmap) {
var copy = {}
for (var key in Texture.properties) {
Texture.properties[key].copy(this, copy)
}
copy.visible = this.visible;
copy.selected = this.selected;
copy.mode = this.mode;
copy.saved = this.saved;
copy.uuid = this.uuid;
copy.old_width = this.old_width;
copy.old_height = this.old_height;
if (bitmap || this.mode === 'bitmap') {
copy.source = this.source
}
return copy
}
extend(data) {
for (var key in Texture.properties) {
Texture.properties[key].merge(this, data)
}
Merge.boolean(this, data, 'visible')
Merge.string(this, data, 'mode', mode => (mode === 'bitmap' || mode === 'link'))
Merge.boolean(this, data, 'saved')
Merge.boolean(this, data, 'keep_size')
if (this.mode === 'bitmap') {
Merge.string(this, data, 'source')
} else if (data.path) {
this.source = this.path.replace(/#/g, '%23') + '?' + tex_version;
}
return this;
}
//Loading
load(cb) {
this.error = 0;
this.show_icon = true;
this.img.src = this.source;
this.load_callback = cb;
return this;
}
fromJavaLink(link, path_array) {
if (typeof link !== 'string' || (link.substr(0, 1) === '#' && !link.includes('/'))) {
this.load();
return this;
}
if (link.substr(0, 22) === 'data:image/png;base64,') {
this.fromDataURL(link)
return this;
}
if (isApp && (link.substr(1, 2) === ':\\' || link.substr(1, 2) === ':/')) {
var path = link.replace(/\\|\//g, osfs).replace(/\?\d+$/, '')
this.fromPath(path)
return this;
}
var can_load = !!path_array.length
var spaces = link.split(':')
if (spaces.length > 1) {
this.namespace = spaces[0]
link = spaces[1]
path_array[path_array.length-1] = this.namespace
}
if (path_array.includes('cit')) {
path_array.pop();
path_array.push(link.replace(/^\.*\//, '').replace(/\//g, osfs)+'.png')
} else {
path_array.push('textures', link.replace(/\//g, osfs)+'.png');
}
var path = path_array.join(osfs);
if (path && can_load) {
this.fromPath(path)
} else {
this.path = path
this.folder = link.replace(/\\/g, '/').split('/')
this.folder = this.folder.splice(0, this.folder.length-1).join('/')
this.name = pathToName(path, true)
this.mode = 'link'
this.saved = true
this.load()
}
return this;
}
fromFile(file) {
if (!file) return this;
if (file.name) this.name = file.name
if (typeof file.content === 'string' && file.content.substr(0, 4) === 'data') {
this.fromDataURL(file.content)
if (!file.path) {
} else if (pathToExtension(file.path) === 'png') {
this.path = file.path
} else if (pathToExtension(file.path) === 'tga') {
this.path = ''
}
} else if (isApp) {
this.fromPath(file.path)
}
this.saved = true
return this;
}
fromPath(path) {
var scope = this;
if (path && pathToExtension(path) === 'tga') {
var targa_loader = new Targa()
targa_loader.open(path, function() {
scope.fromFile({
name: pathToName(path, true),
path: path,
content: targa_loader.getDataURL()
})
})
return this;
}
this.path = path
this.name = pathToName(path, true)
this.mode = 'link'
this.saved = true
if (path.includes('data:image')) {
this.source = path
} else {
this.source = path.replace(/#/g, '%23') + '?' + tex_version
}
this.generateFolder(path)
this.startWatcher()
Painter.current = {}
if (Project.EditSession) {
this.load(() => {
var before = {textures: {}}
before.textures[scope.uuid] = true;
this.edit()
var post = new Undo.save({textures: [this]})
Project.EditSession.sendEdit({
before: before,
post: post,
action: 'loaded_texture',
save_history: false
})
})
} else {
this.load()
}
return this;
}
fromDataURL(data_url) {
this.source = data_url
this.mode = 'bitmap'
this.saved = false;
this.load()
return this;
}
fromDefaultPack() {
if (isApp && settings.default_path && settings.default_path.value) {
if (Format.single_texture) {
var path = Project.BedrockEntityManager.findEntityTexture(Project.geometry_name, 'raw')
if (path) {
this.isDefault = true;
path = settings.default_path.value + osfs + path
if (fs.existsSync(path + '.png')) {
this.fromPath(path + '.png')
delete this.isDefault
return true;
} else if (fs.existsSync(path + '.tga')) {
this.fromPath(path + '.tga')
delete this.isDefault
return true;
}
delete this.isDefault
}
} else if (this.name && this.name.includes('.')) {
var folder = this.folder.replace(/\//g, osfs);
var path = settings.default_path.value + osfs + (folder ? (folder+osfs) : '') + this.name
if (fs.existsSync(path)) {
this.isDefault = true;
this.fromPath(path)
return true;
}
}
}
}
loadEmpty(error_id) {
this.img.src = 'assets/missing.png'
this.error = error_id||1;
this.show_icon = false;
return this;
}
updateSource(dataUrl) {
// Update the source, only used when source is secure + base64
if (!dataUrl) dataUrl = this.source;
this.source = dataUrl;
this.img.src = dataUrl;
this.updateMaterial();
if (this == UVEditor.texture) {
UVEditor.img.src = dataUrl;
};
if (open_dialog == 'UVEditor') {
for (var key in UVEditor.editors) {
var editor = UVEditor.editors[key];
if (this == editor.texture) {
editor.img.src = dataUrl;
}
}
}
return this;
}
updateMaterial() {
let mat = this.getMaterial();
mat.name = this.name;
mat.map.name = this.name;
mat.map.image.src = this.source;
mat.map.needsUpdate = true;
return this;
}
reopen(force) {
var scope = this;
this.stopWatcher()
function _replace() {
Blockbench.import({
resource_id: 'texture',
extensions: ['png', 'tga'],
type: 'PNG Texture',
readtype: 'image',
startpath: scope.path
}, function(files) {
scope.fromFile(files[0])
})
Painter.current = {}
UVEditor.loadData();
Blockbench.dispatchEvent( 'change_texture_path', {texture: scope} )
}
if (scope.saved || force) {
_replace()
} else {
Blockbench.showMessageBox({
translateKey: 'unsaved_texture',
icon: 'warning',
buttons: [tl('dialog.continue'), tl('dialog.cancel')],
confirm: 0,
cancel: 1
}, function(result) {
if (result === 0) {
_replace()
}
})
}
}
refresh(single) {
if (this.mode === 'bitmap') {
return false;
}
if (single) {
tex_version++;
}
this.source = this.source.replace(/\?\d+$/, '?' + tex_version)
this.load();
this.updateMaterial()
TickUpdates.UVEditor = true;
TickUpdates.texture_list = true;
}
reloadTexture() {
this.refresh(true)
}
startWatcher() {
if (this.mode !== 'link' || !isApp || !this.path.match(/\.[a-zA-Z]+$/) || !fs.existsSync(this.path)) {
return;
}
var scope = this;
this.stopWatcher();
let timeout;
this.watcher = fs.watch(scope.path, (eventType) => {
if (eventType == 'change') {
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
scope.reloadTexture();
}, 60)
}
})
}
stopWatcher() {
if (isApp && this.watcher) {
this.watcher.close()
}
return this;
}
generateFolder(path) {
if (path.includes(osfs+'optifine'+osfs+'cit'+osfs)) {
if (Project.export_path) {
let model_arr = Project.export_path.split(osfs).slice(0, -1);
let tex_arr = path.split(osfs).slice(0, -1);
let index = 0;
tex_arr.find((dir, i) => {
if (dir != model_arr[i]) return true;
index++;
})
this.folder = ['.', ...tex_arr.slice(index)].join('/');
} else {
this.folder = '.';
}
} else if (path.includes(osfs+'textures'+osfs)) {
var arr = path.split(osfs+'textures'+osfs);
var arr1 = arr[0].split(osfs);
this.namespace = arr1[arr1.length-1];
var arr2 = arr[arr.length-1].split(osfs);
arr2.pop();
this.folder = arr2.join('/');
} else {
var arr = path.split(osfs)
this.folder = arr[arr.length-2]
if (Format.id === 'java_block' && isApp && settings.dialog_loose_texture.value) {
Blockbench.showMessageBox({
translateKey: 'loose_texture',
icon: 'folder_open',
buttons: [tl('message.loose_texture.change'), tl('dialog.ok'), tl('dialog.dontshowagain')],
confirm: 0,
cancel: 1
}, result => {
if (result === 0) {
this.reopen()
}
if (result === 2) {
settings.dialog_loose_texture.set(false);
}
})
}
}
return this;
}
getMaterial() {
return Project.materials[this.uuid]
}
//Management
select(event) {
Texture.all.forEach(s => {
if (s.selected) s.selected = false;
})
if (event) {
Prop.active_panel = 'textures'
}
this.selected = true
Texture.selected = this;
this.scrollTo();
if (this.render_mode == 'layered') {
Canvas.updatePaintingGrid()
updateSelection()
} else if (Format.single_texture && Texture.all.length > 1) {
Canvas.updateAllFaces()
TickUpdates.selection = true;
}
return this;
}
add(undo) {
var scope = this;
if (isApp && this.path && Project.textures.length) {
for (var tex of Project.textures) {
if (tex.path === scope.path) return tex;
}
}
if (Texture.all.find(t => t.render_mode == 'layered')) {
this.render_mode = 'layered';
}
if (undo) {
Undo.initEdit({textures: []})
}
if (!Project.textures.includes(this)) {
Project.textures.push(this)
}
Blockbench.dispatchEvent( 'add_texture', {texture: this})
loadTextureDraggable()
if (Format.single_texture && Cube.all.length) {
Canvas.updateAllFaces()
if (selected.length) {
UVEditor.loadData()
}
}
TickUpdates.selection = true;
if (undo) {
Undo.finishEdit('Add texture', {textures: [this]})
}
return this;
}
remove(no_update) {
if (!no_update) {
Undo.initEdit({textures: [this]})
}
this.stopWatcher()
if (Texture.selected == this) {
Texture.selected = undefined;
}
Project.textures.splice(Texture.all.indexOf(this), 1)
delete Project.materials[this.uuid];
if (!no_update) {
Canvas.updateAllFaces()
TextureAnimator.updateButton()
hideDialog()
if (UVEditor.texture == this) {
UVEditor.displayTexture();
}
BARS.updateConditions()
Undo.finishEdit('Remove texture', {textures: []})
}
}
toggleVisibility() {
if (this.render_mode !== 'layered') {
this.visible = true;
return this;
}
this.visible = !this.visible;
let c = 0;
Texture.all.forEach(tex => {
if (tex.visible) {
c++;
if (c >= 3 && tex !== this) {
tex.visible = false;
}
}
})
Canvas.updateLayeredTextures();
}
//Use
enableParticle() {
Texture.all.forEach(function(s) {
s.particle = false;
})
if (Format.id == 'java_block') {
this.particle = true
}
return this;
}
fillParticle() {
var particle_tex = false
Texture.all.forEach(function(t) {
if (t.particle) {
particle_tex = t
}
})
if (!particle_tex) {
this.enableParticle()
}
return this;
}
apply(all) {
let affected = Outliner.selected.filter(el => el.faces);
if (!affected.length) return;
var scope = this;
Undo.initEdit({elements: affected})
affected.forEach(function(obj) {
for (var face in obj.faces) {
if (all || Project.box_uv || UVEditor.vue.selected_faces.includes(face)) {
var f = obj.faces[face]
if (all !== 'blank' || (f.texture !== null && !f.getTexture())) {
f.texture = scope.uuid
}
}
}
})
Canvas.updateView({elements: affected, element_aspects: {faces: true}})
UVEditor.loadData()
Undo.finishEdit('Apply texture')
return this;
}
//Interface
openFolder() {
if (!isApp || !this.path) return this;
if (!fs.existsSync(this.path)) {
Blockbench.showQuickMessage('texture.error.file')
return this;
}
shell.showItemInFolder(this.path)
return this;
}
openEditor() {
var scope = this;
if (!settings.image_editor.value) {
changeImageEditor(scope)
} else {
if (fs.existsSync(settings.image_editor.value)) {
if (Blockbench.platform == 'darwin') {
require('child_process').exec(`open '${this.path}' -a '${settings.image_editor.value}'`)
} else {
require('child_process').spawn(settings.image_editor.value, [this.path])
}
} else {
electron.dialog.showMessageBoxSync(currentwindow, {
type: 'info',
noLink: true,
title: tl('message.image_editor_missing.title'),
message: tl('message.image_editor_missing.message'),
detail: tl('message.image_editor_missing.detail')
})
selectImageEditorFile(scope)
}
}
return this;
}
showContextMenu(event) {
var scope = this;
scope.select()
this.menu.open(event, scope)
}
openMenu() {
var scope = this
scope.select()
let title = `${scope.name} (${scope.width} x ${scope.height})`;
var path = '';
if (scope.path) {
var arr = scope.path.split(osfs)
arr.splice(-1)
path = arr.join('<span class="slash">/</span>') + '<span class="slash">/</span><span class="accent_color">' + scope.name + '</span>'
}
var dialog = new Dialog({
id: 'texture_edit',
title,
lines: [
`<div style="height: 140px;">
<div id="texture_menu_thumbnail">${scope.img.outerHTML}</div>
<p class="multiline_text" id="te_path">${settings.streamer_mode.value ? `[${tl('generic.redacted')}]` : path}</p>
</div>`
],
form: {
name: {label: 'generic.name', value: scope.name},
variable: {label: 'dialog.texture.variable', value: scope.id, condition: () => Format.id === 'java_block'},
folder: {label: 'dialog.texture.folder', value: scope.folder, condition: () => Format.id === 'java_block'},
namespace: {label: 'dialog.texture.namespace', value: scope.namespace, condition: () => Format.id === 'java_block'},
},
onConfirm: function(results) {
dialog.hide();
if (
(scope.name === results.name) &&
(results.variable === undefined || scope.id === results.variable) &&
(results.folder === undefined || scope.folder === results.folder) &&
(results.namespace === undefined || scope.namespace === results.namespace)
) {
return;
}
Undo.initEdit({textures: [scope], selected_texture: true})
scope.name = results.name;
if (results.variable !== undefined) scope.id = results.variable;
if (results.folder !== undefined) scope.folder = results.folder;
if (results.namespace !== undefined) scope.namespace = results.namespace;
Undo.finishEdit('Edit texture metadata')
}
}).show()
}
resizeDialog() {
let scope = this;
let dialog = new Dialog({
id: 'resize_texture',
title: 'menu.texture.resize',
form: {
size: {
label: 'dialog.project.texture_size',
type: 'vector',
dimensions: 2,
value: [this.width, this.height],
min: 1
},
fill: {label: 'dialog.resize_texture.fill', type: 'select', default: 'transparent', options: {
transparent: 'dialog.resize_texture.fill.transparent',
color: 'dialog.resize_texture.fill.color',
repeat: 'dialog.resize_texture.fill.repeat',
stretch: 'dialog.resize_texture.fill.stretch'
}}
},
onConfirm: function(formResult) {
let old_width = scope.width;
let old_height = scope.height;
scope.edit((canvas) => {
let new_canvas = document.createElement('canvas')
new_canvas.width = formResult.size[0];
new_canvas.height = formResult.size[1];
let new_ctx = new_canvas.getContext('2d');
new_ctx.imageSmoothingEnabled = false;
switch (formResult.fill) {
case 'transparent':
new_ctx.drawImage(canvas, 0, 0, scope.width, scope.height);
break;
case 'color':
new_ctx.fillStyle = ColorPanel.get();
new_ctx.fillRect(0, 0, formResult.size[0], formResult.size[1])
new_ctx.clearRect(0, 0, scope.width, scope.height)
new_ctx.drawImage(canvas, 0, 0, scope.width, scope.height);
break;
case 'repeat':
for (var x = 0; x < formResult.size[0]; x += scope.width) {
for (var y = 0; y < formResult.size[1]; y += scope.height) {
new_ctx.drawImage(canvas, x, y, scope.width, scope.height);
}
}
break;
case 'stretch':
new_ctx.drawImage(canvas, 0, 0, formResult.size[0], formResult.size[1]);
break;
}
if (Painter.current && Painter.current.canvas) {
delete Painter.current.canvas;
}
scope.keep_size = true;
if (formResult.fill !== 'stretch' && (Format.single_texture || Texture.all.length == 1)) {
Undo.current_save.uv_mode = {
box_uv: Project.box_uv,
width: Project.texture_width,
height: Project.texture_height
}
Undo.current_save.aspects.uv_mode = true;
Project.texture_width = Project.texture_width * (formResult.size[0] / old_width);
Project.texture_height = Project.texture_height * (formResult.size[1] / old_height);
Canvas.updateAllUVs()
}
return new_canvas
})
setTimeout(updateSelection, 100);
dialog.hide()
}
})
dialog.show()
return this;
}
scrollTo() {
var el = $(`#texture_list > li[texid=${this.uuid}]`)
if (el.length === 0 || Texture.all.length < 2) return;
var outliner_pos = $('#texture_list').offset().top
var el_pos = el.offset().top
if (el_pos > outliner_pos && el_pos + 48 < $('#texture_list').height() + outliner_pos) return;
var multiple = el_pos > outliner_pos ? 0.5 : 0.2
var scroll_amount = el_pos + $('#texture_list').scrollTop() - outliner_pos - 20
scroll_amount -= $('#texture_list').height()*multiple - 15
$('#texture_list').animate({
scrollTop: scroll_amount
}, 200);
}
//Export
javaTextureLink() {
var link = this.name.replace(/\.png$/, '')
if (this.folder) {
link = this.folder + '/' + link
}
if (this.namespace && this.namespace !== 'minecraft') {
link = this.namespace + ':' + link
}
return link;
}
save(as) {
var scope = this;
if (scope.saved && !as) {
return this;
}
if (isApp) {
//overwrite path
if (scope.mode === 'link') {
var image = nativeImage.createFromPath(scope.source.replace(/\?\d+$/, '')).toPNG()
} else {
var image = nativeImage.createFromDataURL(scope.source).toPNG()
}
tex_version++;
if (!as && this.path && fs.existsSync(this.path)) {
fs.writeFile(this.path, image, function (err) {
scope.fromPath(scope.path)
})
} else {
var find_path;
if (Format.bone_rig && Project.geometry_name) {
find_path = Project.BedrockEntityManager.findEntityTexture(Project.geometry_name, true)
}
if (!find_path && Project.export_path) {
var arr = Project.export_path.split(osfs);
var index = arr.lastIndexOf('models');
if (index > 1) arr.splice(index, 256, 'textures')
if (scope.folder) arr = arr.concat(scope.folder.split('/'));
arr.push(scope.name)
find_path = arr.join(osfs)
}
Blockbench.export({
resource_id: 'texture',
type: 'PNG Texture',
extensions: ['png'],
name: scope.name,
content: image,
startpath: find_path,
savetype: 'image'
}, function(path) {
scope.fromPath(path)
})
}
} else {
//Download
Blockbench.export({
type: 'PNG Texture',
extensions: ['png'],
name: scope.name,
content: scope.source,
savetype: 'image'
}, function() {
scope.saved = true;
})
}
return this;
}
getBase64() {
var scope = this;
if (isApp && scope.mode === 'link') {
var canvas = document.createElement('canvas')
canvas.width = scope.img.naturalWidth;
canvas.height = scope.img.naturalHeight;
var ctx = canvas.getContext('2d');
ctx.drawImage(scope.img, 0, 0)
var dataUrl = canvas.toDataURL('image/png')
} else {
var dataUrl = scope.source
}
return dataUrl.replace('data:image/png;base64,', '')
}
edit(cb, options) {
var scope = this;
if (!options) options = false;
if (cb) {
Painter.edit(scope, cb, options);
} else if (scope.mode === 'link') {
scope.source = 'data:image/png;base64,' + scope.getBase64();
scope.mode = 'bitmap';
}
scope.saved = false;
}
}
Texture.prototype.menu = new Menu([
{
icon: 'crop_original',
name: 'menu.texture.face',
condition() {return !Project.single_texture && Outliner.selected.length > 0},
click: function(texture) {texture.apply()}
},
{
icon: 'texture',
name: 'menu.texture.blank',
condition() {return !Project.single_texture && Outliner.selected.length > 0},
click: function(texture) {texture.apply('blank')}
},
{
icon: 'fa-cube',
name: 'menu.texture.elements',
condition() {return !Project.single_texture && Outliner.selected.length > 0},
click: function(texture) {texture.apply(true)}
},
{
icon: 'bubble_chart',
name: 'menu.texture.particle',
condition: function() {return Format.id == 'java_block'},
click: function(texture) {
if (texture.particle) {
texture.particle = false
} else {
texture.enableParticle()
}
}
},
'_',
{
icon: 'list',
name: 'menu.texture.render_mode',
children(texture) {
function setViewMode(mode) {
let update_layered = (mode == 'layered' || texture.render_mode == 'layered');
let update_emissive = (mode == 'emissive' || texture.render_mode == 'emissive');
let changed_textures = update_layered ? Texture.all : [texture];
Undo.initEdit({textures: changed_textures});
changed_textures.forEach(t => {
t.render_mode = mode;
});
if (update_layered) {
Texture.all.forEach((tex, i) => {
tex.visible = i < 3
})
Interface.Panels.textures.inside_vue.$forceUpdate()
Canvas.updateLayeredTextures();
}
if (update_emissive) {
texture.getMaterial().uniforms.EMISSIVE.value = mode == 'emissive';
}
Undo.finishEdit('change texture view mode');
}
return [
{name: 'menu.texture.render_mode.normal', icon: texture.render_mode == 'normal' ? 'radio_button_checked' : 'radio_button_unchecked', click() {setViewMode('normal')}},
{name: 'menu.texture.render_mode.emissive', icon: texture.render_mode == 'emissive' ? 'radio_button_checked' : 'radio_button_unchecked', click() {setViewMode('emissive')}},
{name: 'menu.texture.render_mode.layered', icon: texture.render_mode == 'layered' ? 'radio_button_checked' : 'radio_button_unchecked', click() {setViewMode('layered')}, condition: () => Format.single_texture},
]
}
},
{
icon: 'photo_size_select_large',
name: 'menu.texture.resize',
click(texture) {texture.resizeDialog()}
},
'_',
{
icon: 'edit',
name: 'menu.texture.edit',
condition: function(texture) {return texture.mode == 'link'},
click: function(texture) { texture.openEditor()}
},
{
icon: 'folder',
name: 'menu.texture.folder',
condition: function(texture) {return isApp && texture.path},
click: function(texture) {texture.openFolder()}
},
{
icon: 'save',
name: 'menu.texture.save',
condition: function(texture) {return !texture.saved && texture.path},
click: function(texture) {texture.save()}
},
{
icon: 'file_download',
name: 'menu.texture.export',
click: function(texture) {texture.save(true)}
},
'_',
{
icon: 'refresh',
name: 'menu.texture.refresh',
condition: function(texture) {return texture.mode == 'link'},
click: function(texture) {texture.reloadTexture()}
},
{
icon: 'file_upload',
name: 'menu.texture.change',
click: function(texture) { texture.reopen()}
},
{
icon: 'delete',
name: 'generic.delete',
click: function(texture) {
Undo.initEdit({textures: [texture], selected_texture: true, bitmap: true})
texture.remove()
Undo.finishEdit('Delete texture', {textures: [], selected_texture: true, bitmap: true})
}},
'_',
{
icon: 'list',
name: 'menu.texture.properties',
click: function(texture) { texture.openMenu()}
}
])
Texture.getDefault = function() {
if (Texture.selected && Texture.all.includes(Texture.selected)) {
return Texture.selected;
} else if (Texture.selected) {
Texture.selected = undefined;
}
if (Texture.all.length > 1 && Texture.all.find(t => t.render_mode == 'layered')) {
var i = 0;
for (var i = Texture.all.length-1; i >= 0; i--) {
if (Texture.all[i].visible) {
return Texture.all[i]
}
}
}
return Texture.all[0]
}
new Property(Texture, 'string', 'path')
new Property(Texture, 'string', 'name')
new Property(Texture, 'string', 'folder')
new Property(Texture, 'string', 'namespace')
new Property(Texture, 'string', 'id')
new Property(Texture, 'boolean', 'particle')
new Property(Texture, 'string', 'render_mode', {default: 'normal'})
Object.defineProperty(Texture, 'all', {
get() {
return Project.textures || [];
},
set(arr) {
Project.textures.replace(arr);
}
})
Object.defineProperty(Texture, 'selected', {
get() {
return Project.selected_texture
},
set(texture) {
Project.selected_texture = texture;
}
})
function saveTextures(lazy = false) {
Texture.all.forEach(function(tex) {
if (!tex.saved) {
if (lazy && isApp && (!tex.path || !fs.existsSync(tex.path))) return;
tex.save()
}
})
}
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 (tex && elements) {
Undo.initEdit({elements})
elements.forEach(element => {
element.applyTexture(tex, data.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) {
var 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;
var target = OutlinerNode.uuids[uuid];
var array = [];
if (target.type === 'group') {
target.forEachChild(function(cube) {
array.push(cube)
}, [Cube, Mesh])
} else {
array = selected.includes(target) ? selected : [target];
}
Undo.initEdit({elements: array, uv_only: true})
array.forEach(function(cube) {
for (var face in cube.faces) {
cube.faces[face].texture = tex.uuid;
}
})
Undo.finishEdit('Drop texture')
UVEditor.loadData()
Canvas.updateAllFaces()
} else if ($('#uv_viewport:hover').length) {
UVEditor.applyTexture(tex);
}
}, 10)
}
})
}, 42)
})
}
function unselectTextures() {
Texture.all.forEach(function(s) {
s.selected = false;
})
Texture.selected = undefined;
Canvas.updateLayeredTextures()
}
function getTexturesById(id) {
if (id === undefined) return;
id = id.replace('#', '');
return $.grep(Texture.all, function(e) {return e.id == id});
}
Clipbench.setTexture = function(texture) {
//Sets the raw image of the texture
if (!isApp) return;
if (texture.mode === 'bitmap') {
var img = nativeImage.createFromDataURL(texture.source)
} else {
var img = nativeImage.createFromPath(texture.source.split('?')[0])
}
clipboard.writeImage(img)
}
Clipbench.pasteTextures = function() {
function loadImage(dataUrl) {
var texture = new Texture({name: 'pasted', folder: 'block' }).fromDataURL(dataUrl).fillParticle().add(true)
setTimeout(function() {
texture.openMenu()
}, 40)
}
if (isApp) {
var image = clipboard.readImage().toDataURL();
loadImage(image);
} else {
navigator.clipboard.read().then(content => {
if (content && content[0] && content[0].types.includes('image/png')) {
content[0].getType('image/png').then(blob => {
let url = URL.createObjectURL(blob);
loadImage(url);
})
}
}).catch(() => {})
}
}
TextureAnimator = {
isPlaying: false,
interval: false,
start() {
clearInterval(TextureAnimator.interval)
TextureAnimator.isPlaying = true
TextureAnimator.updateButton()
TextureAnimator.interval = setInterval(TextureAnimator.nextFrame, 1000/settings.texture_fps.value)
},
stop() {
TextureAnimator.isPlaying = false
clearInterval(TextureAnimator.interval)
TextureAnimator.updateButton()
},
toggle() {
if (TextureAnimator.isPlaying) {
TextureAnimator.stop()
} else {
TextureAnimator.start()
}
},
updateSpeed() {
if (TextureAnimator.isPlaying) {
TextureAnimator.stop()
TextureAnimator.start()
}
},
nextFrame() {
var animated_textures = []
Texture.all.forEach(tex => {
if (tex.frameCount > 1) {
if (tex.currentFrame >= tex.frameCount-1) {
tex.currentFrame = 0
} else {
tex.currentFrame++;
}
animated_textures.push(tex)
}
})
TextureAnimator.update(animated_textures);
},
update(animated_textures) {
let maxFrame = 0;
animated_textures.forEach(tex => {
$(`.texture[texid="${tex.uuid}"]`).find('img').css('margin-top', (tex.currentFrame*-48)+'px');
maxFrame = Math.max(maxFrame, tex.currentFrame);
})
if (animated_textures.includes(UVEditor.texture)) {
UVEditor.img.style.objectPosition = `0 -${UVEditor.texture.currentFrame * UVEditor.inner_height}px`;
}
Cube.all.forEach(cube => {
var update = false
for (var face in cube.faces) {
update = update || animated_textures.includes(cube.faces[face].getTexture());
}
if (update) {
Canvas.updateUV(cube, true)
}
})
BarItems.animated_texture_frame.update();
Interface.Panels.textures.inside_vue._data.currentFrame = maxFrame;
},
reset() {
TextureAnimator.stop();
Texture.all.forEach(function(tex, i) {
if (tex.frameCount) {
tex.currentFrame = 0
$($('.texture').get(i)).find('img').css('margin-top', '0')
}
})
UVEditor.img.style.objectPosition = '';
while (i < elements.length) {
Canvas.updateUV(elements[i], true)
i++;
}
},
updateButton() {
BarItems.animated_textures.setIcon( TextureAnimator.isPlaying ? 'pause' : 'play_arrow' )
}
}
BARS.defineActions(function() {
new Action('import_texture', {
icon: 'library_add',
category: 'textures',
keybind: new Keybind({key: 't', ctrl: true}),
click: function () {
var start_path;
if (!isApp) {} else
if (Texture.all.length > 0) {
var 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)
arr.splice(-3)
arr.push('textures')
start_path = arr.join(osfs)
}
Blockbench.import({
resource_id: 'texture',
readtype: 'image',
type: 'PNG Texture',
extensions: ['png', 'tga'],
multiple: true,
startpath: start_path
}, function(results) {
var new_textures = []
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)
})
Undo.finishEdit('Add texture')
})
}
})
new Action('create_texture', {
icon: 'icon-create_bitmap',
category: 'textures',
keybind: new Keybind({key: 't', ctrl: true, shift: true}),
click: function () {
TextureGenerator.addBitmapDialog()
}
})
new Action('save_textures', {
icon: 'save',
category: 'textures',
click: function () {saveTextures()}
})
new Action('change_textures_folder', {
icon: 'fas.fa-hdd',
category: 'textures',
condition: () => Texture.all.length > 0,
click: function () {
var path = undefined;
var i = 0;
while (i < Texture.all.length && path === undefined) {
if (typeof Texture.all[i].path == 'string' && Texture.all[i].path.length > 8) {
path = Texture.all[i].path
}
i++;
}
if (!path) {return;}
var path = path.split(osfs)
path.splice(-1)
path = path.join(osfs)
let dirPath = Blockbench.pickDirectory({
resource_id: 'texture',
startpath: path,
})
if (dirPath && dirPath.length) {
var new_path = dirPath[0]
Undo.initEdit({textures: Texture.all})
Texture.all.forEach(function(t) {
if (typeof t.path === 'string' && t.path.includes(path)) {
t.fromPath(t.path.replace(path, new_path))
}
})
Undo.finishEdit('Change textures folder')
}
}
})
function textureAnimationCondition() {
return Format.animated_textures && Texture.all.find(tex => tex.frameCount > 1);
}
new Action('animated_textures', {
icon: 'play_arrow',
category: 'textures',
condition: textureAnimationCondition,
click: function () {
TextureAnimator.toggle()
}
})
function getSliderTexture() {
return [Texture.getDefault(), ...Texture.all].find(tex => tex && tex.frameCount > 1);
}
new NumSlider('animated_texture_frame', {
category: 'textures',
condition: textureAnimationCondition,
getInterval(event) {
return 1;
},
get: function() {
let tex = getSliderTexture()
return tex ? tex.currentFrame+1 : 0;
},
change: function(modify) {
let tex = getSliderTexture()
if (tex) {
tex.currentFrame = Math.clamp(modify(tex.currentFrame+1), 1, tex.frameCount) - 1;
TextureAnimator.update([tex]);
}
}
})
})
Interface.definePanels(function() {
Interface.Panels.textures = new Panel({
id: 'textures',
icon: 'fas.fa-images',
growable: true,
condition: {modes: ['edit', 'paint']},
toolbars: {
head: Toolbars.texturelist
},
onResize() {
this.inside_vue._data.currentFrame += 1;
this.inside_vue._data.currentFrame -= 1;
},
component: {
name: 'panel-textures',
data() { return {
textures: Texture.all,
currentFrame: 0,
}},
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 (texture.frameCount > 1) {
message += ` - ${texture.currentFrame+1}/${texture.frameCount}`
}
return message;
}
},
slideTimelinePointer(e1) {
let scope = this;
if (!this.$refs.timeline) return;
let timeline_offset = $(this.$refs.timeline).offset().left + 8;
let timeline_width = this.$refs.timeline.clientWidth - 8;
let maxFrameCount = this.maxFrameCount;
function slide(e2) {
convertTouchEvent(e2);
let pos = e2.clientX - timeline_offset;
scope.currentFrame = Math.clamp(Math.round((pos / timeline_width) * maxFrameCount), 0, maxFrameCount-1);
let textures = Texture.all.filter(tex => tex.frameCount > 1);
Texture.all.forEach(tex => {
tex.currentFrame = scope.currentFrame % tex.frameCount;
})
TextureAnimator.update(textures);
}
function off(e3) {
removeEventListeners(document, 'mousemove touchmove', slide);
removeEventListeners(document, 'mouseup touchend', off);
}
addEventListeners(document, 'mousemove touchmove', slide);
addEventListeners(document, 'mouseup touchend', off);
slide(e1);
},
getPlayheadPos() {
if (!this.$refs.timeline) return 0;
let width = this.$refs.timeline.clientWidth - 8;
return Math.clamp((this.currentFrame / this.maxFrameCount) * width, 0, width);
}
},
computed: {
maxFrameCount() {
let count = 0;
this.textures.forEach(tex => {
if (tex.frameCount > count) count = tex.frameCount;
});
return count;
}
},
template: `
<div>
<div class="toolbar_wrapper texturelist"></div>
<ul id="texture_list" class="list mobile_scrollbar" @contextmenu.stop.prevent="openMenu($event)">
<li
v-for="texture in textures"
v-bind:class="{ selected: texture.selected, particle: texture.particle}"
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)"
>
<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" />
<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_particle_icon" v-if="texture.particle">bubble_chart</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>
</li>
</ul>
<div id="texture_animation_playback" class="bar" v-show="maxFrameCount">
<div class="tool_wrapper"></div>
<div id="texture_animation_timeline" ref="timeline" @mousedown="slideTimelinePointer">
<div class="texture_animation_frame" v-for="i in maxFrameCount"></div>
<div id="animated_texture_playhead" :style="{left: getPlayheadPos() + 'px'}"></div>
</div>
</div>
</div>
`,
mounted() {
BarItems.animated_textures.toElement('#texture_animation_playback .tool_wrapper')
BarItems.animated_texture_frame.setWidth(52).toElement('#texture_animation_playback .tool_wrapper')
}
},
menu: new Menu([
'paste',
'import_texture',
'create_texture',
'reload_textures',
'change_textures_folder',
'save_textures'
])
})
})