blockbench/js/io/io.js
JannisX11 5ddc8b267b Fix issue with Sketchfab uploader
Fix several issues related to edit sessions
2022-01-16 13:54:36 +01:00

738 lines
22 KiB
JavaScript

//Import
function setupDragHandlers() {
Blockbench.addDragHandler(
'model',
{extensions: Codec.getAllExtensions},
function(files) {
files.forEach(file => {
loadModelFile(file);
})
}
)
Blockbench.addDragHandler(
'style',
{extensions: ['bbtheme']},
function(files) {
CustomTheme.import(files[0]);
}
)
Blockbench.addDragHandler(
'settings',
{extensions: ['bbsettings']},
function(files) {
Settings.import(files[0]);
}
)
Blockbench.addDragHandler(
'plugin',
{extensions: ['bbplugin', 'js']},
function(files) {
files.forEach(file => {
new Plugin().loadFromFile(file, true);
})
}
)
Blockbench.addDragHandler(
'texture',
{extensions: ['png', 'tga'], propagate: true, readtype: 'image', condition: () => Project && !Dialog.open},
function(files, event) {
var texture_li = $(event.target).parents('li.texture')
if (texture_li.length) {
var tex = Texture.all.findInArray('uuid', texture_li.attr('texid'))
if (tex) {
tex.fromFile(files[0])
TickUpdates.selection = true;
return;
}
}
files.forEach(function(f) {
new Texture().fromFile(f).add().fillParticle()
})
}
)
}
function loadModelFile(file) {
let existing_tab = isApp && ModelProject.all.find(project => (
project.save_path == file.path || project.export_path == file.path
))
let extension = pathToExtension(file.path);
function loadIfCompatible(codec, type, content) {
if (codec.load_filter && codec.load_filter.type == type) {
if (codec.load_filter.extensions.includes(extension) && Condition(codec.load_filter.condition, content)) {
if (existing_tab && !codec.multiple_per_file) {
existing_tab.select();
} else {
codec.load(content, file);
}
return true;
}
}
}
// Text
for (let id in Codecs) {
let success = loadIfCompatible(Codecs[id], 'text', file.content);
if (success) return;
}
// JSON
let model = autoParseJSON(file.content);
for (let id in Codecs) {
let success = loadIfCompatible(Codecs[id], 'json', model);
if (success) return;
}
}
//Extruder
var Extruder = {
drawImage: function(file) {
Extruder.canvas = $('#extrusion_canvas').get(0)
var ctx = Extruder.canvas.getContext('2d')
setProgressBar('extrusion_bar', 0)
$('#scan_tolerance').on('input', function() {
$('#scan_tolerance_label').text($(this).val())
})
showDialog('image_extruder')
Extruder.ext_img = new Image()
Extruder.ext_img.src = isApp ? file.path.replace(/#/g, '%23') : file.content
Extruder.image_file = file
Extruder.ext_img.style.imageRendering = 'pixelated'
Extruder.canvas.style.imageRendering = 'pixelated'
Extruder.ext_img.onload = function() {
let ratio = Extruder.ext_img.naturalWidth / Extruder.ext_img.naturalHeight;
Extruder.canvas.width = 256;
Extruder.canvas.height = 256 / ratio;
ctx.clearRect(0, 0, Extruder.canvas.width, Extruder.canvas.height);
ctx.imageSmoothingEnabled = false;
ctx.drawImage(Extruder.ext_img, 0, 0, Extruder.canvas.width, Extruder.canvas.height);
Extruder.width = Extruder.ext_img.naturalWidth;
Extruder.height = Extruder.ext_img.naturalHeight;
if (Extruder.width > 128) return;
var p = 0
ctx.beginPath();
for (var x = 0; x < Extruder.canvas.width; x += 256 / Extruder.width) {
ctx.moveTo(0.5 + x + p, p);
ctx.lineTo(0.5 + x + p, 256 + p);
}
for (var x = 0; x < Extruder.canvas.height; x += 256 / Extruder.width) {
ctx.moveTo(p, 0.5 + x + p);
ctx.lineTo(256 + p, 0.5 + x + p);
}
ctx.strokeStyle = "black";
ctx.stroke();
}
//Grid
},
startConversion: function() {
var scan_mode = $('select#scan_mode option:selected').attr('id') /*areas, lines, columns, pixels*/
var isNewProject = elements.length === 0;
var pixel_opacity_tolerance = parseInt($('#scan_tolerance').val())
//Undo
Undo.initEdit({elements: selected, outliner: true, textures: []})
var texture = new Texture().fromFile(Extruder.image_file).add(false).fillParticle()
//var ext_x, ext_y;
var ctx = Painter.getCanvas(texture).getContext('2d')
var c = document.createElement('canvas')
var ctx = c.getContext('2d');
c.width = Extruder.ext_img.naturalWidth;
c.height = Extruder.ext_img.naturalHeight;
ctx.drawImage(Extruder.ext_img, 0, 0)
var image_data = ctx.getImageData(0, 0, c.width, c.height).data
var finished_pixels = {}
var cube_nr = 0;
var cube_name = texture.name.split('.')[0]
selected.empty()
//Scale Index
var scale_i = 1;
scale_i = 16 / Extruder.width;
let uv_scale_x = Project.texture_width / Extruder.width;
let uv_scale_y = Project.texture_height / Extruder.height;
function isOpaquePixel(px_x, px_y) {
var opacity = image_data[(px_x + ctx.canvas.width * px_y) * 4 + 3]
return Math.isBetween(px_x, 0, Extruder.width-1)
&& Math.isBetween(px_y, 0, Extruder.height-1)
&& opacity >= pixel_opacity_tolerance;
}
function finishPixel(x, y) {
if (finished_pixels[x] === undefined) {
finished_pixels[x] = {}
}
finished_pixels[x][y] = true
}
function isPixelFinished(x, y) {
return (finished_pixels[x] !== undefined && finished_pixels[x][y] === true)
}
//Scanning
let ext_y = 0;
while (ext_y < Extruder.height) {
let ext_x = 0;
while (ext_x < Extruder.width) {
if (isPixelFinished(ext_x, ext_y) === false && isOpaquePixel(ext_x, ext_y) === true) {
//Search From New Pixel
var loop = true;
var rect = {x: ext_x, y: ext_y, x2: ext_x, y2: ext_y}
var safety_limit = 5000
//Expanding Loop
while (loop === true && safety_limit) {
var y_check, x_check, canExpandX, canExpandY;
//Expand X
if (scan_mode === 'areas' || scan_mode === 'lines') {
y_check = rect.y
x_check = rect.x2 + 1
canExpandX = true
while (y_check <= rect.y2) {
//Check If Row is Free
if (isOpaquePixel(x_check, y_check) === false || isPixelFinished(x_check, y_check) === true) {
canExpandX = false;
}
y_check += 1
}
if (canExpandX === true) {
rect.x2 += 1
}
} else {
canExpandX = false;
}
//Expand Y
if (scan_mode === 'areas' || scan_mode === 'columns') {
x_check = rect.x
y_check = rect.y2 + 1
canExpandY = true
while (x_check <= rect.x2) {
//Check If Row is Free
if (isOpaquePixel(x_check, y_check) === false || isPixelFinished(x_check, y_check) === true) {
canExpandY = false
}
x_check += 1
}
if (canExpandY === true) {
rect.y2 += 1
}
} else {
canExpandY = false;
}
//Conclusion
if (canExpandX === false && canExpandY === false) {
loop = false;
}
safety_limit--;
}
//Draw Rectangle
var draw_x = rect.x
var draw_y = rect.y
while (draw_y <= rect.y2) {
draw_x = rect.x
while (draw_x <= rect.x2) {
finishPixel(draw_x, draw_y)
draw_x++;
}
draw_y++;
}
var current_cube = new Cube({
name: cube_name+'_'+cube_nr,
autouv: 0,
from: [rect.x*scale_i, 0, rect.y*scale_i],
to: [(rect.x2+1)*scale_i, scale_i, (rect.y2+1)*scale_i],
faces: {
up: {uv:[rect.x*uv_scale_x, rect.y*uv_scale_y, (rect.x2+1)*uv_scale_x, (rect.y2+1)*uv_scale_y], texture: texture},
down: {uv:[rect.x*uv_scale_x, (rect.y2+1)*uv_scale_y, (rect.x2+1)*uv_scale_x, rect.y*uv_scale_y], texture: texture},
north: {uv:[(rect.x2+1)*uv_scale_x, rect.y*uv_scale_y, rect.x*uv_scale_x, (rect.y+1)*uv_scale_y], texture: texture},
south: {uv:[rect.x*uv_scale_x, rect.y2*uv_scale_y, (rect.x2+1)*uv_scale_x, (rect.y2+1)*uv_scale_y], texture: texture},
east: {uv:[rect.x2*uv_scale_x, rect.y*uv_scale_y, (rect.x2+1)*uv_scale_x, (rect.y2+1)*uv_scale_y], texture: texture, rotation: 90},
west: {uv:[rect.x*uv_scale_x, rect.y*uv_scale_y, (rect.x+1)*uv_scale_x, (rect.y2+1)*uv_scale_y], texture: texture, rotation: 270},
}
}).init()
selected.push(current_cube)
cube_nr++;
}
ext_x++;
}
ext_y++;
}
var group = new Group(cube_name).init().addTo()
selected.forEach(function(s) {
s.addTo(group).init()
})
Undo.finishEdit('Add extruded texture', {elements: selected, outliner: true, textures: [Texture.all[Texture.all.length-1]]})
hideDialog()
}
}
//Export
function uploadSketchfabModel() {
if (elements.length === 0 || !Format) {
return;
}
let tag_suggestions = ['low-poly', 'pixel-art'];
if (Format.id !== 'free') tag_suggestions.push('minecraft');
if (Format.id === 'skin') tag_suggestions.push('skin');
if (!Mesh.all.length) tag_suggestions.push('voxel');
let clean_project_name = Project.name.toLowerCase().replace(/[_.-]+/g, '-').replace(/[^a-z0-9-]+/, '')
if (Project.name) tag_suggestions.push(clean_project_name);
if (clean_project_name.includes('-')) tag_suggestions.safePush(...clean_project_name.split('-').filter(s => s.length > 2 && s != 'geo').reverse());
let categories = {
"": "-",
"animals-pets": "Animals & Pets",
"architecture": "Architecture",
"art-abstract": "Art & Abstract",
"cars-vehicles": "Cars & Vehicles",
"characters-creatures": "Characters & Creatures",
"cultural-heritage-history": "Cultural Heritage & History",
"electronics-gadgets": "Electronics & Gadgets",
"fashion-style": "Fashion & Style",
"food-drink": "Food & Drink",
"furniture-home": "Furniture & Home",
"music": "Music",
"nature-plants": "Nature & Plants",
"news-politics": "News & Politics",
"people": "People",
"places-travel": "Places & Travel",
"science-technology": "Science & Technology",
"sports-fitness": "Sports & Fitness",
"weapons-military": "Weapons & Military",
}
var dialog = new Dialog({
id: 'sketchfab_uploader',
title: 'dialog.sketchfab_uploader.title',
width: 640,
form: {
token: {label: 'dialog.sketchfab_uploader.token', value: settings.sketchfab_token.value, type: 'password'},
about_token: {type: 'info', text: tl('dialog.sketchfab_uploader.about_token', ['[sketchfab.com/settings/password](https://sketchfab.com/settings/password)'])},
name: {label: 'dialog.sketchfab_uploader.name', value: capitalizeFirstLetter(Project.name.replace(/\..+/, '').replace(/[_.-]/g, ' '))},
description: {label: 'dialog.sketchfab_uploader.description', type: 'textarea'},
category1: {label: 'dialog.sketchfab_uploader.category', type: 'select', options: categories, value: ''},
category2: {label: 'dialog.sketchfab_uploader.category2', type: 'select', options: categories, value: ''},
tags: {label: 'dialog.sketchfab_uploader.tags', placeholder: 'Tag1 Tag2'},
tag_suggestions: {label: 'dialog.sketchfab_uploader.suggested_tags', type: 'buttons', buttons: tag_suggestions, click(index) {
let {tags} = dialog.getFormResult();
let new_tag = tag_suggestions[index];
if (!tags.split(/\s/g).includes(new_tag)) {
tags += ' ' + new_tag;
dialog.setFormValues({tags});
}
}},
animations: {label: 'dialog.sketchfab_uploader.animations', value: true, type: 'checkbox', condition: (Format.animation_mode && Animator.animations.length)},
draft: {label: 'dialog.sketchfab_uploader.draft', type: 'checkbox', value: true},
divider: '_',
private: {label: 'dialog.sketchfab_uploader.private', type: 'checkbox'},
password: {label: 'dialog.sketchfab_uploader.password'},
},
onConfirm: function(formResult) {
if (formResult.token && !formResult.name) {
Blockbench.showQuickMessage('message.sketchfab.name_or_token', 1800)
return;
}
if (!formResult.tags.split(' ').includes('blockbench')) {
formResult.tags += ' blockbench';
}
var data = new FormData()
data.append('token', formResult.token)
data.append('name', formResult.name)
data.append('description', formResult.description)
data.append('tags', formResult.tags)
data.append('isPublished', !formResult.draft)
//data.append('background', JSON.stringify({color: '#00ff00'}))
data.append('private', formResult.private)
data.append('password', formResult.password)
data.append('source', 'blockbench')
if (formResult.category1 || formResult.category2) {
let selected_categories = [];
if (formResult.category1) selected_categories.push(formResult.category1);
if (formResult.category2) selected_categories.push(formResult.category2);
data.append('categories', selected_categories);
}
settings.sketchfab_token.value = formResult.token
Codecs.gltf.compile({animations: formResult.animations}).then(content => {
var blob = new Blob([content], {type: "text/plain;charset=utf-8"});
var file = new File([blob], 'model.gltf')
data.append('modelFile', file)
$.ajax({
url: 'https://api.sketchfab.com/v3/models',
data: data,
cache: false,
contentType: false,
processData: false,
type: 'POST',
success: function(response) {
Blockbench.showMessageBox({
title: tl('message.sketchfab.success'),
message:
`[${formResult.name} on Sketchfab](https://sketchfab.com/models/${response.uid})`, //\n\n&nbsp;\n\n`+
icon: 'icon-sketchfab',
})
},
error: function(response) {
Blockbench.showQuickMessage(tl('message.sketchfab.error') + `Error ${response.status}`, 1500)
console.error(response);
}
})
})
dialog.hide()
}
})
dialog.show()
}
//Json
function compileJSON(object, options) {
if (typeof options !== 'object') options = {}
function newLine(tabs) {
if (options.small === true) {return '';}
var s = '\n'
for (var i = 0; i < tabs; i++) {
s += '\t'
}
return s;
}
function escape(string) {
return string.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n|\r\n/g, '\\n').replace(/\t/g, '\\t')
}
function handleVar(o, tabs) {
var out = ''
if (typeof o === 'string') {
//String
out += '"' + escape(o) + '"'
} else if (typeof o === 'boolean') {
//Boolean
out += (o ? 'true' : 'false')
} else if (o === null || o === Infinity || o === -Infinity) {
//Null
out += 'null'
} else if (typeof o === 'number') {
//Number
o = (Math.round(o*100000)/100000).toString()
out += o
} else if (o instanceof Array) {
//Array
let has_content = false
let has_objects = !!o.find(item => typeof item === 'object');
out += '['
for (var i = 0; i < o.length; i++) {
var compiled = handleVar(o[i], tabs+1)
if (compiled) {
if (has_content) {out += ',' + ((options.small || has_objects) ? '' : ' ')}
if (has_objects) {out += newLine(tabs)}
out += compiled
has_content = true
}
}
if (has_objects) {out += newLine(tabs-1)}
out += ']'
} else if (typeof o === 'object') {
//Object
var breaks = o.constructor.name !== 'oneLiner';
var has_content = false
out += '{'
for (var key in o) {
if (o.hasOwnProperty(key)) {
var compiled = handleVar(o[key], tabs+1)
if (compiled) {
if (has_content) {out += ',' + (breaks || options.small?'':' ')}
if (breaks) {out += newLine(tabs)}
out += '"' + escape(key) + '":' + (options.small === true ? '' : ' ')
out += compiled
has_content = true
}
}
}
if (breaks && has_content) {out += newLine(tabs-1)}
out += '}'
}
return out;
}
return handleVar(object, 1)
}
function autoParseJSON(data, feedback) {
if (data.substr(0, 4) === '<lz>') {
data = LZUTF8.decompress(data.substr(4), {inputEncoding: 'StorageBinaryString'})
}
if (data.charCodeAt(0) === 0xFEFF) {
data = data.substr(1)
}
try {
data = JSON.parse(data)
} catch (err1) {
data = data.replace(/\/\*[^(\*\/)]*\*\/|\/\/.*/g, '')
try {
data = JSON.parse(data)
} catch (err) {
if (feedback === false) return;
function logErrantPart(whole, start, length) {
var line = whole.substr(0, start).match(/\n/gm)
line = line ? line.length+1 : 1
var result = '';
var lines = whole.substr(start, length).split(/\n/gm)
lines.forEach((s, i) => {
result += `#${line+i} ${s}\n`
})
console.log(result.substr(0, result.length-1) + ' <-- HERE')
}
console.error(err)
var length = err.toString().split('at position ')[1]
if (length) {
length = parseInt(length)
var start = limitNumber(length-20, 0, Infinity)
logErrantPart(data, start, 1+length-start)
} else if (err.toString().includes('Unexpected end of JSON input')) {
logErrantPart(data, data.length-10, 10)
}
Blockbench.showMessageBox({
translateKey: 'invalid_file',
icon: 'error',
message: tl('message.invalid_file.message', [err])
})
return;
}
}
return data;
}
BARS.defineActions(function() {
//Import
new Action('open_model', {
icon: 'assessment',
category: 'file',
keybind: new Keybind({key: 'o', ctrl: true}),
click: function () {
var startpath;
if (isApp && recent_projects && recent_projects.length) {
startpath = recent_projects[0].path;
if (typeof startpath == 'string') {
startpath = startpath.split(osfs);
startpath.pop();
startpath = startpath.join(osfs);
}
}
Blockbench.import({
resource_id: 'model',
extensions: Codec.getAllExtensions(),
type: 'Model',
startpath,
multiple: true
}, function(files) {
files.forEach(file => {
loadModelFile(file);
})
})
}
})
new Action('open_from_link', {
icon: 'link',
category: 'file',
click() {
Blockbench.textPrompt('action.open_from_link', '', link => {
if (link.match(/https:\/\/blckbn.ch\//) || link.length == 4) {
let code = link.replace(/[/]+/g, '').substr(-4);
$.getJSON(`https://blckbn.ch/api/models/${code}`, (model) => {
Codecs.project.load(model, {path: ''});
}).fail(error => {
Blockbench.showQuickMessage('message.invalid_link')
})
} else {
$.getJSON(link, (model) => {
Codecs.project.load(model, {path: ''});
}).fail(error => {
Blockbench.showQuickMessage('message.invalid_link')
})
}
}, 'https://blckbn.ch/1234')
}
})
new Action('extrude_texture', {
icon: 'eject',
category: 'file',
condition: _ => Format && !Project.box_uv,
click: function () {
Blockbench.import({
resource_id: 'texture',
extensions: ['png'],
type: 'PNG Texture',
readtype: 'image'
}, function(files) {
if (files.length) {
showDialog('image_extruder')
Extruder.drawImage(files[0])
}
})
}
})
//Export
new Action('export_over', {
icon: 'save',
category: 'file',
keybind: new Keybind({key: 's', ctrl: true}),
click: function () {
if (isApp) {
saveTextures()
if (Format) {
if (Project.save_path) {
Codecs.project.write(Codecs.project.compile(), Project.save_path);
}
if (Project.export_path && Format.codec && Format.codec.compile) {
Format.codec.write(Format.codec.compile(), Project.export_path)
} else if (Format.codec && Format.codec.export && !Project.save_path) {
Format.codec.export()
} else if (!Project.save_path) {
Project.saved = true;
}
}
if (Format.animation_mode && Format.animation_files && Animation.all.length) {
BarItems.save_all_animations.trigger();
}
} else {
saveTextures()
if (Format.codec && Format.codec.export) {
Format.codec.export()
}
}
}
})
if (!isApp) {
new Action('export_asset_archive', {
icon: 'archive',
category: 'file',
condition: _ => Format && Format.codec,
click: function() {
var archive = new JSZip();
var content = Format.codec.compile()
var name = `${Format.codec.fileName()}.${Format.codec.extension}`
archive.file(name, content)
Texture.all.forEach(tex => {
if (tex.mode === 'bitmap') {
archive.file(pathToName(tex.name) + '.png', tex.source.replace('data:image/png;base64,', ''), {base64: true});
}
})
archive.generateAsync({type: 'blob'}).then(content => {
Blockbench.export({
type: 'Zip Archive',
extensions: ['zip'],
name: 'assets',
startpath: Project.export_path,
content: content,
savetype: 'zip'
})
Project.saved = true;
})
}
})
}
new Action('upload_sketchfab', {
icon: 'icon-sketchfab',
category: 'file',
click: function(ev) {
uploadSketchfabModel()
}
})
new Action('share_model', {
icon: 'share',
condition: () => Outliner.elements.length,
click() {
var dialog = new Dialog({
id: 'share_model',
title: 'dialog.share_model.title',
form: {
expire_time: {label: 'dialog.share_model.expire_time', type: 'select', default: '2d', options: {
'10m': tl('dates.minutes', [10]),
'1h': tl('dates.hour', [1]),
'1d': tl('dates.day', [1]),
'2d': tl('dates.days', [2]),
'1w': tl('dates.week', [1]),
'2w': tl('dates.weeks', [2]),
}},
info: {type: 'info', text: 'The model will be stored on the Blockbench servers for the duration specified above. [Learn more](https://blockbench.net/blockbench-model-sharing-service/)'}
},
buttons: ['generic.share', 'dialog.cancel'],
onConfirm: function(formResult) {
let expire_time = formResult.expire_time;
let model = Codecs.project.compile({compressed: false});
$.ajax({
url: 'https://blckbn.ch/api/model',
data: JSON.stringify({ expire_time, model }),
cache: false,
contentType: 'application/json; charset=utf-8',
dataType: 'json',
type: 'POST',
success: function(response) {
let link = `https://blckbn.ch/${response.id}`
let link_dialog = new Dialog({
id: 'share_model_link',
title: 'dialog.share_model.title',
form: {
link: {type: 'text', value: link}
},
buttons: ['action.copy', 'dialog.close'],
onConfirm() {
link_dialog.hide();
if (isApp || navigator.clipboard) {
Clipbench.setText(link);
Blockbench.showQuickMessage('dialog.share_model.copied_to_clipboard');
} else {
Blockbench.showMessageBox({
title: 'dialog.share_model.title',
message: `[${link}](${link})`,
})
}
}
}).show();
},
error: function(response) {
Blockbench.showQuickMessage('dialog.share_model.failed', 1500)
console.error(response);
}
})
dialog.hide()
}
})
dialog.show()
}
})
})