blockbench/js/texturing/texture_generator.js
2023-02-16 21:37:47 +01:00

1731 lines
58 KiB
JavaScript

const TextureGenerator = {
background_color: new ColorPicker({
id: 'background_color',
name: 'data.color',
private: true,
}),
face_data: {
up: {c1: '#b4d4e1', c2: '#ecf8fd', place: t => {return {x: t.posx+t.z, y: t.posy, w: t.x, h: t.z}}},
down: {c1: '#536174', c2: '#6e788c', place: t => {return {x: t.posx+t.z+t.x, y: t.posy, w: t.x, h: t.z}}},
east: {c1: '#43e88d', c2: '#7BFFA3', place: t => {return {x: t.posx, y: t.posy+t.z, w: t.z, h: t.y}}},
north: {c1: '#5bbcf4', c2: '#7BD4FF', place: t => {return {x: t.posx+t.z, y: t.posy+t.z, w: t.x, h: t.y}}},
west: {c1: '#f48686', c2: '#FFA7A4', place: t => {return {x: t.posx+t.z+t.x, y: t.posy+t.z, w: t.z, h: t.y}}},
south: {c1: '#f8dd72', c2: '#FFF899', place: t => {return {x: t.posx+t.z+t.x+t.z,y: t.posy+t.z, w: t.x, h: t.y}}},
},
addBitmapDialog() {
let type_options = {};
if (Format.edit_mode) {
type_options.template = 'dialog.create_texture.type.template'
if (!Project.box_uv) {
type_options.color_map = 'dialog.create_texture.type.color_map';
}
}
type_options.blank = 'dialog.create_texture.type.blank';
let resolution = Texture.getDefault() ? (Texture.getDefault().width/Project.texture_width)*16 : 16;
TextureGenerator.background_color.set('#00000000')
let resolution_presets = {
16: '16x',
32: '32x',
64: '64x',
128: '128x',
256: '256x',
512: '512x',
};
var dialog = new Dialog({
id: 'add_bitmap',
title: tl('action.create_texture'),
width: 480,
form: {
name: {label: 'generic.name', value: 'texture'},
folder: {label: 'dialog.create_texture.folder', condition: {features: ['texture_folder']}},
type: {label: 'dialog.create_texture.type', type: 'select', options: type_options},
section2: "_",
resolution: {label: 'dialog.create_texture.pixel_density', description: 'dialog.create_texture.pixel_density.desc', type: 'select', value: resolution_presets[resolution] ? resolution : undefined, condition: (form) => (form.type == 'template'), options: resolution_presets},
resolution_vec: {label: 'dialog.create_texture.resolution', type: 'vector', condition: (form) => (form.type == 'blank'), dimensions: 2, value: [Project.texture_width, Project.texture_height], min: 16, max: 2048},
color: {label: 'data.color', type: 'color', colorpicker: TextureGenerator.background_color},
rearrange_uv:{label: 'dialog.create_texture.rearrange_uv', description: 'dialog.create_texture.rearrange_uv.desc', type: 'checkbox', value: true, condition: (form) => (form.type == 'template')},
box_uv: {label: 'dialog.project.uv_mode.box_uv', type: 'checkbox', value: false, condition: (form) => (form.type == 'template' && !Project.box_uv)},
compress: {label: 'dialog.create_texture.compress', description: 'dialog.create_texture.compress.desc', type: 'checkbox', value: true, condition: (form) => (form.type == 'template' && Project.box_uv && form.rearrange_uv)},
power: {label: 'dialog.create_texture.power', description: 'dialog.create_texture.power.desc', type: 'checkbox', value: true, condition: (form) => (form.type !== 'blank' && (form.rearrange_uv || form.type == 'color_map'))},
double_use: {label: 'dialog.create_texture.double_use', description: 'dialog.create_texture.double_use.desc', type: 'checkbox', value: true, condition: (form) => (form.type == 'template' && Project.box_uv && form.rearrange_uv)},
combine_polys: {label: 'dialog.create_texture.combine_polys', description: 'dialog.create_texture.combine_polys.desc', type: 'checkbox', value: true, condition: (form) => (form.type == 'template' && form.rearrange_uv && Mesh.selected.length)},
max_edge_angle: {label: 'dialog.create_texture.max_edge_angle', description: 'dialog.create_texture.max_edge_angle.desc', type: 'number', value: 36, condition: (form) => (form.type == 'template' && form.rearrange_uv && Mesh.selected.length)},
max_island_angle: {label: 'dialog.create_texture.max_island_angle', description: 'dialog.create_texture.max_island_angle.desc', type: 'number', value: 45, condition: (form) => (form.type == 'template' && form.rearrange_uv && Mesh.selected.length)},
padding: {label: 'dialog.create_texture.padding', description: 'dialog.create_texture.padding.desc', type: 'checkbox', value: false, condition: (form) => (form.type == 'template' && form.rearrange_uv)},
},
onFormChange(form) {
if (form.type == 'template' && TextureGenerator.background_color.get().toHex8() === 'ffffffff') {
TextureGenerator.background_color.set('#00000000')
}
},
onConfirm: function(results) {
results.particle = 'auto';
if (results.type == 'blank') {
results.resolution = results.resolution_vec;
}
dialog.hide()
TextureGenerator.addBitmap(results);
return false;
}
}).show()
},
appendToTemplateDialog() {
let texture = Texture.getDefault();
if (!texture) return;
TextureGenerator.background_color.set('#00000000');
var dialog = new Dialog({
id: 'add_bitmap',
title: tl('action.append_to_template'),
width: 480,
form: {
color: {label: 'data.color', type: 'color', colorpicker: TextureGenerator.background_color},
box_uv: {label: 'dialog.project.uv_mode.box_uv', type: 'checkbox', value: false, condition: (form) => (!Project.box_uv)},
compress: {label: 'dialog.create_texture.compress', description: 'dialog.create_texture.compress.desc', type: 'checkbox', value: true, condition: (form) => Project.box_uv},
power: {label: 'dialog.create_texture.power', description: 'dialog.create_texture.power.desc', type: 'checkbox', value: Math.isPowerOfTwo(texture.width)},
double_use: {label: 'dialog.create_texture.double_use', description: 'dialog.create_texture.double_use.desc', type: 'checkbox', value: true, condition: (form) => Project.box_uv},
combine_polys: {label: 'dialog.create_texture.combine_polys', description: 'dialog.create_texture.combine_polys.desc', type: 'checkbox', value: true, condition: (form) => (Mesh.selected.length)},
max_edge_angle: {label: 'dialog.create_texture.max_edge_angle', description: 'dialog.create_texture.max_edge_angle.desc', type: 'number', value: 45, condition: (form) => Mesh.selected.length},
max_island_angle: {label: 'dialog.create_texture.max_island_angle', description: 'dialog.create_texture.max_island_angle.desc', type: 'number', value: 45, condition: (form) => Mesh.selected.length},
padding: {label: 'dialog.create_texture.padding', description: 'dialog.create_texture.padding.desc', type: 'checkbox', value: false, condition: (form) => (form.rearrange_uv)},
},
onFormChange(form) {
if (TextureGenerator.background_color.get().toHex8() === 'ffffffff') {
TextureGenerator.background_color.set('#00000000')
}
},
onConfirm(options) {
dialog.hide()
options.rearrange_uv = true;
options.resolution = 16 * texture.width / Project.texture_width;
if (Format.single_texture) {
options.texture = Texture.getDefault()
}
if (Project.box_uv || options.box_uv) {
TextureGenerator.generateBoxTemplate(options, texture);
} else {
TextureGenerator.generateFaceTemplate(options, texture);
}
return false;
}
}).show()
},
addBitmap(options, after) {
if (typeof options !== 'object') {
options = {}
}
if (!options.resolution || isNaN(options.resolution[0]) || isNaN(options.resolution[1])) {
options.resolution = [16, 16]
}
if (options.color === undefined) {
options.color = new tinycolor().toRgb()
}
if (Format.single_texture) {
options.texture = Texture.getDefault()
}
var texture = new Texture({
mode: 'bitmap',
keep_size: true,
name: options.name ? options.name : 'texture',
folder: options.folder ? options.folder : 'block'
})
function makeTexture(dataUrl) {
texture.fromDataURL(dataUrl).add(false).select()
switch (options.particle) {
case 'auto':
texture.fillParticle();
break;
case true:
texture.enableParticle();
break;
}
if (typeof after === 'function') {
after(texture)
}
if (options.type == 'blank') {
Undo.finishEdit('Create blank texture', {textures: [texture], selected_texture: true, bitmap: true})
}
return texture;
}
if (options.type == 'template') {
if (Project.box_uv || options.box_uv) {
TextureGenerator.generateBoxTemplate(options, makeTexture);
} else {
TextureGenerator.generateFaceTemplate(options, makeTexture);
}
} else if (options.type == 'color_map') {
TextureGenerator.generateColorMapTemplate(options, makeTexture);
} else {
Undo.initEdit({textures: [], selected_texture: true})
TextureGenerator.generateBlank(options.resolution[1], options.resolution[0], options.color, makeTexture)
}
},
generateBlank(height, width, color, cb) {
var canvas = document.createElement('canvas')
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext('2d')
ctx.fillStyle = new tinycolor(color).toRgbString()
ctx.fillRect(0, 0, width, height)
cb(canvas.toDataURL())
},
//constructors
boxUVCubeTemplate: function(obj, min_size) {
let floor_uv = Format.box_uv_float_size != true;
this.x = Math.round(obj.size(0, floor_uv)) || min_size;
this.y = Math.round(obj.size(1, floor_uv)) || min_size;
this.z = Math.round(obj.size(2, floor_uv)) || min_size;
this.posx = obj.uv_offset[0];
this.posy = obj.uv_offset[1];
this.obj = obj;
this.template_size = (obj.size(2, floor_uv) + obj.size(1, floor_uv))+ (obj.size(2, floor_uv) + obj.size(0, floor_uv))*2;
this.height = this.z + this.y;
this.width = 2* (this.x + this.z);
return this;
},
//BoxUV Template
generateBoxTemplate(options, makeTexture) {
var res = options.resolution;
var background_color = options.color;
var texture = options.texture;
var min_size = (Project.box_uv || options.box_uv) ? 0 : 1;
var res_multiple = res / 16
var templates = [];
var doubles = {};
var extend_x = 0;
var extend_y = 0;
var avg_size = 0;
var new_resolution = [];
var cubes = (Format.single_texture && typeof makeTexture == 'function') ? Cube.all.slice() : Cube.selected.slice();
Undo.initEdit({
textures: makeTexture instanceof Texture ? [makeTexture] : [],
elements: cubes,
uv_only: true,
bitmap: true,
selected_texture: true,
uv_mode: true
})
var i = cubes.length-1
while (i >= 0) {
let obj = cubes[i]
if (obj.visibility === true) {
var template = new TextureGenerator.boxUVCubeTemplate(obj, min_size);
if (options.double_use && Project.box_uv && Texture.all.length) {
var double_key = [...obj.uv_offset, ...obj.size(undefined, true), ].join('_')
if (doubles[double_key]) {
// improve chances that original is not mirrored
if (doubles[double_key][0].obj.mirror_uv && !obj.mirror_uv) {
templates[templates.indexOf(doubles[double_key][0])] = template;
doubles[double_key].splice(0, 0, template)
} else {
doubles[double_key].push(template)
}
doubles[double_key][0].duplicates = doubles[double_key];
i--;
continue;
} else {
doubles[double_key] = [template]
}
}
templates.push(template)
avg_size += templates[templates.length-1].template_size
}
i--;
}
//Cancel if no cubes
if (templates.length == 0) {
if (Mesh.selected.length) {
Blockbench.showMessageBox({
title: 'message.no_valid_elements',
message: 'message.meshes_and_box_uv',
icon: 'fa-gem'
})
} else {
Blockbench.showMessage('message.no_valid_elements', 'center');
}
return;
} else if (Mesh.selected.length) {
Blockbench.showMessage('message.meshes_and_box_uv', 'center');
}
templates.sort(function(a,b) {
return b.template_size - a.template_size;
})
if (options.rearrange_uv) {
if (options.compress || makeTexture instanceof Texture) {
var fill_map = {};
if (makeTexture instanceof Texture) {
extend_x = makeTexture.width;
extend_y = makeTexture.height;
Cube.all.forEach(element => {
for (let fkey in element.faces) {
let face = element.faces[fkey];
if (face.getTexture() !== makeTexture) continue;
let rect = face.getBoundingRect();
for (let x = Math.floor(rect.ax); x < Math.ceil(rect.bx); x++) {
for (let y = Math.floor(rect.ay); y < Math.ceil(rect.by); y++) {
if (!fill_map[x]) fill_map[x] = {};
fill_map[x][y] = true;
}
}
}
})
}
function occupy(x, y) {
if (!fill_map[x]) fill_map[x] = {}
fill_map[x][y] = true
}
function check(x, y) {
return fill_map[x] && fill_map[x][y]
}
function forTemplatePixel(tpl, sx, sy, cb) {
let w = tpl.width;
let h = tpl.height;
if (options.padding) {
w++; h++;
}
for (var x = 0; x < w; x++) {
for (var y = 0; y < h; y++) {
if (y >= tpl.z || (x >= tpl.z && x < (tpl.z + 2*tpl.x + (options.padding ? 1 : 0)))) {
if (cb(sx+x, sy+y)) return;
}
}
}
}
function place(tpl, x, y) {
var works = true;
forTemplatePixel(tpl, x, y, (tx, ty) => {
if (check(tx, ty)) {
works = false;
return true;
}
})
if (works) {
forTemplatePixel(tpl, x, y, occupy)
tpl.posx = x;
tpl.posy = y;
extend_x = Math.max(extend_x, x + tpl.width);
extend_y = Math.max(extend_y, y + tpl.height);
return true;
}
}
templates.forEach(tpl => {
var vert = extend_x > extend_y;
//Scan for empty spot
for (var line = 0; line < 2e3; line++) {
for (var space = 0; space <= line; space++) {
if (place(tpl, space, line)) return;
if (space == line) continue;
if (place(tpl, line, space)) return;
}
}
})
} else {
//OLD -------------------------------------------
var lines = [[]]
var line_length = Math.sqrt(cubes.length/2)
avg_size /= templates.length
var o = 0
var i = 0
var ox = 0
templates.forEach(function(tpl) {
if (ox >= line_length) {
o = ox = 0
i++
lines[i] = []
}
lines[i][o] = tpl
o++;
ox += tpl.template_size/avg_size
})
lines.forEach(function(temps) {
var x_pos = 0
var y_pos = 0 //Y Position of current area relative to this bone
var filled_x_pos = 0;
var max_height = 0
//Find the maximum height of the line
temps.forEach(function(t) {
max_height = Math.max(max_height, t.height + (options.padding ? 1 : 0))
})
//Place
temps.forEach(function(t) {
let w = t.width;
let h = t.height;
if (options.padding) {
w++; h++;
}
if (y_pos > 0 && (y_pos + h) <= max_height) {
//same column
t.posx = x_pos
t.posy = y_pos + extend_y
filled_x_pos = Math.max(filled_x_pos, x_pos + w)
y_pos += h
} else {
//new column
x_pos = filled_x_pos
y_pos = h
t.posx = x_pos
t.posy = extend_y
filled_x_pos = Math.max(filled_x_pos, x_pos + w)
}
//size of widest bone
extend_x = Math.max(extend_x, filled_x_pos)
})
extend_y += max_height
})
}
var max_size = Math.max(extend_x, extend_y);
if (options.power) {
max_size = Math.getNextPower(max_size, 16);
} else {
max_size = Math.ceil(max_size/16)*16;
}
new_resolution = [max_size, max_size];
} else {
new_resolution = [Project.texture_width, Project.texture_height];
}
if (background_color.getAlpha() != 0) {
background_color = background_color.toRgbString()
}
let canvas = document.createElement('canvas');
let ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;
if (makeTexture instanceof Texture) {
if (makeTexture.mode === 'link') {
makeTexture.source = 'data:image/png;base64,' + makeTexture.getBase64();
makeTexture.mode = 'bitmap';
makeTexture.saved = false;
}
canvas.width = Math.max(new_resolution[0] * res_multiple, makeTexture.width);
canvas.height = Math.max(new_resolution[1] * res_multiple, makeTexture.height);
ctx.drawImage(makeTexture.img, 0, 0);
} else {
canvas.width = new_resolution[0] * res_multiple;
canvas.height = new_resolution[1] * res_multiple;
}
//Drawing
TextureGenerator.old_project_resolution = [Project.texture_width, Project.texture_height]
let affected_elements = TextureGenerator.changeProjectResolution(new_resolution[0], new_resolution[1]);
templates.forEach(function(t) {
if (options.rearrange_uv) {
t.obj.uv_offset[0] = t.posx;
t.obj.uv_offset[1] = t.posy;
if (Project.box_uv || Format.optional_box_uv) t.obj.box_uv = true;
//if true, dupes must be flipped
let reverse_flip = t.obj.mirror_uv;
t.obj.mirror_uv = false;
if (t.duplicates) {
t.duplicates.forEach(dupl => {
if (dupl.obj !== t.obj) {
dupl.obj.mirror_uv = dupl.obj.mirror_uv !== reverse_flip;
}
})
}
}
TextureGenerator.paintCubeBoxTemplate(t.obj, texture, canvas, t);
})
var dataUrl = canvas.toDataURL()
var texture = typeof makeTexture == 'function' ? makeTexture(dataUrl) : makeTexture;
if (makeTexture instanceof Texture) {
makeTexture.updateSource(dataUrl);
}
if (texture) {
cubes.forEach(function(cube) {
if (!Format.single_texture) {
cube.applyTexture(texture, true);
}
cube.preview_controller.updateUV(cube);
cube.autouv = 0;
})
}
if (options.box_uv && !Project.box_uv && Project.optional_box_uv) {
Project.box_uv = true;
}
templates.forEach(function(t) {
if (options.rearrange_uv) {
t.obj.uv_offset[0] = t.posx;
t.obj.uv_offset[1] = t.posy;
if (t.duplicates) {
t.duplicates.forEach(dupl => {
if (dupl.obj !== t.obj) {
dupl.obj.uv_offset[0] = t.posx;
dupl.obj.uv_offset[1] = t.posy;
}
})
}
}
})
updateSelection()
let updated_elements = cubes.slice();
updated_elements.safePush(...affected_elements);
Undo.finishEdit('Create template', {
textures: [texture],
bitmap: true,
elements: updated_elements,
selected_texture: true,
uv_only: true,
uv_mode: true
})
// Warning
if (cubes.find(cube => {
let size = cube.size();
return (size[0] > 0.001 && size[0] < 0.999) || (size[1] > 0.001 && size[1] < 0.999) || (size[2] > 0.001 && size[2] < 0.999)
})) {
Blockbench.showMessageBox({
title: 'message.small_face_dimensions.title',
message: tl('message.small_face_dimensions.message') + (Format.optional_box_uv ? '\n\n' + tl('message.small_face_dimensions.face_uv') : ''),
icon: 'warning',
})
}
},
boxUVdrawTemplateRectangle(border_color, color, face, coords, texture, canvas) {
if (typeof background_color === 'string') {
border_color = background_color
color = undefined
}
var res_multiple = canvas.width/Project.texture_width;
var ctx = canvas.getContext('2d');
ctx.fillStyle = border_color;
ctx.fillRect(
coords.x*res_multiple,
coords.y*res_multiple,
coords.w*res_multiple,
coords.h*res_multiple
)
if (coords.w*res_multiple > 2 && coords.h*res_multiple > 2) {
if (color == null) {
ctx.clearRect(
coords.x * res_multiple + 1,
coords.y * res_multiple + 1,
coords.w * res_multiple - 2,
coords.h * res_multiple - 2
)
} else if (color) {
ctx.fillStyle = color
ctx.fillRect(
coords.x * res_multiple + 1,
coords.y * res_multiple + 1,
coords.w * res_multiple - 2,
coords.h * res_multiple - 2
)
}
}
},
boxUVdrawTexture(face, coords, texture, canvas) {
if (!Format.single_texture) {
if (face.texture === undefined || face.texture === null) return false;
texture = face.getTexture()
}
if (!texture || !texture.img) return false;
var ctx = canvas.getContext('2d');
var res_multiple = canvas.width/Project.texture_width;
ctx.save()
var uv = face.uv.slice();
if (face.direction === 'up') {
uv = [uv[2], uv[3], uv[0], uv[1]]
} else if (face.direction === 'down') {
uv = [uv[2], uv[1], uv[0], uv[3]]
}
var src = getRectangle(uv[0], uv[1], uv[2], uv[3])
var flip = [
uv[0] > uv[2] ? -1 : 1,
uv[1] > uv[3] ? -1 : 1
]
if (flip[0] + flip[1] < 1) {
ctx.scale(flip[0], flip[1])
}
if (face.rotation) {
ctx.rotate(Math.degToRad(face.rotation))
let rot = face.rotation
if (rot <= 180) flip[1] *= -1;
if (rot >= 180) flip[0] *= -1;
while (rot > 0) {
[coords.x, coords.y] = [coords.y, coords.x];
[coords.w, coords.h] = [coords.h, coords.w];
rot -= 90;
}
}
ctx.imageSmoothingEnabled = false;
ctx.drawImage(
texture.img,
src.ax/TextureGenerator.old_project_resolution[0] * texture.img.naturalWidth,
src.ay/TextureGenerator.old_project_resolution[1] * texture.img.naturalHeight,
src.x /TextureGenerator.old_project_resolution[0] * texture.img.naturalWidth,
src.y /TextureGenerator.old_project_resolution[1] * texture.img.naturalHeight,
coords.x*res_multiple*flip[0],
coords.y*res_multiple*flip[1],
coords.w*res_multiple*flip[0],
coords.h*res_multiple*flip[1]
)
ctx.restore()
return true;
},
paintCubeBoxTemplate(cube, texture, canvas, template, transparent) {
if (!template) {
template = new TextureGenerator.boxUVCubeTemplate(cube, Project.box_uv ? 0 : 1);
}
for (var face in TextureGenerator.face_data) {
let d = TextureGenerator.face_data[face]
if (face == 'west' && cube.size(0) == 0) continue;
if (face == 'down' && cube.size(1) == 0) continue;
if (face == 'south' && cube.size(2) == 0) continue;
if (!cube.faces[face].getTexture() ||
!TextureGenerator.boxUVdrawTexture(cube.faces[face], d.place(template), texture, canvas)
) {
TextureGenerator.boxUVdrawTemplateRectangle(d.c1, transparent ? null : d.c2, cube.faces[face], d.place(template), texture, canvas)
}
}
if (template && template.duplicates) {
template.duplicates.forEach(t_2 => {
t_2.obj.uv_offset[0] = cube.uv_offset[0];
t_2.obj.uv_offset[1] = cube.uv_offset[1];
if (t_2.obj !== cube) {
t_2.obj.mirror_uv = t_2.obj.mirror_uv != cube.mirror_uv;
}
})
}
if (!cube.box_uv) {
var size = cube.size(undefined, Format.box_uv_float_size != true);
size.forEach((n, i) => {
size[i] = n;
})
var face_list = [
{face: 'north', fIndex: 10, from: [size[2], size[2]], size: [size[0], size[1]]},
{face: 'east', fIndex: 0, from: [0, size[2]], size: [size[2], size[1]]},
{face: 'south', fIndex: 8, from: [size[2]*2 + size[0], size[2]], size: [size[0], size[1]]},
{face: 'west', fIndex: 2, from: [size[2] + size[0], size[2]], size: [size[2], size[1]]},
{face: 'up', fIndex: 4, from: [size[2]+size[0], size[2]], size: [-size[0], -size[2]]},
{face: 'down', fIndex: 6, from: [size[2]+size[0]*2, 0], size: [-size[0], size[2]]}
]
face_list.forEach(function(f) {
cube.faces[f.face].uv[0] = (f.from[0] + Math.floor(cube.uv_offset[0]+0.0000001)) / canvas.width * Project.texture_width;
cube.faces[f.face].uv[1] = (f.from[1] + Math.floor(cube.uv_offset[1]+0.0000001)) / canvas.height * Project.texture_height;
cube.faces[f.face].uv[2] = (f.from[0]+f.size[0]+ Math.floor(cube.uv_offset[0]+0.0000001)) / canvas.width * Project.texture_width;
cube.faces[f.face].uv[3] = (f.from[1]+f.size[1]+ Math.floor(cube.uv_offset[1]+0.0000001)) / canvas.height * Project.texture_height;
cube.faces[f.face].rotation = 0;
})
}
},
//Face Template
generateFaceTemplate(options, makeTexture) {
var res_multiple = options.resolution / 16;
var background_color = options.color;
var texture = options.texture;
var new_resolution = [];
let vec1 = new THREE.Vector3(),
vec2 = new THREE.Vector3(),
vec3 = new THREE.Vector3(),
vec4 = new THREE.Vector3();
var face_list = [];
var element_list = ((Format.single_texture && typeof makeTexture == 'function') ? Outliner.elements : Outliner.selected).filter(el => {
return (el instanceof Cube || el instanceof Mesh) && el.visibility;
});
function faceRect(cube, face_key, tex, x, y) {
this.cube = cube;
if (options.rearrange_uv) {
this.width = Math.abs(x) * res_multiple;
this.height = Math.abs(y) * res_multiple;
this.width = ((this.width >= 0.01 && this.width < 1) ? 1 : Math.round(this.width)) / res_multiple;
this.height = ((this.height >= 0.01 && this.height < 1) ? 1 : Math.round(this.height)) / res_multiple;
} else {
this.posx = cube.faces[face_key].uv[0], cube.faces[face_key].uv[0+2];
this.posy = cube.faces[face_key].uv[1], cube.faces[face_key].uv[1+2];
this.width = cube.faces[face_key].uv[0+2] - cube.faces[face_key].uv[0];
this.height = cube.faces[face_key].uv[1+2] - cube.faces[face_key].uv[1];
}
this.size = this.width * this.height;
this.face_key = face_key;
this.texture = tex
this.face = cube.faces[face_key];
face_list.push(this);
}
Undo.initEdit({
textures: makeTexture instanceof Texture ? [makeTexture] : [],
elements: element_list,
uv_only: true,
bitmap: true,
selected_texture: true,
uv_mode: true
})
element_list.forEach(element => {
if (element instanceof Cube) {
for (var face_key in element.faces) {
var face = element.faces[face_key];
var tex = face.getTexture();
if (tex !== null) {
var x = 0;
var y = 0;
switch (face_key) {
case 'north': x = element.size(0); y = element.size(1); break;
case 'east': x = element.size(2); y = element.size(1); break;
case 'south': x = element.size(0); y = element.size(1); break;
case 'west': x = element.size(2); y = element.size(1); break;
case 'up': x = element.size(0); y = element.size(2); break;
case 'down': x = element.size(0); y = element.size(2); break;
}
new faceRect(element, face_key, tex, x, y)
}
}
} else {
let mesh = element;
let face_groups = [];
for (let fkey in mesh.faces) {
let face = mesh.faces[fkey];
if (face.vertices.length < 3) continue;
if (makeTexture instanceof Texture && BarItems.selection_mode.value !== 'object' && !face.isSelected()) continue;
face_groups.push({
type: 'face_group',
mesh,
faces: [face],
keys: [fkey],
edges: new Map(),
normal: face.getNormal(true),
vertex_uvs: {},
texture: face.getTexture()
})
}
function getEdgeLength(edge) {
let edge_vertices = edge.map(vkey => mesh.vertices[vkey]);
return Math.sqrt(
Math.pow(edge_vertices[1][0] - edge_vertices[0][0], 2) +
Math.pow(edge_vertices[1][1] - edge_vertices[0][1], 2) +
Math.pow(edge_vertices[1][2] - edge_vertices[0][2], 2)
)
}
// Sort straight faces first
function getNormalStraightness(normal) {
let absolute = normal.map(Math.abs);
return (
(absolute[0] < 0.5 ? absolute[0] : (1-absolute[0]))*1.6 +
(absolute[1] < 0.5 ? absolute[1] : (1-absolute[1])) +
(absolute[2] < 0.5 ? absolute[2] : (1-absolute[2]))
)
}
face_groups.sort((a, b) => {
return getNormalStraightness(a.normal) - getNormalStraightness(b.normal);
})
let processed_faces = [];
function projectFace(face, fkey, face_group, connection) {
// Project vertex coords onto plane
let {vertex_uvs} = face_group;
let normal_vec = vec1.fromArray(face.getNormal(true));
let plane = new THREE.Plane().setFromNormalAndCoplanarPoint(
normal_vec,
vec2.fromArray(mesh.vertices[face.vertices[0]])
)
let sorted_vertices = face.getSortedVertices();
let rot = cameraTargetToRotation([0, 0, 0], normal_vec.toArray());
let e = new THREE.Euler(Math.degToRad(rot[1] - 90), Math.degToRad(rot[0] + 180), 0);
let face_vertex_uvs = {};
face.vertices.forEach(vkey => {
let coplanar_pos = plane.projectPoint(vec3.fromArray(mesh.vertices[vkey]), vec4);
coplanar_pos.applyEuler(e);
face_vertex_uvs[vkey] = [
Math.roundTo(coplanar_pos.x, 4),
Math.roundTo(coplanar_pos.z, 4),
]
})
if (connection) {
// Rotate to connect to previous face
let other_face_vertex_uvs = vertex_uvs[connection.fkey];
let uv_hinge = face_vertex_uvs[connection.edge[0]];
let uv_latch = face_vertex_uvs[connection.edge[1]];
let uv_lock = other_face_vertex_uvs[connection.edge[1]];
// Join hinge
let offset = uv_hinge.slice().V2_subtract(other_face_vertex_uvs[connection.edge[0]]);
for (let vkey in face_vertex_uvs) {
face_vertex_uvs[vkey].V2_subtract(offset);
}
// Join latch
uv_hinge = uv_hinge.slice();
let angle = Math.atan2(
uv_hinge[0] - uv_latch[0],
uv_hinge[1] - uv_latch[1],
) - Math.atan2(
uv_hinge[0] - uv_lock[0],
uv_hinge[1] - uv_lock[1],
);
let s = Math.sin(angle);
let c = Math.cos(angle);
for (let vkey in face_vertex_uvs) {
let point = face_vertex_uvs[vkey].slice().V2_subtract(uv_hinge);
face_vertex_uvs[vkey][0] = point[0] * c - point[1] * s;
face_vertex_uvs[vkey][1] = point[0] * s + point[1] * c;
face_vertex_uvs[vkey].V2_add(uv_hinge);
}
// Check overlap
function isSameUVVertex(point_a, point_b) {
return (Math.epsilon(point_a[0], point_b[0], 0.1)
&& Math.epsilon(point_a[1], point_b[1], 0.1))
}
let i = -1;
for (let other_face of face_group.faces) {
i++;
if (other_face == connection.face) continue;
let other_fkey = face_group.keys[i];
let sorted_vertices_b = other_face.getSortedVertices();
let l1 = 0;
for (let vkey_1_a of sorted_vertices) {
let vkey_1_b = sorted_vertices[l1+1] || sorted_vertices[0]
let l2 = 0;
for (let vkey_2_a of sorted_vertices_b) {
let vkey_2_b = sorted_vertices_b[l2+1] || sorted_vertices_b[0];
if (intersectLines(
face_vertex_uvs[vkey_1_a],
face_vertex_uvs[vkey_1_b],
face_group.vertex_uvs[other_fkey][vkey_2_a],
face_group.vertex_uvs[other_fkey][vkey_2_b]
)) {
if (
!isSameUVVertex(face_vertex_uvs[vkey_1_a], face_group.vertex_uvs[other_fkey][vkey_2_a]) &&
!isSameUVVertex(face_vertex_uvs[vkey_1_a], face_group.vertex_uvs[other_fkey][vkey_2_b]) &&
!isSameUVVertex(face_vertex_uvs[vkey_1_b], face_group.vertex_uvs[other_fkey][vkey_2_a]) &&
!isSameUVVertex(face_vertex_uvs[vkey_1_b], face_group.vertex_uvs[other_fkey][vkey_2_b])
) {
return false;
}
}
l2++;
}
l1++;
}
}
}
face_group.vertex_uvs[fkey] = face_vertex_uvs;
return true;
}
if (options.combine_polys) {
face_groups.slice().forEach((face_group) => {
if (!face_groups.includes(face_group)) return;
function growFromFaces(faces) {
let perimeter = {};
for (let fkey in faces) {
let face = faces[fkey];
processed_faces.push(face);
[2, 0, 3, 1].forEach(i => {
if (!face.vertices[i]) return;
let other_face_match = face.getAdjacentFace(i);
let edge = other_face_match && face.vertices.filter(vkey => other_face_match.face.vertices.includes(vkey));
if (other_face_match && edge.length == 2 && !face_group.faces.includes(other_face_match.face) && !processed_faces.includes(other_face_match.face)) {
let other_face = other_face_match.face;
let other_face_group = face_groups.find(group => group.faces[0] == other_face);
if (!other_face_group) return;
let seam = mesh.getSeam(other_face_match.edge);
if (seam === 'divide') return;
if (seam !== 'join') {
let angle = face.getAngleTo(other_face);
if (angle > (options.max_edge_angle||36)) return;
let angle_total = face_group.faces[0].getAngleTo(other_face);
if (angle_total > (options.max_island_angle||45)) return;
let edge_length = getEdgeLength(other_face_match.edge);
if (edge_length < 2.2) return;
}
let projection_success = projectFace(other_face, other_face_match.key, face_group, {face, fkey, edge});
if (!projection_success) return;
face_group.faces.push(other_face);
face_group.keys.push(other_face_match.key);
face_groups.remove(other_face_group);
perimeter[other_face_match.key] = other_face;
}
})
}
if (Object.keys(perimeter).length) growFromFaces(perimeter);
}
projectFace(face_group.faces[0], face_group.keys[0], face_group);
growFromFaces({[face_group.keys[0]]: face_group.faces[0]});
});
} else {
face_groups.forEach(face_group => {
face_group.faces.forEach((face, i) => {
let fkey = face_group.keys[i];
projectFace(face, fkey, face_group);
})
})
}
face_groups.forEach(face_group => {
let {vertex_uvs} = face_group;
// Rotate UV to match corners
if (face_group.faces.length == 1) {
let rotation_angles = {};
let precise_rotation_angle = {};
face_group.faces.forEach((face, i) => {
let fkey = face_group.keys[i];
let vertices = face.getSortedVertices();
vertices.forEach((vkey, i) => {
let vkey2 = vertices[i+1] || vertices[0];
let edge_length = getEdgeLength([vkey, vkey2]);
let rot = Math.atan2(
vertex_uvs[fkey][vkey2][0] - vertex_uvs[fkey][vkey][0],
vertex_uvs[fkey][vkey2][1] - vertex_uvs[fkey][vkey][1],
)
let snap = 2;
rot = (Math.radToDeg(rot) + 360) % 90;
let rounded
let last_difference = snap;
for (let rounded_angle in precise_rotation_angle) {
let precise = precise_rotation_angle[rounded_angle];
if (Math.abs(rot - precise) < last_difference) {
last_difference = Math.abs(rot - precise);
rounded = rounded_angle;
}
}
if (!rounded) rounded = Math.round(rot / snap) * snap;
if (rotation_angles[rounded]) {
rotation_angles[rounded] += edge_length;
} else {
rotation_angles[rounded] = edge_length;
precise_rotation_angle[rounded] = rot;
}
})
})
let angles = Object.keys(rotation_angles).map(k => parseInt(k));
angles.sort((a, b) => {
let diff = rotation_angles[b] - rotation_angles[a];
if (diff) {
return diff;
} else {
return a < b ? -1 : 1;
}
})
if (rotation_angles[angles[0]] > 1) {
let angle = Math.degToRad(precise_rotation_angle[angles[0]]);
let s = Math.sin(angle);
let c = Math.cos(angle);
for (let fkey in vertex_uvs) {
for (let vkey in vertex_uvs[fkey]) {
let point = vertex_uvs[fkey][vkey].slice();
vertex_uvs[fkey][vkey][0] = point[0] * c - point[1] * s;
vertex_uvs[fkey][vkey][1] = point[0] * s + point[1] * c;
}
}
}
}
// Define UV bounding box
let min_x = Infinity;
let min_z = Infinity;
for (let fkey in vertex_uvs) {
for (let vkey in vertex_uvs[fkey]) {
min_x = Math.min(min_x, vertex_uvs[fkey][vkey][0]);
min_z = Math.min(min_z, vertex_uvs[fkey][vkey][1]);
}
}
for (let fkey in vertex_uvs) {
for (let vkey in vertex_uvs[fkey]) {
vertex_uvs[fkey][vkey][0] -= min_x;
vertex_uvs[fkey][vkey][1] -= min_z;
}
}
// Round
if (face_group.faces.length == 1 && face_group.faces[0].vertices.length == 4) {
let sorted_vertices = face_group.faces[0].getSortedVertices();
sorted_vertices.forEach((vkey, vi) => {
let vkey2 = sorted_vertices[vi+1] || sorted_vertices[0];
let vkey0 = sorted_vertices[vi-1] || sorted_vertices.last();
let snap = 1;
let vertex_uvs_1 = vertex_uvs[face_group.keys[0]];
if (Math.epsilon(vertex_uvs_1[vkey][0], vertex_uvs_1[vkey2][0], 0.001)) {
let min = vertex_uvs_1[vkey][0] > vertex_uvs_1[vkey0][0] ? 1 : 0;
vertex_uvs_1[vkey][0] = vertex_uvs_1[vkey2][0] = Math.round(Math.max(min, vertex_uvs_1[vkey][0] * snap)) / snap;
}
if (Math.epsilon(vertex_uvs_1[vkey][1], vertex_uvs_1[vkey2][1], 0.001)) {
let min = vertex_uvs_1[vkey][1] > vertex_uvs_1[vkey0][1] ? 1 : 0;
vertex_uvs_1[vkey][1] = vertex_uvs_1[vkey2][1] = Math.round(Math.max(min, vertex_uvs_1[vkey][1] * snap)) / snap;
}
})
}
max_x = -Infinity;
max_z = -Infinity;
for (let fkey in vertex_uvs) {
for (let vkey in vertex_uvs[fkey]) {
max_x = Math.max(max_x, vertex_uvs[fkey][vkey][0]);
max_z = Math.max(max_z, vertex_uvs[fkey][vkey][1]);
}
}
// Align right if face points to right side of model
if ((face_group.normal[0] > 0) != (face_group.normal[2] < 0)) {
for (let fkey in vertex_uvs) {
for (let vkey in vertex_uvs[fkey]) {
vertex_uvs[fkey][vkey][0] += Math.ceil(max_x) - max_x;
}
}
}
// Align bottom if face points downwards
if (face_group.normal[1] < 0) {
for (let fkey in vertex_uvs) {
for (let vkey in vertex_uvs[fkey]) {
vertex_uvs[fkey][vkey][1] += Math.ceil(max_z) - max_z;
}
}
}
face_group.posx = 0;
face_group.posy = 0;
face_group.vertex_uvs = vertex_uvs;
face_group.width = max_x;
face_group.height = max_z;
face_group.size = max_x * max_z;
let axis = [0, 1, 2].sort((a, b) => {
return Math.abs(face_group.normal[b]) - Math.abs(face_group.normal[a]) - 0.0001
})[0]
if (axis == 0 && face_group.normal[0] >= 0) face_group.face_key = 'east';
if (axis == 0 && face_group.normal[0] <= 0) face_group.face_key = 'west';
if (axis == 1 && face_group.normal[1] >= 0) face_group.face_key = 'up';
if (axis == 1 && face_group.normal[1] <= 0) face_group.face_key = 'down';
if (axis == 2 && face_group.normal[2] >= 0) face_group.face_key = 'south';
if (axis == 2 && face_group.normal[2] <= 0) face_group.face_key = 'north';
})
face_list.push(...face_groups);
}
})
if (face_list.length == 0) {
Blockbench.showMessage('message.no_valid_elements', 'center')
return;
}
if (options.rearrange_uv) {
var extend_x = 0;
var extend_y = 0;
var fill_map = {};
if (makeTexture instanceof Texture) {
extend_x = makeTexture.width / res_multiple;
extend_y = makeTexture.height / res_multiple;
[...Cube.all, ...Mesh.all].forEach(element => {
for (let fkey in element.faces) {
let face = element.faces[fkey];
if (face.getTexture() !== makeTexture) continue;
let rect = face.getBoundingRect();
let face_matrix;
if (face instanceof MeshFace) {
face_matrix = face.getOccupationMatrix(false);
}
for (let x = Math.floor(rect.ax); x < Math.ceil(rect.bx); x++) {
if (face_matrix && !face_matrix[x]) continue;
for (let y = Math.floor(rect.ay); y < Math.ceil(rect.by); y++) {
if (face_matrix && !face_matrix[x][y]) continue;
if (!fill_map[x]) fill_map[x] = {};
fill_map[x][y] = true;
}
}
}
})
}
face_list.forEach(face_group => {
if (!face_group.mesh) return;
let face_uvs = face_group.faces.map((face, i) => {
return face.getSortedVertices().map(vkey => {
return face_group.vertex_uvs[face_group.keys[i]][vkey];
})
});
face_group.matrix = getPolygonOccupationMatrix(face_uvs, face_group.width, face_group.height);
})
face_list.sort(function(a,b) {
return b.size - a.size;
})
function occupy(x, y) {
if (!fill_map[x]) fill_map[x] = {}
fill_map[x][y] = true
}
function check(x, y) {
return fill_map[x] && fill_map[x][y]
}
function forTemplatePixel(tpl, sx, sy, cb) {
let w = tpl.width;
let h = tpl.height;
if (options.padding) {
w++; h++;
for (var x = 0; x < w; x++) {
if (tpl.matrix && !tpl.matrix[x] && !tpl.matrix[x-1]) continue;
for (var y = 0; y < h; y++) {
if (
tpl.matrix &&
(!tpl.matrix[x] || !tpl.matrix[x][y]) &&
(!tpl.matrix[x-1] || !tpl.matrix[x-1][y]) &&
(!tpl.matrix[x] || !tpl.matrix[x][y-1]) &&
(!tpl.matrix[x-1] || !tpl.matrix[x-1][y-1])
) continue;
if (cb(sx+x, sy+y)) return;
}
}
} else {
for (var x = 0; x < w; x++) {
if (tpl.matrix && !tpl.matrix[x]) continue;
for (var y = 0; y < h; y++) {
if (tpl.matrix && !tpl.matrix[x][y]) continue;
if (cb(sx+x, sy+y)) return;
}
}
}
}
function place(tpl, x, y) {
var works = true;
forTemplatePixel(tpl, x, y, (tx, ty) => {
if (check(tx, ty)) {
works = false;
return true;
}
})
if (works) {
forTemplatePixel(tpl, x, y, occupy)
tpl.posx = x;
tpl.posy = y;
extend_x = Math.max(extend_x, x + tpl.width);
extend_y = Math.max(extend_y, y + tpl.height);
return true;
}
}
face_list.forEach(tpl => {
//Scan for empty spot
for (var line = 0; line < 2e3; line++) {
for (var space = 0; space <= line; space++) {
if (place(tpl, space, line)) return;
if (space == line) continue;
if (place(tpl, line, space)) return;
}
}
})
var max_size = Math.max(extend_x, extend_y)
if (options.power) {
max_size = Math.getNextPower(max_size*res_multiple, 16)/res_multiple;
} else {
max_size = Math.ceil(max_size*res_multiple/16)*16/res_multiple;
}
new_resolution = [max_size, max_size];
} else {
new_resolution = [Project.texture_width, Project.texture_height];
face_list.forEach(face_group => {
if (!face_group.mesh) return;
let face_uvs = face_group.faces.map((face, i) => {
let rect = face.getBoundingRect();
face_group.posx = rect.ax;
face_group.posy = rect.ay;
return face.getSortedVertices().map(vkey => {
return [face.uv[vkey][0]-rect.ax, face.uv[vkey][1]-rect.ay];
})
});
face_group.matrix = getPolygonOccupationMatrix(face_uvs, face_group.width, face_group.height);
})
}
if (background_color.getAlpha() != 0) {
background_color = background_color.toRgbString()
}
let canvas = document.createElement('canvas');
let ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;
if (makeTexture instanceof Texture) {
if (makeTexture.mode === 'link') {
makeTexture.source = 'data:image/png;base64,' + makeTexture.getBase64();
makeTexture.mode = 'bitmap';
makeTexture.saved = false;
}
canvas.width = Math.max(new_resolution[0] * res_multiple, makeTexture.width);
canvas.height = Math.max(new_resolution[1] * res_multiple, makeTexture.height);
ctx.drawImage(makeTexture.img, 0, 0);
} else {
canvas.width = new_resolution[0] * res_multiple;
canvas.height = new_resolution[1] * res_multiple;
}
function getPolygonOccupationMatrix(vertex_uv_faces, width, height) {
let matrix = {};
function vSub(a, b) {
return [a[0]-b[0], a[1]-b[1]];
}
function getSide(a, b) {
let cosine_sign = a[0]*b[1] - a[1]*b[0];
if (cosine_sign > 0) return 1;
if (cosine_sign < 0) return -1;
}
function pointInsidePolygon(x, y) {
face_uvs:
for (let vertex_uvs of vertex_uv_faces) {
let previous_side;
let i = 0;
vertices:
for (let a of vertex_uvs) {
let b = vertex_uvs[i+1] || vertex_uvs[0];
let affine_segment = vSub(b, a);
let affine_point = vSub([x, y], a);
let side = getSide(affine_segment, affine_point);
if (!side) continue face_uvs;
if (!previous_side) previous_side = side;
if (side !== previous_side) continue face_uvs;
i++;
}
return true;
}
return false;
}
for (let x = 0; x < (0 + width); x++) {
for (let y =0; y < (0 + height); y++) {
let inside = ( pointInsidePolygon(x+0.00001, y+0.00001)
|| pointInsidePolygon(x+0.99999, y+0.00001)
|| pointInsidePolygon(x+0.00001, y+0.99999)
|| pointInsidePolygon(x+0.99999, y+0.99999));
if (!inside) {
let px_rect = [[x, y], [x+0.99999, y+0.99999]]
faces:
for (let vertex_uvs of vertex_uv_faces) {
let i = 0;
for (let a of vertex_uvs) {
let b = vertex_uvs[i+1] || vertex_uvs[0];
if (pointInRectangle(a, ...px_rect)) {
inside = true; break faces;
}
if (lineIntersectsReactangle(a, b, ...px_rect)) {
inside = true; break faces;
}
i++;
}
}
}
if (inside) {
if (!matrix[x]) matrix[x] = {};
matrix[x][y] = true;
}
}
}
return matrix;
}
function drawTemplateRectangle(border_color, color, coords) {
if (typeof background_color === 'string') {
border_color = background_color
color = undefined
}
ctx.fillStyle = border_color
ctx.fillRect(
Math.floor(coords.x * res_multiple),
Math.floor(coords.y * res_multiple),
Math.ceil(coords.w * res_multiple),
Math.ceil(coords.h * res_multiple)
)
if (coords.w*res_multiple > 2 && coords.h*res_multiple > 2 && color) {
ctx.fillStyle = color
ctx.fillRect(
Math.floor(coords.x * res_multiple + 1),
Math.floor(coords.y * res_multiple + 1),
Math.ceil(coords.w * res_multiple - 2),
Math.ceil(coords.h * res_multiple - 2)
)
}
}
function drawTemplatePolygons(border_color, color, ftemp, pos) {
if (!pos.w || !pos.h) return;
if (typeof background_color === 'string') {
border_color = background_color
color = undefined
}
if (border_color) border_color = tinycolor(border_color).toRgb();
if (color) color = tinycolor(color).toRgb();
let R = res_multiple;
let matrix = ftemp.matrix;
Painter.scanCanvas(ctx, pos.x * R, pos.y * R, pos.w * R, pos.h * R, (x, y) => {
x -= pos.x*R;
y -= pos.y*R;
if (matrix[Math.floor(x / R)] && matrix[Math.floor(x / R)][Math.floor(y / R)]) {
if (
color &&
(matrix[Math.floor((x+1) / R)] && matrix[Math.floor((x+1) / R)][Math.floor(y / R) ]) &&
(matrix[Math.floor((x-1) / R)] && matrix[Math.floor((x-1) / R)][Math.floor(y / R) ]) &&
(matrix[Math.floor(x / R) ] && matrix[Math.floor(x / R) ][Math.floor((y+1) / R)]) &&
(matrix[Math.floor(x / R) ] && matrix[Math.floor(x / R) ][Math.floor((y-1) / R)])
) {
return [color.r, color.g, color.b, color.a * 255];
} else {
return [border_color.r, border_color.g, border_color.b, border_color.a * 255];
}
}
})
}
function drawCubeTexture(face, coords) {
if (!Format.single_texture) {
if (face.texture === undefined || face.texture === null) return false;
texture = face.getTexture()
} else {
texture = Texture.getDefault();
}
if (!texture || !texture.img) return false;
ctx.save()
var uv = face.uv.slice();
if (face.direction === 'up') {
uv = [uv[2], uv[3], uv[0], uv[1]]
} else if (face.direction === 'down') {
uv = [uv[2], uv[1], uv[0], uv[3]]
}
var src = getRectangle(uv[0], uv[1], uv[2], uv[3])
var flip = [
uv[0] > uv[2] ? -1 : 1,
uv[1] > uv[3] ? -1 : 1
]
if (flip[0] + flip[1] < 1) {
ctx.scale(flip[0], flip[1])
}
if (face.rotation) {
ctx.rotate(Math.degToRad(face.rotation))
let rot = face.rotation
if (rot <= 180) flip[1] *= -1;
if (rot >= 180) flip[0] *= -1;
while (rot > 0) {
[coords.x, coords.y] = [coords.y, coords.x];
[coords.w, coords.h] = [coords.h, coords.w];
rot -= 90;
}
}
ctx.imageSmoothingEnabled = false;
ctx.drawImage(
texture.img,
src.ax/Project.texture_width * texture.img.naturalWidth,
src.ay/Project.texture_height * texture.img.naturalHeight,
src.x /Project.texture_width * texture.img.naturalWidth,
src.y /Project.texture_height * texture.img.naturalHeight,
coords.x*res_multiple*flip[0],
coords.y*res_multiple*flip[1],
coords.w*res_multiple*flip[0],
coords.h*res_multiple*flip[1]
)
ctx.restore()
return true;
}
function drawMeshTexture(ftemp, coords) {
let i = 0;
for (let face of ftemp.faces) {
let texture;
if (!Format.single_texture) {
if (face.texture === undefined) return false;
texture = face.getTexture()
} else {
texture = Texture.getDefault();
}
if (!texture || !texture.img) return false;
ctx.save()
let target_uvs = ftemp.vertex_uvs[ftemp.keys[i]];
let R = res_multiple;
let min = [Infinity, Infinity];
let max = [0, 0];
let target_min = [Infinity, Infinity];
let target_max = [0, 0];
face.vertices.forEach(vkey => {
min[0] = Math.min(min[0], face.uv[vkey][0]);
min[1] = Math.min(min[1], face.uv[vkey][1]);
max[0] = Math.max(max[0], face.uv[vkey][0]);
max[1] = Math.max(max[1], face.uv[vkey][1]);
target_min[0] = Math.min(target_min[0], target_uvs[vkey][0]);
target_min[1] = Math.min(target_min[1], target_uvs[vkey][1]);
target_max[0] = Math.max(target_max[0], target_uvs[vkey][0]);
target_max[1] = Math.max(target_max[1], target_uvs[vkey][1]);
})
let a_old = face.uv[face.vertices[0]].slice();
let b_old = face.uv[face.vertices[1]].slice();
let a_new = target_uvs[face.vertices[0]].slice();
let b_new = target_uvs[face.vertices[1]].slice();
let rotation_old = Math.atan2(
b_old[1] - a_old[1],
b_old[0] - a_old[0],
)
let rotation_new = Math.atan2(
b_new[1] - a_new[1],
b_new[0] - a_new[0],
)
let rotation_difference = Math.radToDeg(rotation_new - rotation_old);
ctx.beginPath()
// Mask
for (let x in ftemp.matrix) {
x = parseInt(x);
for (let y in ftemp.matrix[x]) {
y = parseInt(y);
ctx.rect((coords.x + x)*R, (coords.y + y)*R, R, R);
}
}
ctx.closePath();
ctx.clip();
ctx.imageSmoothingEnabled = false;
let rotate = Math.round((((rotation_difference + 540) % 360) - 180) / 90) * 90;
if (rotate) {
let offset = [
coords.x*R + (target_min[0] + (target_max[0] - target_min[0])/2) * R,
coords.y*R + (target_min[1] + (target_max[1] - target_min[1])/2) * R,
]
ctx.translate(...offset);
ctx.rotate(Math.degToRad(Math.round(rotation_difference / 90) * 90));
ctx.translate(-offset[0], -offset[1]);
if (Math.abs(rotate) == 90) {
let target_size = [
Math.ceil((target_max[1] - target_min[1]) * R),
Math.ceil((target_max[0] - target_min[0]) * R),
]
let target_pos = [
coords.x*R + target_min[0] * R,
coords.y*R + target_min[1] * R,
];
target_pos[0] = target_pos[0] - target_size[0]/2 + target_size[1]/2;
target_pos[1] = target_pos[1] - target_size[1]/2 + target_size[0]/2;
ctx.drawImage(
texture.img,
min[0] / Project.texture_width * texture.img.naturalWidth,
min[1] / Project.texture_height * texture.img.naturalHeight,
Math.ceil((max[0] - min[0]) / Project.texture_width * texture.img.naturalWidth),
Math.ceil((max[1] - min[1]) / Project.texture_height * texture.img.naturalHeight),
...target_pos,
...target_size
)
}
} else {
ctx.drawImage(
texture.img,
min[0] / Project.texture_width * texture.img.naturalWidth,
min[1] / Project.texture_height * texture.img.naturalHeight,
Math.ceil((max[0] - min[0]) / Project.texture_width * texture.img.naturalWidth),
Math.ceil((max[1] - min[1]) / Project.texture_height * texture.img.naturalHeight),
coords.x*R + target_min[0] * R,
coords.y*R + target_min[1] * R,
Math.ceil((target_max[0] - target_min[0]) * R),
Math.ceil((target_max[1] - target_min[1]) * R),
)
}
ctx.restore()
i++;
}
return true;
}
//Drawing
face_list.forEach(function(ftemp) {
var pos = {
x: ftemp.posx,
y: ftemp.posy,
w: Math.ceil(ftemp.width * res_multiple) / res_multiple,
h: Math.ceil(ftemp.height * res_multiple) / res_multiple
}
var d = TextureGenerator.face_data[ftemp.face_key];
var flip_rotation = false;
if (ftemp.cube) {
// Cube
if (!ftemp.texture ||
!drawCubeTexture(ftemp.face, pos)
) {
drawTemplateRectangle(d.c1, d.c2, pos)
} else if (ftemp.cube) {
flip_rotation = ftemp.face.rotation % 180 != 0;
}
} else if (ftemp.mesh) {
// Mesh
if (!ftemp.texture ||
!drawMeshTexture(ftemp, pos)
) {
drawTemplatePolygons(d.c1, d.c2, ftemp, pos)
}
}
if (options.rearrange_uv) {
if (ftemp.cube) {
ftemp.face.extend({
rotation: 0,
uv: flip_rotation ? [pos.y, pos.x] : [pos.x, pos.y]
})
ftemp.face.uv_size = flip_rotation ? [pos.h, pos.w] : [pos.w, pos.h];
if (ftemp.face_key == 'up') {
[ftemp.face.uv[2], ftemp.face.uv[0]] = [ftemp.face.uv[0], ftemp.face.uv[2]];
[ftemp.face.uv[3], ftemp.face.uv[1]] = [ftemp.face.uv[1], ftemp.face.uv[3]];
}
if (ftemp.face_key == 'down') {
[ftemp.face.uv[2], ftemp.face.uv[0]] = [ftemp.face.uv[0], ftemp.face.uv[2]];
}
} else {
ftemp.faces.forEach((face, i) => {
let fkey = ftemp.keys[i];
face.vertices.forEach(vkey => {
if (!face.uv[vkey]) face.uv[vkey] = [];
face.uv[vkey][0] = ftemp.vertex_uvs[fkey][vkey][0] + ftemp.posx;
face.uv[vkey][1] = ftemp.vertex_uvs[fkey][vkey][1] + ftemp.posy;
})
})
}
}
})
var dataUrl = canvas.toDataURL()
var texture = typeof makeTexture == 'function' ? makeTexture(dataUrl) : makeTexture;
if (makeTexture instanceof Texture) {
makeTexture.updateSource(dataUrl);
}
let affected_elements = TextureGenerator.changeProjectResolution(new_resolution[0], new_resolution[1], makeTexture instanceof Texture ? makeTexture : undefined);
if (texture) {
element_list.forEach(function(element) {
if (!Format.single_texture) {
for (var key in element.faces) {
if (element.faces[key].texture !== null) {
element.faces[key].texture = texture.uuid;
}
}
element.preview_controller.updateFaces(element);
}
element.preview_controller.updateUV(element);
if (typeof element.autouv !== 'undefined') {
element.autouv = 0;
}
})
}
updateSelection()
Undo.finishEdit(makeTexture instanceof Texture ? 'Append to template' : 'Create template', {
textures: [texture],
bitmap: true,
elements: [...element_list, ...affected_elements],
selected_texture: true,
uv_only: true,
uv_mode: true
})
},
generateColorMapTemplate(options, cb) {
var background_color = options.color;
var texture = options.texture;
var new_resolution = [];
var face_list = [];
var element_list = (Format.single_texture ? Outliner.elements : Outliner.selected).filter(el => {
return (el instanceof Cube || el instanceof Mesh) && el.visibility;
});
Undo.initEdit({
textures: [],
elements: element_list,
uv_only: true,
selected_texture: true,
uv_mode: true
})
element_list.forEach(element => {
for (let fkey in element.faces) {
let face = element.faces[fkey];
if (element instanceof Mesh && face.vertices.length <= 2) continue;
if (element instanceof Cube && face.texture === null) continue;
face_list.push({element, fkey, face});
}
})
if (face_list.length == 0) {
Blockbench.showMessage('message.no_valid_elements', 'center')
return;
}
var max_size = Math.ceil(Math.sqrt(face_list.length));
if (options.power) {
max_size = Math.getNextPower(max_size, 16);
} else {
max_size = Math.ceil(max_size/16)*16;
}
new_resolution = [max_size, max_size];
if (background_color.getAlpha() != 0) {
background_color = background_color.toRgbString()
}
var canvas = document.createElement('canvas')
canvas.width = new_resolution[0];
canvas.height = new_resolution[1];
var ctx = canvas.getContext('2d');
ctx.fillStyle = typeof background_color == 'string' ? background_color : 'white';
ctx.imageSmoothingEnabled = false;
let texture_ctxs = {};
//Drawing
face_list.forEach(({face}, i) => {
let x = i % max_size;
let y = Math.floor(i / max_size);
let texture;
if (!Format.single_texture) {
if (face.texture !== undefined && face.texture !== null) {
texture = face.getTexture()
}
} else {
texture = Texture.getDefault();
}
if (texture && texture.img) {
if (!texture_ctxs[texture.uuid]) {
texture_ctxs[texture.uuid] = new CanvasFrame(texture.img).ctx;
}
let color = Painter.getPixelColor(
texture_ctxs[texture.uuid],
Math.floor((face instanceof CubeFace ? face.uv : face.uv[face.vertices[0]])[0] / Project.texture_width * texture.width),
Math.floor((face instanceof CubeFace ? face.uv : face.uv[face.vertices[0]])[1] / Project.texture_height * texture.height),
);
ctx.fillStyle = color ? color.toHexString() : 'white';
} else {
ctx.fillStyle = typeof background_color == 'string' ? background_color : 'white';
}
ctx.fillRect(x, y, 1, 1);
if (face instanceof CubeFace) {
face.uv = [x+0.25, y+0.25, x+0.75, y+0.75];
} else if (face instanceof MeshFace) {
let vertices = face.getSortedVertices();
face.uv[vertices[0]] = [x+0.75, y+0.25];
face.uv[vertices[1]] = [x+0.25, y+0.25];
face.uv[vertices[2]] = [x+0.25, y+0.75];
if (vertices[3]) face.uv[vertices[3]] = [x+0.75, y+0.75];
}
})
var dataUrl = canvas.toDataURL()
var texture = cb(dataUrl)
let affected_elements = TextureGenerator.changeProjectResolution(new_resolution[0], new_resolution[1]);
if (texture) {
face_list.forEach(({face, fkey}, i) => {
face.texture = texture.uuid;
})
element_list.forEach(function(element) {
element.preview_controller.updateFaces(element);
element.preview_controller.updateUV(element);
if (typeof element.autouv !== 'undefined') {
element.autouv = 0;
}
})
}
updateSelection()
Undo.finishEdit('Create template', {
textures: [texture],
bitmap: true,
elements: [...element_list, ...affected_elements],
selected_texture: true,
uv_only: true,
uv_mode: true
})
},
//Misc
changeProjectResolution(width, height, texture) {
let factor_x = width / Project.texture_width;
let factor_y = height / Project.texture_height;
Project.texture_width = width;
Project.texture_height = height;
let changed_elements = [];
if (!Project.box_uv && !Format.single_texture && (factor_x !== 1 || factor_y !== 1)) {
changed_elements = Outliner.elements.filter(el => el.faces && !el.selected);
Undo.current_save.addElements(changed_elements, {uv_only: true});
changed_elements.forEach(element => {
if (element instanceof Cube) {
for (var key in element.faces) {
let face = element.faces[key];
if (texture && face.getTexture() == texture) continue;
if (face.texture) {
face.uv[0] *= factor_x
face.uv[1] *= factor_y;
face.uv[2] *= factor_x;
face.uv[3] *= factor_y;
}
}
} else if (element instanceof Mesh) {
for (var fkey in element.faces) {
let face = element.faces[fkey];
if (texture && face.getTexture() == texture) continue;
for (let vkey in face.uv) {
face.uv[vkey][0] *= factor_x;
face.uv[vkey][1] *= factor_y;
}
}
}
element.preview_controller.updateUV(element);
})
}
return changed_elements;
}
}