Implement new start screen + new start screen tech

Implement mesh painting grid, close #1014
Implement mesh mirror painting, close #1013
Fix issue displaying mesh without faces
Implement mesh UV mirroring
This commit is contained in:
JannisX11 2021-09-21 21:13:24 +02:00
parent 6b5a5f5cc0
commit 4c9d9acd5d
12 changed files with 528 additions and 232 deletions

View File

@ -4,23 +4,32 @@
"text_color": "var(--color-bright_ui_text)",
"graphic": {
"type": "image",
"source": "https://web.blockbench.net/content/splash_art.png?39",
"width": 630,
"height": 420
"source": "./content/splash_art.png?39",
"width": 1000,
"aspect_ratio": "16/9",
"description": "Splash Art Placeholder by [DerBauersSohn](https://twitter.com/DerBauersSohn__)"
},
"layout": "vertical",
"text": [
{"text": "## Blockbench 3.9"},
{"text": "### Highlights of this update:"},
{"type": "list", "list": [
"Load, export and share keymaps and settings!",
"Travel through time using the new Edit History!",
"Hold Alt to resize cubes in both directions",
"Get creative with the new gradient tool",
"Transform spaces are now available in animation mode",
"Dozens of quality-of-life features and improvements!"
]},
{"text": "Make sure to read the [full changelog](https://github.com/JannisX11/blockbench/releases/tag/v3.9.0)!"},
{"text": "Splash Art by [LorenOLoren](https://twitter.com/LorenOLoren1) and [Dankbarkeit](https://twitter.com/Dxnkbarkeit)"}
{"text": "Welcome to Blockbench 4.0, the Mesh Update!", "type": "h1"},
{"text": "Check out the [full changelog](https://github.com/JannisX11/blockbench/releases/tag/v4.0.0)!"}
],
"features": [
{
"image": "./content/splash_art.png",
"title": "Poly Mesh Editing!",
"text": "Blockbench now supports poly mesh editing, which let's you create any shapes in generic models."
},
{
"image": "./content/splash_art.png",
"title": "Tabs!",
"text": "Open multiple models at the same time and easily switch between them using the new project tabs!"
},
{
"image": "./content/splash_art.png",
"title": "Theme Browser",
"text": "You can now discover and switch to new themes directly inside Blockbench. Also, the theme menu and the other settings menus have been re-designed and cleaned up!"
}
]
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 668 KiB

View File

@ -710,6 +710,9 @@
display: flex;
position: relative;
}
#start_screen > content > section.vertical {
flex-direction: column;
}
#start_screen section div.start_screen_left, #start_screen section div.start_screen_right {
display: block;
padding: 24px;
@ -723,8 +726,17 @@
grid-gap: 5px;
}
#start_screen div.start_screen_left {
max-width: 100%;
flex-grow: 0;
}
#start_screen div.start_screen_left.graphic {
background-size: cover;
position: relative;
padding: 0;
}
#start_screen div.start_screen_left.graphic p {
position: absolute;
font-size: 0.96em;
}
#start_screen div.start_screen_left i.graphic_icon {
font-size: 40px;
@ -735,6 +747,10 @@
flex-grow: 1;
width: 62%;
}
#start_screen section.vertical div.start_screen_right {
width: auto;
text-align: center;
}
#start_screen i.start_screen_close_button {
position: absolute;
top: 8px;
@ -744,6 +760,33 @@
#start_screen i.start_screen_close_button:not(:hover) {
opacity: 0.8;
}
#start_screen section.vertical.bright_ui .start_screen_right {
box-shadow: 0 0 14px #00103030;
}
#start_screen .start_screen_features {
display: flex;
flex-wrap: wrap;
box-sizing: border-box;
padding: 12px;
background: #00004006;
}
#start_screen .start_screen_features > li {
width: 30%;
flex-grow: 1;
min-width: 250px;
box-sizing: border-box;
justify-content: space-between;
text-align: center;
margin: 7px;
padding: 7px;
}
#start_screen .start_screen_features > li > * {
max-width: 100%;
font-weight: 300;
}
#start_screen .start_screen_features > li > h3 {
font-size: 1.4em;
}
@media (max-device-width: 640px) {
#start_screen {
width: 100%;

View File

@ -1564,6 +1564,7 @@ const BARS = {
for (let key in mesh.faces) {
let face = mesh.faces[key];
if (!face.vertices.includes(vertex_key)) continue;
if (face.vertices.length > 2) {
face.vertices.remove(vertex_key);
delete face.uv[vertex_key];

View File

@ -559,7 +559,10 @@ function addStartScreenSection(id, data) {
left.css('background-image', `url('${data.graphic.source}')`)
}
if (data.graphic.width) {
left.css('width', data.graphic.width+'px').css('flex-shrink', '0');
left.css('width', data.graphic.width+'px');
}
if (data.graphic.width && data.text) {
left.css('flex-shrink', '0');
}
if (data.graphic.width && data.graphic.height && Blockbench.isMobile) {
left.css('height', '0')
@ -567,7 +570,17 @@ function addStartScreenSection(id, data) {
.css('padding-bottom', (data.graphic.height/data.graphic.width*100)+'%')
} else {
if (data.graphic.height) left.css('height', data.graphic.height+'px');
if (data.graphic.width && !data.graphic.height) left.css('height', data.graphic.width+'px');
if (data.graphic.width && !data.graphic.height && !data.graphic.aspect_ratio) left.css('height', data.graphic.width+'px');
if (data.graphic.aspect_ratio) left.css('aspect-ratio', data.graphic.aspect_ratio);
}
if (data.graphic.description) {
let content = $(marked(data.graphic.description));
content.css({
'bottom': '15px',
'right': '15px',
'color': data.graphic.description_color || '#ffffff',
});
left.append(content);
}
}
if (data.text instanceof Array) {
@ -576,8 +589,8 @@ function addStartScreenSection(id, data) {
data.text.forEach(line => {
var content = line.text ? marked(tl(line.text)) : '';
switch (line.type) {
case 'h1': var tag = 'h3'; break;
case 'h2': var tag = 'h4'; break;
case 'h1': var tag = 'h2'; break;
case 'h2': var tag = 'h3'; break;
case 'list':
var tag = 'ul class="list_style"';
line.list.forEach(string => {
@ -594,6 +607,24 @@ function addStartScreenSection(id, data) {
right.append(l);
})
}
if (data.layout == 'vertical') {
obj.addClass('vertical');
}
if (data.features instanceof Array) {
let features_section = document.createElement('ul');
features_section.className = 'start_screen_features'
data.features.forEach(feature => {
let li = document.createElement('li');
let img = new Image(); img.src = feature.image;
let title = document.createElement('h3'); title.textContent = feature.title;
let text = document.createElement('p'); text.textContent = feature.text;
li.append(img, title, text);
features_section.append(li);
})
obj.append(features_section);
}
if (data.closable !== false) {
obj.append(`<i class="material-icons start_screen_close_button">clear</i>`);
obj.find('i.start_screen_close_button').click((e) => {
@ -625,6 +656,10 @@ function addStartScreenSection(id, data) {
(function() {
/*$.getJSON('./content/news.json').then(data => {
addStartScreenSection('new_version', data.new_version)
})*/
var news_call = $.getJSON('https://web.blockbench.net/content/news.json')
Promise.all([news_call, documentReady]).then((data) => {
if (!data || !data[0]) return;

View File

@ -228,6 +228,7 @@ class ModelProject {
Blockbench.dispatchEvent('select_project', {project: this});
if (Preview.selected) Preview.selected.occupyTransformer();
setProjectTitle(this.name);
setStartScreen(!Project);
updateInterface();

View File

@ -183,8 +183,8 @@ BARS.defineActions(function() {
if (Modes.previous_id == 'animate') {
Animator.preview();
}
Cube.all.forEach(cube => {
Canvas.buildGridBox(cube)
Outliner.elements.forEach(cube => {
if (cube.preview_controller.updatePaintingGrid) cube.preview_controller.updatePaintingGrid(cube);
})
$('#main_colorpicker').spectrum('set', ColorPanel.vue._data.main_color);
BarItems.slider_color_h.update();
@ -196,8 +196,8 @@ BARS.defineActions(function() {
},
onUnselect: () => {
Canvas.updateAllBones()
Cube.all.forEach(cube => {
Canvas.buildGridBox(cube)
Outliner.elements.forEach(cube => {
if (cube.preview_controller.updatePaintingGrid) cube.preview_controller.updatePaintingGrid(cube);
})
UVEditor.vue.setMode('uv');
three_grid.visible = true;

View File

@ -829,7 +829,7 @@ new NodePreviewController(Cube, {
}
if (Modes.paint) {
Canvas.buildGridBox(element);
element.preview_controller.updatePaintingGrid(element);
}
},
updateGeometry(element) {
@ -1035,6 +1035,119 @@ new NodePreviewController(Cube, {
mesh.geometry.attributes.highlight.array.set(Array(mesh.geometry.attributes.highlight.count).fill(highlighted));
mesh.geometry.attributes.highlight.needsUpdate = true;
}
},
updatePaintingGrid(cube) {
var mesh = cube.mesh;
if (mesh === undefined) return;
mesh.remove(mesh.grid_box);
if (cube.visibility == false) return;
if (!Modes.paint || !settings.painting_grid.value) return;
var from = cube.from.slice();
var to = cube.to.slice();
if (cube.inflate) {
from[0] -= cube.inflate; from[1] -= cube.inflate; from[2] -= cube.inflate;
to[0] += cube.inflate; to[1] += cube.inflate; to[2] += cube.inflate;
}
var vertices = [];
var epsilon = 0.0001
function getVector2(arr, axis) {
switch (axis) {
case 0: return [arr[1], arr[2]]; break;
case 1: return [arr[0], arr[2]]; break;
case 2: return [arr[0], arr[1]]; break;
}
}
function addVector(u, v, axis, w) {
switch (axis) {
case 0: vertices.push(w, u, v); break;
case 1: vertices.push(u, w, v); break;
case 2: vertices.push(u, v, w); break;
}
}
function addFace(name, uv_offset, axis, side) {
var start = getVector2(from, axis)
var end = getVector2(to, axis)
var face = cube.faces[name];
var texture = face.getTexture();
if (texture === null) return;
var px_x = texture ? Project.texture_width / texture.width : 1;
var px_y = texture ? Project.texture_height / texture.height : 1;
var uv_size = [
Math.abs(face.uv_size[0]),
Math.abs(face.uv_size[1])
]
uv_offset = [
uv_offset[0] == true
? (face.uv_size[0] > 0 ? (px_x-face.uv[2]) : ( face.uv[2]))
: (face.uv_size[0] > 0 ? ( face.uv[0]) : (px_x-face.uv[0])),
uv_offset[1] == true
? (face.uv_size[1] > 0 ? (px_y-face.uv[3]) : ( face.uv[3]))
: (face.uv_size[1] > 0 ? ( face.uv[1]) : (px_y-face.uv[1]))
]
uv_offset[0] = uv_offset[0] % px_x;
uv_offset[1] = uv_offset[1] % px_y;
if ((face.rotation % 180 == 90) != (axis == 0)) {
uv_size.reverse();
uv_offset.reverse();
}
var w = side == 0 ? from[axis] : to[axis]
//Columns
var width = end[0]-start[0];
var step = Math.abs( width / uv_size[0] );
if (texture) step *= Project.texture_width / texture.width;
if (step < epsilon) step = epsilon;
for (var col = start[0] - uv_offset[0]; col <= end[0]; col += step) {
if (col >= start[0]) {
addVector(col, start[1], axis, w);
addVector(col, end[1], axis, w);
}
}
//lines
var height = end[1]-start[1];
var step = Math.abs( height / uv_size[1] );
let tex_height = texture.frameCount ? (texture.height / texture.frameCount) : texture.height;
if (texture) step *= Project.texture_height / tex_height;
if (step < epsilon) step = epsilon;
for (var line = start[1] - uv_offset[1]; line <= end[1]; line += step) {
if (line >= start[1]) {
addVector(start[0], line, axis, w);
addVector(end[0], line, axis, w);
}
}
}
addFace('north', [true, true], 2, 0);
addFace('south', [false, true], 2, 1);
addFace('west', [false, true], 0, 0);
addFace('east', [true, true], 0, 1);
addFace('down', [false, true], 1, 0);
addFace('up', [false, false], 1, 1);
var geometry = new THREE.BufferGeometry();
geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) );
let box = new THREE.LineSegments(geometry, new THREE.LineBasicMaterial({color: gizmo_colors.grid}));
box.geometry.translate(-cube.origin[0], -cube.origin[1], -cube.origin[2]);
box.no_export = true;
box.name = cube.uuid+'_grid_box';
box.renderOrder = 2;
box.frustumCulled = false;
mesh.grid_box = box;
mesh.add(box);
}
})

View File

@ -116,6 +116,48 @@ class MeshFace extends Face {
if (this.mesh.faces[fkey] == this) return fkey;
}
}
UVToLocal(uv) {
let p0 = this.uv[this.vertices[0]];
let p1 = this.uv[this.vertices[1]];
let p2 = this.uv[this.vertices[2]];
let vertexa = this.mesh.vertices[this.vertices[0]];
let vertexb = this.mesh.vertices[this.vertices[1]];
let vertexc = this.mesh.vertices[this.vertices[2]];
let b0 = (p1[0] - p0[0]) * (p2[1] - p0[1]) - (p2[0] - p0[0]) * (p1[1] - p0[1])
let b1 = ((p1[0] - uv[0]) * (p2[1] - uv[1]) - (p2[0] - uv[0]) * (p1[1] - uv[1])) / b0
let b2 = ((p2[0] - uv[0]) * (p0[1] - uv[1]) - (p0[0] - uv[0]) * (p2[1] - uv[1])) / b0
let b3 = ((p0[0] - uv[0]) * (p1[1] - uv[1]) - (p1[0] - uv[0]) * (p0[1] - uv[1])) / b0
let local_space = new THREE.Vector3(
vertexa[0] * b1 + vertexb[0] * b2 + vertexc[0] * b3,
vertexa[1] * b1 + vertexb[1] * b2 + vertexc[1] * b3,
vertexa[2] * b1 + vertexb[2] * b2 + vertexc[2] * b3,
)
return local_space;
}
localToUV(vector) {
let va = new THREE.Vector3().fromArray(this.mesh.vertices[this.vertices[0]]);
let vb = new THREE.Vector3().fromArray(this.mesh.vertices[this.vertices[1]]);
let vc = new THREE.Vector3().fromArray(this.mesh.vertices[this.vertices[2]]);
let uva = new THREE.Vector2().fromArray(this.uv[this.vertices[0]]);
let uvb = new THREE.Vector2().fromArray(this.uv[this.vertices[1]]);
let uvc = new THREE.Vector2().fromArray(this.uv[this.vertices[2]]);
let uv = THREE.Triangle.getUV(vector, va, vb, vc, uva, uvb, uvc, new THREE.Vector2());
return uv.toArray();
}
getCenter() {
let center = [0, 0, 0];
this.vertices.forEach(vkey => {
let vertex = this.mesh.vertices[vkey];
center.V3_add(vertex);
})
center.V3_divide(this.vertices.length);
return center;
}
}
new Property(MeshFace, 'array', 'vertices', {default: 0});
@ -650,6 +692,10 @@ new NodePreviewController(Mesh, {
mesh.vertex_points.geometry.computeBoundingSphere();
mesh.outline.geometry.computeBoundingSphere();
updateCubeHighlights()
if (Modes.paint) {
Mesh.preview_controller.updatePaintingGrid(element);
}
},
updateFaces(element) {
let {mesh} = element;
@ -718,6 +764,7 @@ new NodePreviewController(Mesh, {
}
mesh.material = materials;
if (!mesh.material) mesh.material = Canvas.transparentMaterial;
}
},
updateUV(element, animation = true) {
@ -765,11 +812,11 @@ new NodePreviewController(Mesh, {
}
let line_colors = [];
mesh.outline.vertex_order.forEach(key => {
mesh.outline.vertex_order.forEach((key, i) => {
let color;
if (!Modes.edit || BarItems.selection_mode.value == 'object') {
color = gizmo_colors.outline;
} else if (selected_vertices.includes(key)) {
} else if (selected_vertices.includes(key) && selected_vertices.includes(mesh.outline.vertex_order[i + ((i%2) ? -1 : 1) ])) {
color = white;
} else {
color = gizmo_colors.grid;
@ -810,6 +857,71 @@ new NodePreviewController(Mesh, {
mesh.geometry.attributes.highlight.array.set(array);
mesh.geometry.attributes.highlight.needsUpdate = true;
},
updatePaintingGrid(element) {
var mesh = element.mesh;
if (mesh === undefined) return;
mesh.remove(mesh.grid_box);
if (element.visibility == false) return;
if (!Modes.paint || !settings.painting_grid.value) return;
var positions = [];
for (let fkey in element.faces) {
let face = element.faces[fkey];
if (face.vertices <= 2) continue;
let offset = face.getNormal(true).V3_multiply(0.01);
let x_memory = {};
let y_memory = {};
let texture = face.getTexture();
var psize_x = texture ? Project.texture_width / texture.width : 1;
var psize_y = texture ? Project.texture_height / texture.height : 1;
let vertices = face.getSortedVertices();
vertices.forEach((vkey1, i) => {
let vkey2 = vertices[i+1] || vertices[0];
let uv1 = face.uv[vkey1].slice();
let uv2 = face.uv[vkey2].slice();
if (uv1[0] > uv2[0]) [uv1[0], uv2[0]] = [uv2[0], uv1[0]];
if (uv1[1] > uv2[1]) [uv1[1], uv2[1]] = [uv2[1], uv1[1]];
for (let x = Math.ceil(uv1[0] / psize_x) * psize_x; x < uv2[0]; x += psize_x) {
if (!x_memory[x]) x_memory[x] = [];
let y = uv1[1] + (uv2[1] - uv1[1]) * Math.lerp(uv1[0], uv2[0], x);
x_memory[x].push(face.UVToLocal([x, y]).toArray().V3_add(offset));
}
for (let y = Math.ceil(uv1[1] / psize_y) * psize_y; y < uv2[1]; y += psize_y) {
if (!y_memory[y]) y_memory[y] = [];
let x = uv1[0] + (uv2[0] - uv1[0]) * Math.lerp(uv1[1], uv2[1], y);
y_memory[y].push(face.UVToLocal([x, y]).toArray().V3_add(offset));
}
})
for (let key in x_memory) {
let points = x_memory[key];
if (points.length == 2) {
positions.push(...points[0], ...points[1]);
}
}
for (let key in y_memory) {
let points = y_memory[key];
if (points.length == 2) {
positions.push(...points[0], ...points[1]);
}
}
}
var geometry = new THREE.BufferGeometry();
geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) );
let box = new THREE.LineSegments(geometry, new THREE.LineBasicMaterial({color: gizmo_colors.grid}));
box.no_export = true;
box.name = element.uuid+'_grid_box';
box.renderOrder = 2;
box.frustumCulled = false;
mesh.grid_box = box;
mesh.add(box);
}
})
@ -830,9 +942,9 @@ BARS.defineActions(function() {
}},
diameter: {label: 'dialog.add_primitive.diameter', type: 'number', value: 16},
height: {label: 'dialog.add_primitive.height', type: 'number', value: 8, condition: ({shape}) => ['cylinder', 'cone', 'cube', 'pyramid', 'tube'].includes(shape)},
sides: {label: 'dialog.add_primitive.sides', type: 'number', value: 12, condition: ({shape}) => ['cylinder', 'cone', 'circle', 'torus', 'sphere', 'tube'].includes(shape)},
sides: {label: 'dialog.add_primitive.sides', type: 'number', value: 12, min: 3, max: 48, condition: ({shape}) => ['cylinder', 'cone', 'circle', 'torus', 'sphere', 'tube'].includes(shape)},
minor_diameter: {label: 'dialog.add_primitive.minor_diameter', type: 'number', value: 4, condition: ({shape}) => ['torus', 'tube'].includes(shape)},
minor_sides: {label: 'dialog.add_primitive.minor_sides', type: 'number', value: 8, condition: ({shape}) => ['torus'].includes(shape)},
minor_sides: {label: 'dialog.add_primitive.minor_sides', type: 'number', value: 8, min: 2, max: 32, condition: ({shape}) => ['torus'].includes(shape)},
},
onConfirm(result) {
let elements = [];

View File

@ -1064,127 +1064,11 @@ const Canvas = {
vertex_uvs.array.set(arr[2], index*8 + 4); //0,0
vertex_uvs.array.set(arr[3], index*8 + 6); //1,0
},
buildGridBox(cube) {
var mesh = cube.mesh;
if (mesh === undefined) return;
mesh.remove(mesh.grid_box);
if (cube.visibility == false) return;
if (!Modes.paint || !settings.painting_grid.value) return;
var box = Canvas.getPaintingGrid(cube);
box.name = cube.uuid+'_grid_box';
box.renderOrder = 2;
box.frustumCulled = false;
mesh.grid_box = box;
mesh.add(box);
},
getPaintingGrid(cube) {
var from = cube.from.slice();
var to = cube.to.slice();
if (cube.inflate) {
from[0] -= cube.inflate; from[1] -= cube.inflate; from[2] -= cube.inflate;
to[0] += cube.inflate; to[1] += cube.inflate; to[2] += cube.inflate;
}
var vertices = [];
var epsilon = 0.0001
function getVector2(arr, axis) {
switch (axis) {
case 0: return [arr[1], arr[2]]; break;
case 1: return [arr[0], arr[2]]; break;
case 2: return [arr[0], arr[1]]; break;
}
}
function addVector(u, v, axis, w) {
switch (axis) {
case 0: vertices.push(w, u, v); break;
case 1: vertices.push(u, w, v); break;
case 2: vertices.push(u, v, w); break;
}
}
function addFace(name, uv_offset, axis, side) {
var start = getVector2(from, axis)
var end = getVector2(to, axis)
var face = cube.faces[name];
var texture = face.getTexture();
if (texture === null) return;
var px_x = texture ? Project.texture_width / texture.width : 1;
var px_y = texture ? Project.texture_height / texture.height : 1;
var uv_size = [
Math.abs(face.uv_size[0]),
Math.abs(face.uv_size[1])
]
uv_offset = [
uv_offset[0] == true
? (face.uv_size[0] > 0 ? (px_x-face.uv[2]) : ( face.uv[2]))
: (face.uv_size[0] > 0 ? ( face.uv[0]) : (px_x-face.uv[0])),
uv_offset[1] == true
? (face.uv_size[1] > 0 ? (px_y-face.uv[3]) : ( face.uv[3]))
: (face.uv_size[1] > 0 ? ( face.uv[1]) : (px_y-face.uv[1]))
]
uv_offset[0] = uv_offset[0] % px_x;
uv_offset[1] = uv_offset[1] % px_y;
if ((face.rotation % 180 == 90) != (axis == 0)) {
uv_size.reverse();
uv_offset.reverse();
}
var w = side == 0 ? from[axis] : to[axis]
//Columns
var width = end[0]-start[0];
var step = Math.abs( width / uv_size[0] );
if (texture) step *= Project.texture_width / texture.width;
if (step < epsilon) step = epsilon;
for (var col = start[0] - uv_offset[0]; col <= end[0]; col += step) {
if (col >= start[0]) {
addVector(col, start[1], axis, w);
addVector(col, end[1], axis, w);
}
}
//lines
var height = end[1]-start[1];
var step = Math.abs( height / uv_size[1] );
let tex_height = texture.frameCount ? (texture.height / texture.frameCount) : texture.height;
if (texture) step *= Project.texture_height / tex_height;
if (step < epsilon) step = epsilon;
for (var line = start[1] - uv_offset[1]; line <= end[1]; line += step) {
if (line >= start[1]) {
addVector(start[0], line, axis, w);
addVector(end[0], line, axis, w);
}
}
}
addFace('north', [true, true], 2, 0);
addFace('south', [false, true], 2, 1);
addFace('west', [false, true], 0, 0);
addFace('east', [true, true], 0, 1);
addFace('down', [false, true], 1, 0);
addFace('up', [false, false], 1, 1);
var geometry = new THREE.BufferGeometry();
geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) );
var lines = new THREE.LineSegments(geometry, new THREE.LineBasicMaterial({color: gizmo_colors.grid}));
lines.geometry.translate(-cube.origin[0], -cube.origin[1], -cube.origin[2]);
lines.no_export = true;
return lines;
},
updatePaintingGrid() {
Cube.all.forEach(cube => {
Canvas.buildGridBox(cube)
Outliner.elements.forEach(element => {
if (element.preview_controller.updatePaintingGrid) {
element.preview_controller.updatePaintingGrid(element);
}
})
},

View File

@ -456,8 +456,9 @@ const Painter = {
},
runMirrorBrush(texture, x, y, event, uvTag) {
if (uvTag && Painter.current.element) {
let mirror_cube = Painter.getMirrorCube(Painter.current.element);
if (mirror_cube) {
let mirror_element = Painter.getMirrorCube(Painter.current.element);
let even_brush_size = BarItems.slider_brush_size.get()%2 == 0 && Toolbox.selected.brushTool;
if (mirror_element instanceof Cube) {
let uvFactorX = 1 / Project.texture_width * texture.img.naturalWidth;
let uvFactorY = 1 / Project.texture_height * texture.img.naturalHeight;
@ -465,7 +466,7 @@ const Painter = {
let face = Painter.current.face;
let side_face = (face === 'west' || face === 'east')
if (side_face) face = CubeFace.opposite[face];
face = mirror_cube.faces[face];
face = mirror_element.faces[face];
if (side_face &&
uvTag[1] === face.uv[1] && uvTag[3] === face.uv[3] &&
@ -481,7 +482,7 @@ const Painter = {
//calculate new point
if (face.uv[0] > face.uv[0+2] == uvTag[0] > uvTag[0+2]) {
point_on_uv[0] = Math.max(face.uv[0], face.uv[0+2]) * uvFactorX - point_on_uv[0] - 1;
if (BarItems.slider_brush_size.get()%2 == 0 && Toolbox.selected.brushTool) point_on_uv[0] += 1
if (even_brush_size) point_on_uv[0] += 1
} else {
point_on_uv[0] = Math.min(face.uv[0], face.uv[0+2]) * uvFactorX + point_on_uv[0];
}
@ -492,9 +493,50 @@ const Painter = {
}
let cube = Painter.current.element;
Painter.current.element = mirror_cube;
Painter.current.element = mirror_element;
Painter.useBrushlike(texture, ...point_on_uv, event, face.uv, true, true);
Painter.current.element = cube;
} else if (mirror_element instanceof Mesh) {
let mesh = mirror_element;
let clicked_face = mesh.faces[Painter.current.face];
let normal = clicked_face.getNormal(true);
let center = clicked_face.getCenter();
let e = 0.01;
let face;
for (let fkey in mesh.faces) {
let normal2 = mesh.faces[fkey].getNormal(true);
let center2 = mesh.faces[fkey].getCenter();
if (
Math.epsilon(normal[0], -normal2[0], e) && Math.epsilon(normal[1], normal2[1], e) && Math.epsilon(normal[2], normal2[2], e) &&
Math.epsilon(center[0], -center2[0], e) && Math.epsilon(center[1], center2[1], e) && Math.epsilon(center[2], center2[2], e)
) {
face = mesh.faces[fkey];
}
}
if (!face) return;
if (!even_brush_size) {
x += 0.5; y += 0.5;
}
let world_coord = mesh.mesh.localToWorld(clicked_face.UVToLocal([x, y]));
world_coord.x *= -1;
mesh.mesh.worldToLocal(world_coord);
let point_on_uv = face.localToUV(world_coord);
if (even_brush_size) {
point_on_uv = point_on_uv.map(v => Math.round(v))
} else {
point_on_uv = point_on_uv.map(v => Math.floor(v))
}
console.log([x, y], point_on_uv)
let old_mesh = Painter.current.element;
Painter.current.element = mesh;
Painter.useBrushlike(texture, ...point_on_uv, event, face.uv, true, true);
Painter.current.element = old_mesh;
}
}
},
@ -755,24 +797,36 @@ const Painter = {
added.a = original_a
return mix;
},
getMirrorCube(cube) {
getMirrorCube(element) {
let center = Format.centered_grid ? 0 : 8;
let e = 0.002
if (cube.from[0]-center === center-cube.to[0] && !cube.rotation[1] && !cube.rotation[2]) {
return cube;
} else {
for (var cube2 of Cube.all) {
if (
cube.inflate === cube2.inflate &&
Math.epsilon(cube.from[2], cube2.from[2], e) && Math.epsilon(cube.to[2], cube2.to[2], e) &&
Math.epsilon(cube.from[1], cube2.from[1], e) && Math.epsilon(cube.to[1], cube2.to[1], e) &&
Math.epsilon(cube.size(0), cube2.size(0), e) && Math.epsilon(cube.to[0]-center, center-cube2.from[0], e)
) {
return cube2;
if (element instanceof Cube) {
if (element.from[0]-center === center-element.to[0] && !element.rotation[1] && !element.rotation[2]) {
return element;
} else {
for (var element2 of Cube.all) {
if (
element.inflate === element2.inflate &&
Math.epsilon(element.from[2], element2.from[2], e) && Math.epsilon(element.to[2], element2.to[2], e) &&
Math.epsilon(element.from[1], element2.from[1], e) && Math.epsilon(element.to[1], element2.to[1], e) &&
Math.epsilon(element.size(0), element2.size(0), e) && Math.epsilon(element.to[0]-center, center-element2.from[0], e)
) {
return element2;
}
}
}
return false;
} else if (element instanceof Mesh) {
if (element instanceof Mesh && element.origin[0] === center && !element.rotation[1] && !element.rotation[2]) {
return element;
} else {
for (var element2 of Mesh.all) {
if (Object.keys(element.vertices).length !== Object.keys(element2.vertices).length) continue;
return element2;
}
}
return element;
}
return false;
},
updateNslideValues() {
BarItems.slider_brush_size.update()

View File

@ -486,6 +486,9 @@ const UVEditor = {
i++;
}
},
forElements(cb) {
this.getMappableElements().forEach(cb);
},
//Load
loadSelectedFace() {
this.face = $('#uv_panel_sides input:checked').attr('id').replace('_radio', '')
@ -884,28 +887,54 @@ const UVEditor = {
},
mirrorX(event) {
var scope = this;
this.forCubes(obj => {
this.forElements(obj => {
scope.getFaces(obj, event).forEach(function(side) {
var proxy = obj.faces[side].uv[0]
obj.faces[side].uv[0] = obj.faces[side].uv[2]
obj.faces[side].uv[2] = proxy
if (obj instanceof Cube) {
var proxy = obj.faces[side].uv[0]
obj.faces[side].uv[0] = obj.faces[side].uv[2]
obj.faces[side].uv[2] = proxy
} else if (obj instanceof Mesh) {
let center = 0;
let count = 0;
obj.faces[side].vertices.forEach(vkey => {
center += obj.faces[side].uv[vkey][0];
count++;
})
center /= count;
obj.faces[side].vertices.forEach(vkey => {
obj.faces[side].uv[vkey][0] = center*2 - obj.faces[side].uv[vkey][0];
})
}
})
obj.autouv = 0
Canvas.updateUV(obj)
if (obj.autouv) obj.autouv = 0
obj.preview_controller.updateUV(obj);
})
this.message('uv_editor.mirrored')
this.loadData()
},
mirrorY(event) {
var scope = this;
this.forCubes(obj => {
this.forElements(obj => {
scope.getFaces(obj, event).forEach(function(side) {
var proxy = obj.faces[side].uv[1]
obj.faces[side].uv[1] = obj.faces[side].uv[3]
obj.faces[side].uv[3] = proxy
if (obj instanceof Cube) {
var proxy = obj.faces[side].uv[1]
obj.faces[side].uv[1] = obj.faces[side].uv[3]
obj.faces[side].uv[3] = proxy
} else if (obj instanceof Mesh) {
let center = 0;
let count = 0;
obj.faces[side].vertices.forEach(vkey => {
center += obj.faces[side].uv[vkey][1];
count++;
})
center /= count;
obj.faces[side].vertices.forEach(vkey => {
obj.faces[side].uv[vkey][1] = center*2 - obj.faces[side].uv[vkey][1];
})
}
})
obj.autouv = 0
Canvas.updateUV(obj)
if (obj.autouv) obj.autouv = 0
obj.preview_controller.updateUV(obj);
})
this.message('uv_editor.mirrored')
this.loadData()
@ -1161,62 +1190,77 @@ const UVEditor = {
'_',
'copy',
'paste',
{icon: 'photo_size_select_large', name: 'menu.uv.mapping', condition: () => !Project.box_uv, children: function(editor) { return [
{icon: UVEditor.getReferenceFace().enabled!==false ? 'check_box' : 'check_box_outline_blank', name: 'generic.export', click: function() {
Undo.initEdit({elements: Cube.selected, uv_only: true})
UVEditor.toggleUV(event)
Undo.finishEdit('Toggle UV export')
}},
'uv_maximize',
'uv_auto',
'uv_rel_auto',
{icon: 'rotate_90_degrees_ccw', condition: () => Format.uv_rotation, name: 'menu.uv.mapping.rotation', children: function() {
var off = 'radio_button_unchecked'
var on = 'radio_button_checked'
let reference_face = UVEditor.getReferenceFace()
return [
{icon: (!reference_face.rotation ? on : off), name: '0&deg;', click: function() {
Undo.initEdit({elements: Cube.selected, uv_only: true})
UVEditor.setRotation(0)
Undo.finishEdit('Rotate UV')
}},
{icon: (reference_face.rotation === 90 ? on : off), name: '90&deg;', click: function() {
Undo.initEdit({elements: Cube.selected, uv_only: true})
UVEditor.setRotation(90)
Undo.finishEdit('Rotate UV')
}},
{icon: (reference_face.rotation === 180 ? on : off), name: '180&deg;', click: function() {
Undo.initEdit({elements: Cube.selected, uv_only: true})
UVEditor.setRotation(180)
Undo.finishEdit('Rotate UV')
}},
{icon: (reference_face.rotation === 270 ? on : off), name: '270&deg;', click: function() {
Undo.initEdit({elements: Cube.selected, uv_only: true})
UVEditor.setRotation(270)
Undo.finishEdit('Rotate UV')
}}
]
}},
'uv_turn_mapping',
{
icon: (UVEditor.getReferenceFace().uv[0] > UVEditor.getReferenceFace().uv[2] ? 'check_box' : 'check_box_outline_blank'),
name: 'menu.uv.mapping.mirror_x',
click: function() {
Undo.initEdit({elements: Cube.selected, uv_only: true})
UVEditor.mirrorX(event)
Undo.finishEdit('Mirror UV')
{icon: 'photo_size_select_large', name: 'menu.uv.mapping', condition: () => !Project.box_uv, children: function(editor) {
let reference_face = UVEditor.getReferenceFace();
function isMirrored(axis) {
if (reference_face instanceof CubeFace) {
reference_face.uv[axis+0] > reference_face.uv[axis+2]
} else {
let vertices = reference_face.getSortedVertices();
if (vertices.length <= 2) return false;
if (!Math.epsilon(reference_face.uv[vertices[0]][axis], reference_face.uv[vertices[1]][axis], 0.01)) {
return reference_face.uv[vertices[0]][axis] > reference_face.uv[vertices[1]][axis];
} else {
return reference_face.uv[vertices[0]][axis] > reference_face.uv[vertices[2]][axis];
}
}
},
{
icon: (UVEditor.getReferenceFace().uv[1] > UVEditor.getReferenceFace().uv[3] ? 'check_box' : 'check_box_outline_blank'),
name: 'menu.uv.mapping.mirror_y',
click: function() {
}
return [
{icon: reference_face.enabled!==false ? 'check_box' : 'check_box_outline_blank', name: 'generic.export', click: function() {
Undo.initEdit({elements: Cube.selected, uv_only: true})
UVEditor.mirrorY(event)
Undo.finishEdit('Mirror UV')
}
},
]}},
UVEditor.toggleUV(event)
Undo.finishEdit('Toggle UV export')
}},
'uv_maximize',
'uv_auto',
'uv_rel_auto',
{icon: 'rotate_90_degrees_ccw', condition: () => Format.uv_rotation, name: 'menu.uv.mapping.rotation', children: function() {
var off = 'radio_button_unchecked'
var on = 'radio_button_checked'
return [
{icon: (!reference_face.rotation ? on : off), name: '0&deg;', click: function() {
Undo.initEdit({elements: Cube.selected, uv_only: true})
UVEditor.setRotation(0)
Undo.finishEdit('Rotate UV')
}},
{icon: (reference_face.rotation === 90 ? on : off), name: '90&deg;', click: function() {
Undo.initEdit({elements: Cube.selected, uv_only: true})
UVEditor.setRotation(90)
Undo.finishEdit('Rotate UV')
}},
{icon: (reference_face.rotation === 180 ? on : off), name: '180&deg;', click: function() {
Undo.initEdit({elements: Cube.selected, uv_only: true})
UVEditor.setRotation(180)
Undo.finishEdit('Rotate UV')
}},
{icon: (reference_face.rotation === 270 ? on : off), name: '270&deg;', click: function() {
Undo.initEdit({elements: Cube.selected, uv_only: true})
UVEditor.setRotation(270)
Undo.finishEdit('Rotate UV')
}}
]
}},
'uv_turn_mapping',
{
icon: (isMirrored(0) ? 'check_box' : 'check_box_outline_blank'),
name: 'menu.uv.mapping.mirror_x',
click: function() {
Undo.initEdit({elements: Cube.selected, uv_only: true})
UVEditor.mirrorX(event)
Undo.finishEdit('Mirror UV')
}
},
{
icon: (isMirrored(1) ? 'check_box' : 'check_box_outline_blank'),
name: 'menu.uv.mapping.mirror_y',
click: function() {
Undo.initEdit({elements: Cube.selected, uv_only: true})
UVEditor.mirrorY(event)
Undo.finishEdit('Mirror UV')
}
},
]
}},
'face_tint',
{icon: 'flip_to_back', condition: () => (Format.id == 'java_block'&& Cube.selected.length), name: 'action.cullface' , children: function() {
var off = 'radio_button_unchecked';