blockbench/js/io/io.js
JannisX11 66f7ec2b44 Add UV rotate tool
Improve array export of JSON compiler
Stop texture animations playing when switching tab
Fix duplicate keybinding from add mesh button
Improve transform space normal calculation
Fix interface issues
Add "Instance" property type
2021-09-20 18:45:02 +02:00

692 lines
20 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'},
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
))
if (existing_tab) {
existing_tab.select();
return;
}
let extension = pathToExtension(file.path);
// Text
for (let id in Codecs) {
let codec = Codecs[id];
if (codec.load_filter && codec.load_filter.type == 'text') {
if (codec.load_filter.extensions.includes(extension) && Condition(codec.load_filter.condition, file.content)) {
codec.load(file.content, file);
return;
}
}
}
// JSON
let model = autoParseJSON(file.content);
for (let id in Codecs) {
let codec = Codecs[id];
if (codec.load_filter && codec.load_filter.type == 'json') {
if (codec.load_filter.extensions.includes(extension) && Condition(codec.load_filter.condition, model)) {
codec.load(model, file);
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) {
return;
}
var dialog = new Dialog({
id: 'sketchfab_uploader',
title: 'dialog.sketchfab_uploader.title',
width: 540,
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'},
description: {label: 'dialog.sketchfab_uploader.description', type: 'textarea'},
tags: {label: 'dialog.sketchfab_uploader.tags', placeholder: 'Tag1 Tag2'},
animations: {label: 'dialog.sketchfab_uploader.animations', value: true, type: 'checkbox', condition: (Format.animation_mode && Animator.animations.length)},
//color: {type: 'color', label: 'dialog.sketchfab_uploader.color'},
draft: {label: 'dialog.sketchfab_uploader.draft', type: 'checkbox'},
// Category
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: formResult.color.toHexString()}))
data.append('private', formResult.private)
data.append('password', formResult.password)
data.append('source', 'blockbench')
settings.sketchfab_token.value = formResult.token
Codecs.gltf.compile({animations: formResult.animations}, (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('message.sketchfab.error', 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 += ',' + (breaks || options.small?'':' ')}
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.export_path && Format.codec && Format.codec.compile) {
Format.codec.write(Format.codec.compile(), Project.export_path)
} else if (Project.save_path) {
Codecs.project.write(Codecs.project.compile(), Project.save_path);
} else if (Format.codec && Format.codec.export) {
Format.codec.export()
} else {
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: () => Cube.all.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()
}
})
})