mirror of
https://github.com/JannisX11/blockbench.git
synced 2024-12-09 04:50:29 +08:00
Merge branch 'seams' into next
This commit is contained in:
commit
5828884041
@ -25,7 +25,7 @@
|
||||
<script>
|
||||
if (typeof module === 'object') {window.module = module; module = undefined;}//jQuery Fix
|
||||
const isApp = typeof require !== 'undefined';
|
||||
const appVersion = '4.2.0-beta.1';
|
||||
const appVersion = '4.2.0-beta.2';
|
||||
|
||||
|
||||
if (localStorage.getItem('theme')) {
|
||||
|
@ -171,6 +171,26 @@ function updateSelection(options = {}) {
|
||||
updateNslideValues();
|
||||
Interface.status_bar.vue.updateSelectionInfo();
|
||||
if (settings.highlight_cubes.value || (Mesh.all[0])) updateCubeHighlights();
|
||||
if (Toolbox.selected.id == 'seam_tool' && Mesh.selected[0]) {
|
||||
let value;
|
||||
let selected_vertices = Mesh.selected[0].getSelectedVertices();
|
||||
Mesh.selected[0].forAllFaces((face) => {
|
||||
if (value == '') return;
|
||||
let vertices = face.getSortedVertices();
|
||||
vertices.forEach((vkey_a, i) => {
|
||||
let vkey_b = vertices[i+1] || vertices[0];
|
||||
if (selected_vertices.includes(vkey_a) && selected_vertices.includes(vkey_b)) {
|
||||
let seam = Mesh.selected[0].getSeam([vkey_a, vkey_b]) || 'auto';
|
||||
if (value == undefined) {
|
||||
value = seam;
|
||||
} else if (value !== seam) {
|
||||
value = '';
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
BarItems.select_seam.set(value || 'auto');
|
||||
}
|
||||
Canvas.updatePivotMarker();
|
||||
Transformer.updateSelection();
|
||||
Preview.all.forEach(preview => {
|
||||
|
@ -1933,6 +1933,7 @@ const BARS = {
|
||||
'rotate_tool',
|
||||
'pivot_tool',
|
||||
'vertex_snap_tool',
|
||||
'seam_tool',
|
||||
'brush_tool',
|
||||
'fill_tool',
|
||||
'eraser',
|
||||
@ -1944,6 +1945,9 @@ const BARS = {
|
||||
vertical: Blockbench.isMobile == true,
|
||||
default_place: true
|
||||
})
|
||||
Blockbench.onUpdateTo('4.2.0-beta.2', () => {
|
||||
Toolbars.tools.add(BarItems.seam_tool, 5);
|
||||
})
|
||||
|
||||
Toolbars.element_position = new Toolbar({
|
||||
id: 'element_position',
|
||||
@ -2158,6 +2162,12 @@ const BARS = {
|
||||
'selection_mode'
|
||||
]
|
||||
})
|
||||
Toolbars.seam_tool = new Toolbar({
|
||||
id: 'seam_tool',
|
||||
children: [
|
||||
'select_seam'
|
||||
]
|
||||
})
|
||||
Blockbench.onUpdateTo('4.0', () => {
|
||||
Toolbars.vertex_snap.add(BarItems.selection_mode);
|
||||
})
|
||||
|
@ -109,10 +109,17 @@ class MeshFace extends Face {
|
||||
|| pointInsidePolygon(x+0.00001, y+0.99999)
|
||||
|| pointInsidePolygon(x+0.99999, y+0.99999));
|
||||
if (!inside) {
|
||||
let i = 0;
|
||||
let px_rect = [[x, y], [x+0.99999, y+0.99999]]
|
||||
for (let vkey of sorted_vertices) {
|
||||
if (pointInRectangle(face.uv[vkey], [x, y], [x+0.99999, y+0.99999])) {
|
||||
let vkey_b = sorted_vertices[i+1] || sorted_vertices[0]
|
||||
if (pointInRectangle(face.uv[vkey], ...px_rect)) {
|
||||
inside = true; break;
|
||||
}
|
||||
if (lineIntersectsReactangle(face.uv[vkey], face.uv[vkey_b], ...px_rect)) {
|
||||
inside = true; break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
if (inside) {
|
||||
@ -123,6 +130,11 @@ class MeshFace extends Face {
|
||||
}
|
||||
return matrix;
|
||||
}
|
||||
getAngleTo(other_face) {
|
||||
let a = new THREE.Vector3().fromArray(this.getNormal());
|
||||
let b = new THREE.Vector3().fromArray(other_face.getNormal());
|
||||
return Math.radToDeg(a.angleTo(b));
|
||||
}
|
||||
invert() {
|
||||
if (this.vertices.length < 3) return this;
|
||||
[this.vertices[0], this.vertices[1]] = [this.vertices[1], this.vertices[0]];
|
||||
@ -179,7 +191,8 @@ class MeshFace extends Face {
|
||||
return {
|
||||
face,
|
||||
key: fkey,
|
||||
index: index_b
|
||||
index: index_b,
|
||||
edge: side_vertices
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -243,6 +256,7 @@ class Mesh extends OutlinerElement {
|
||||
|
||||
this.vertices = {};
|
||||
this.faces = {};
|
||||
this.seams = {};
|
||||
|
||||
if (!data.vertices) {
|
||||
this.addVertices([2, 4, 2], [2, 4, -2], [2, 0, 2], [2, 0, -2], [-2, 4, 2], [-2, 4, -2], [-2, 0, 2], [-2, 0, -2]);
|
||||
@ -275,6 +289,18 @@ class Mesh extends OutlinerElement {
|
||||
get vertice_list() {
|
||||
return Object.keys(this.vertices).map(key => this.vertices[key]);
|
||||
}
|
||||
setSeam(edge, value) {
|
||||
let key = edge.slice(0, 2).sort().join('_');
|
||||
if (value) {
|
||||
this.seams[key] = value;
|
||||
} else {
|
||||
delete this.seams[key];
|
||||
}
|
||||
}
|
||||
getSeam(edge) {
|
||||
let key = edge.slice(0, 2).sort().join('_');
|
||||
return this.seams[key];
|
||||
}
|
||||
getWorldCenter(ignore_selected_vertices) {
|
||||
let m = this.mesh;
|
||||
let pos = Reusable.vec1.set(0, 0, 0);
|
||||
@ -901,6 +927,8 @@ new NodePreviewController(Mesh, {
|
||||
|
||||
let mesh = element.mesh;
|
||||
let white = new THREE.Color(0xffffff);
|
||||
let join = new THREE.Color(0x16d606);
|
||||
let divide = new THREE.Color(0xff4400);
|
||||
let selected_vertices = element.getSelectedVertices();
|
||||
|
||||
if (BarItems.selection_mode.value == 'vertex') {
|
||||
@ -920,14 +948,27 @@ new NodePreviewController(Mesh, {
|
||||
|
||||
let line_colors = [];
|
||||
mesh.outline.vertex_order.forEach((key, i) => {
|
||||
let key_b = Modes.edit && mesh.outline.vertex_order[i + ((i%2) ? -1 : 1) ];
|
||||
let color;
|
||||
let selected;
|
||||
if (!Modes.edit || BarItems.selection_mode.value == 'object') {
|
||||
color = gizmo_colors.outline;
|
||||
} else if (selected_vertices.includes(key) && selected_vertices.includes(mesh.outline.vertex_order[i + ((i%2) ? -1 : 1) ])) {
|
||||
} else if (selected_vertices.includes(key) && selected_vertices.includes(key_b)) {
|
||||
color = white;
|
||||
selected = true;
|
||||
} else {
|
||||
color = gizmo_colors.grid;
|
||||
}
|
||||
if (Toolbox.selected.id === 'seam_tool') {
|
||||
let seam = element.getSeam([key, key_b]);
|
||||
if (seam == 'join') color = join;
|
||||
if (seam == 'divide') color = divide;
|
||||
if (selected) {
|
||||
color.r *= 1.2;
|
||||
color.g *= 1.2;
|
||||
color.b *= 1.2;
|
||||
}
|
||||
}
|
||||
line_colors.push(color.r, color.g, color.b);
|
||||
})
|
||||
mesh.outline.geometry.setAttribute('color', new THREE.Float32BufferAttribute(line_colors, 3));
|
||||
@ -1366,6 +1407,64 @@ BARS.defineActions(function() {
|
||||
updateSelection();
|
||||
}
|
||||
})
|
||||
|
||||
let seam_timeout;
|
||||
new Tool('seam_tool', {
|
||||
icon: 'content_cut',
|
||||
transformerMode: 'hidden',
|
||||
toolbar: 'seam_tool',
|
||||
category: 'tools',
|
||||
selectElements: true,
|
||||
modes: ['edit'],
|
||||
condition: () => Mesh.all.length,
|
||||
onCanvasClick(data) {
|
||||
if (!seam_timeout) {
|
||||
seam_timeout = setTimeout(() => {
|
||||
seam_timeout = null;
|
||||
}, 200)
|
||||
} else {
|
||||
clearTimeout(seam_timeout);
|
||||
seam_timeout = null;
|
||||
BarItems.select_seam.trigger();
|
||||
}
|
||||
},
|
||||
onSelect: function() {
|
||||
BarItems.selection_mode.set('edge');
|
||||
BarItems.view_mode.set('solid');
|
||||
BarItems.view_mode.onChange();
|
||||
},
|
||||
onUnselect: function() {
|
||||
BarItems.selection_mode.set('object');
|
||||
BarItems.view_mode.set('textured');
|
||||
BarItems.view_mode.onChange();
|
||||
}
|
||||
})
|
||||
new BarSelect('select_seam', {
|
||||
options: {
|
||||
auto: true,
|
||||
divide: true,
|
||||
join: true,
|
||||
},
|
||||
condition: () => Modes.edit && Mesh.all.length,
|
||||
onChange({value}) {
|
||||
if (value == 'auto') value = null;
|
||||
Undo.initEdit({elements: Mesh.selected});
|
||||
Mesh.selected.forEach(mesh => {
|
||||
let selected_vertices = mesh.getSelectedVertices();
|
||||
mesh.forAllFaces((face) => {
|
||||
let vertices = face.getSortedVertices();
|
||||
vertices.forEach((vkey_a, i) => {
|
||||
let vkey_b = vertices[i+1] || vertices[0];
|
||||
if (selected_vertices.includes(vkey_a) && selected_vertices.includes(vkey_b)) {
|
||||
mesh.setSeam([vkey_a, vkey_b], value);
|
||||
}
|
||||
})
|
||||
});
|
||||
Mesh.preview_controller.updateSelection(mesh);
|
||||
})
|
||||
Undo.finishEdit('Set mesh seam');
|
||||
}
|
||||
})
|
||||
new Action('create_face', {
|
||||
icon: 'fas.fa-draw-polygon',
|
||||
category: 'edit',
|
||||
@ -1421,8 +1520,8 @@ BARS.defineActions(function() {
|
||||
|
||||
let [face_key] = mesh.addFaces(new_face);
|
||||
UVEditor.selected_faces.push(face_key);
|
||||
|
||||
if (Reusable.vec1.fromArray(reference_face.getNormal(true)).angleTo(Reusable.vec2.fromArray(new_face)) > Math.PI/2) {
|
||||
|
||||
if (reference_face.angleTo(new_face) > 90) {
|
||||
new_face.invert();
|
||||
}
|
||||
}
|
||||
|
@ -974,7 +974,7 @@ class Preview {
|
||||
}
|
||||
mouseup(event) {
|
||||
this.showContextMenu(event);
|
||||
if (settings.canvas_unselect.value && event.which != 2 && this.controls.hasMoved === false && !this.selection.activated && !Transformer.dragging && !this.selection.click_target) {
|
||||
if (settings.canvas_unselect.value && (event.which === 1 || event.which === 3) && this.controls.hasMoved === false && !this.selection.activated && !Transformer.dragging && !this.selection.click_target) {
|
||||
unselectAll();
|
||||
}
|
||||
delete this.selection.click_target;
|
||||
|
@ -49,6 +49,8 @@ const TextureGenerator = {
|
||||
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)},
|
||||
|
||||
},
|
||||
@ -77,7 +79,7 @@ const TextureGenerator = {
|
||||
TextureGenerator.background_color.set('#00000000');
|
||||
var dialog = new Dialog({
|
||||
id: 'add_bitmap',
|
||||
title: tl('action.create_texture'),
|
||||
title: tl('action.append_to_template'),
|
||||
width: 480,
|
||||
form: {
|
||||
color: {label: 'data.color', type: 'color', colorpicker: TextureGenerator.background_color},
|
||||
@ -86,6 +88,8 @@ const TextureGenerator = {
|
||||
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) => (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: 45, 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.rearrange_uv)},
|
||||
},
|
||||
onFormChange(form) {
|
||||
@ -709,121 +713,266 @@ const TextureGenerator = {
|
||||
} else {
|
||||
let mesh = element;
|
||||
let face_groups = [];
|
||||
for (let key in mesh.faces) {
|
||||
let face = mesh.faces[key];
|
||||
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: [0],
|
||||
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) {
|
||||
function tryToMergeFaceGroup(face_group) {
|
||||
face_groups.slice().forEach((face_group) => {
|
||||
if (!face_groups.includes(face_group)) return;
|
||||
|
||||
let matches = face_groups.filter(group_b => {
|
||||
if (group_b == face_group) return false;
|
||||
if (face_group.faces.find(face => face.vertices.find(vkey => group_b.faces.find(face => face.vertices.includes(vkey)))) == undefined) return false;
|
||||
return face_group.normal.find((v, i) => !Math.epsilon(v, group_b.normal[i], 0.002)) == undefined;
|
||||
});
|
||||
matches.forEach(match => {
|
||||
face_group.faces.push(...match.faces);
|
||||
face_group.keys.push(...match.keys);
|
||||
face_groups.remove(match);
|
||||
|
||||
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.slice().forEach(tryToMergeFaceGroup);
|
||||
})
|
||||
}
|
||||
|
||||
face_groups.forEach(face_group => {
|
||||
// Project vertex coords onto plane
|
||||
let normal_vec = vec1.fromArray(face_group.normal);
|
||||
let plane = new THREE.Plane().setFromNormalAndCoplanarPoint(
|
||||
normal_vec,
|
||||
vec2.fromArray(mesh.vertices[face_group.faces[0].vertices[0]])
|
||||
)
|
||||
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 vertex_uvs = {};
|
||||
face_group.faces.forEach(face => {
|
||||
face.vertices.forEach(vkey => {
|
||||
if (!vertex_uvs[vkey]) {
|
||||
let coplanar_pos = plane.projectPoint(vec3.fromArray(mesh.vertices[vkey]), vec4);
|
||||
coplanar_pos.applyEuler(e);
|
||||
vertex_uvs[vkey] = [
|
||||
Math.roundTo(coplanar_pos.x, 4),
|
||||
Math.roundTo(coplanar_pos.z, 4),
|
||||
]
|
||||
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;
|
||||
}
|
||||
})
|
||||
})
|
||||
// Rotate UV to match corners
|
||||
let rotation_angles = {};
|
||||
let precise_rotation_angle = {};
|
||||
face_group.faces.forEach(face => {
|
||||
let vertices = face.getSortedVertices();
|
||||
vertices.forEach((vkey, i) => {
|
||||
let vkey2 = vertices[i+1] || vertices[0];
|
||||
let rot = Math.atan2(
|
||||
vertex_uvs[vkey2][0] - vertex_uvs[vkey][0],
|
||||
vertex_uvs[vkey2][1] - vertex_uvs[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 (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;
|
||||
}
|
||||
}
|
||||
if (!rounded) rounded = Math.round(rot / snap) * snap;
|
||||
if (rotation_angles[rounded]) {
|
||||
rotation_angles[rounded]++;
|
||||
} else {
|
||||
rotation_angles[rounded] = 1;
|
||||
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;
|
||||
}
|
||||
})
|
||||
let angle = Math.degToRad(precise_rotation_angle[angles[0]]);
|
||||
let s = Math.sin(angle);
|
||||
let c = Math.cos(angle);
|
||||
for (let vkey in vertex_uvs) {
|
||||
let point = vertex_uvs[vkey].slice();
|
||||
vertex_uvs[vkey][0] = point[0] * c - point[1] * s;
|
||||
vertex_uvs[vkey][1] = point[0] * s + point[1] * c;
|
||||
}
|
||||
|
||||
|
||||
// Define UV bounding box
|
||||
let min_x = Infinity;
|
||||
let min_z = Infinity;
|
||||
for (let vkey in vertex_uvs) {
|
||||
min_x = Math.min(min_x, vertex_uvs[vkey][0]);
|
||||
min_z = Math.min(min_z, vertex_uvs[vkey][1]);
|
||||
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 vkey in vertex_uvs) {
|
||||
vertex_uvs[vkey][0] -= min_x;
|
||||
vertex_uvs[vkey][1] -= min_z;
|
||||
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
|
||||
@ -833,24 +982,35 @@ const TextureGenerator = {
|
||||
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[vkey][0], vertex_uvs[vkey2][0], 0.001)) {
|
||||
let min = vertex_uvs[vkey][0] > vertex_uvs[vkey0][0] ? 1 : 0;
|
||||
vertex_uvs[vkey][0] = vertex_uvs[vkey2][0] = Math.round(Math.max(min, vertex_uvs[vkey][0] * snap)) / snap;
|
||||
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[vkey][1], vertex_uvs[vkey2][1], 0.001)) {
|
||||
let min = vertex_uvs[vkey][1] > vertex_uvs[vkey0][1] ? 1 : 0;
|
||||
vertex_uvs[vkey][1] = vertex_uvs[vkey2][1] = Math.round(Math.max(min, vertex_uvs[vkey][1] * 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;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
let max_x = -Infinity;
|
||||
let max_z = -Infinity;
|
||||
for (let vkey in vertex_uvs) {
|
||||
max_x = Math.max(max_x, vertex_uvs[vkey][0]);
|
||||
max_z = Math.max(max_z, vertex_uvs[vkey][1]);
|
||||
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) {
|
||||
for (let fkey in vertex_uvs) {
|
||||
for (let vkey in vertex_uvs[fkey]) {
|
||||
vertex_uvs[fkey][vkey][0] += Math.ceil(max_x) - max_x;
|
||||
}
|
||||
}
|
||||
}
|
||||
face_group.posx = 0;
|
||||
face_group.posy = 0;
|
||||
@ -915,9 +1075,9 @@ const TextureGenerator = {
|
||||
|
||||
face_list.forEach(face_group => {
|
||||
if (!face_group.mesh) return;
|
||||
let face_uvs = face_group.faces.map((face) => {
|
||||
let face_uvs = face_group.faces.map((face, i) => {
|
||||
return face.getSortedVertices().map(vkey => {
|
||||
return face_group.vertex_uvs[vkey];
|
||||
return face_group.vertex_uvs[face_group.keys[i]][vkey];
|
||||
})
|
||||
});
|
||||
face_group.matrix = getPolygonOccupationMatrix(face_uvs, face_group.width, face_group.height);
|
||||
@ -1045,6 +1205,23 @@ const TextureGenerator = {
|
||||
|| 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;
|
||||
@ -1161,70 +1338,114 @@ const TextureGenerator = {
|
||||
return true;
|
||||
}
|
||||
function drawMeshTexture(ftemp, coords) {
|
||||
if (!Format.single_texture) {
|
||||
if (ftemp.faces[0].texture === undefined) return false;
|
||||
texture = ftemp.faces[0].getTexture()
|
||||
} else {
|
||||
texture = Texture.getDefault();
|
||||
}
|
||||
if (!texture || !texture.img) return false;
|
||||
let i = 0;
|
||||
for (let face of ftemp.faces) {
|
||||
|
||||
/*
|
||||
ctx.save()
|
||||
let a_old = ftemp.faces[0].uv[ftemp.faces[0].vertices[0]];
|
||||
let b_old = ftemp.faces[0].uv[ftemp.faces[0].vertices[1]];
|
||||
let a_new = ftemp.vertex_uvs[ftemp.faces[0].vertices[0]];
|
||||
let b_new = ftemp.vertex_uvs[ftemp.faces[0].vertices[1]];
|
||||
let _old = Math.atan2(
|
||||
b_old[1] - a_old[1],
|
||||
b_old[0] - a_old[0],
|
||||
)
|
||||
let _new = Math.atan2(
|
||||
b_new[1] - a_new[1],
|
||||
b_new[0] - a_new[0],
|
||||
)
|
||||
let rotation_difference = Math.radToDeg(_new - _old);
|
||||
if (Math.abs(((rotation_difference + 540) % 360) - 180) > 15) return false;
|
||||
*/
|
||||
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 R = res_multiple;
|
||||
let min = [Infinity, Infinity];
|
||||
let max = [0, 0];
|
||||
ftemp.faces.forEach(face => {
|
||||
|
||||
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]);
|
||||
})
|
||||
})
|
||||
|
||||
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;
|
||||
|
||||
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,
|
||||
coords.y*R,
|
||||
coords.w*R,
|
||||
coords.h*R
|
||||
)
|
||||
ctx.restore()
|
||||
|
||||
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) / Project.texture_width * texture.img.naturalWidth,
|
||||
coords.y*R + (target_min[1] + (target_max[1] - target_min[1])/2) / Project.texture_height * texture.img.naturalHeight,
|
||||
]
|
||||
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]) / Project.texture_height * texture.img.naturalHeight),
|
||||
Math.ceil((target_max[0] - target_min[0]) / Project.texture_width * texture.img.naturalWidth),
|
||||
]
|
||||
let target_pos = [
|
||||
coords.x*R + target_min[0] / Project.texture_width * texture.img.naturalWidth,
|
||||
coords.y*R + target_min[1] / Project.texture_height * texture.img.naturalHeight,
|
||||
];
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (Math.abs(rotate) != 90) {
|
||||
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] / Project.texture_width * texture.img.naturalWidth,
|
||||
coords.y*R + target_min[1] / Project.texture_height * texture.img.naturalHeight,
|
||||
Math.ceil((target_max[0] - target_min[0]) / Project.texture_width * texture.img.naturalWidth),
|
||||
Math.ceil((target_max[1] - target_min[1]) / Project.texture_height * texture.img.naturalHeight),
|
||||
)
|
||||
}
|
||||
ctx.restore()
|
||||
i++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -1272,11 +1493,12 @@ const TextureGenerator = {
|
||||
[ftemp.face.uv[2], ftemp.face.uv[0]] = [ftemp.face.uv[0], ftemp.face.uv[2]];
|
||||
}
|
||||
} else {
|
||||
ftemp.faces.forEach(face => {
|
||||
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[vkey][0] + ftemp.posx;
|
||||
face.uv[vkey][1] = ftemp.vertex_uvs[vkey][1] + ftemp.posy;
|
||||
face.uv[vkey][0] = ftemp.vertex_uvs[fkey][vkey][0] + ftemp.posx;
|
||||
face.uv[vkey][1] = ftemp.vertex_uvs[fkey][vkey][1] + ftemp.posy;
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -448,6 +448,7 @@ const UVEditor = {
|
||||
matches.forEach(s => {
|
||||
Project.selected_elements.safePush(s)
|
||||
});
|
||||
if (!event.shiftKey) UVEditor.selectMeshUVIsland(UVEditor.selected_faces[0]);
|
||||
updateSelection();
|
||||
}
|
||||
return matches;
|
||||
@ -623,6 +624,32 @@ const UVEditor = {
|
||||
}
|
||||
UVEditor.displayTools();
|
||||
},
|
||||
selectMeshUVIsland(face_key) {
|
||||
if (face_key && Mesh.selected[0] && Mesh.selected[0].faces[face_key]) {
|
||||
if (UVEditor.selected_faces.length == 1) {
|
||||
let mesh = Mesh.selected[0];
|
||||
function crawl(face) {
|
||||
for (let i = 0; i < face.vertices.length; i++) {
|
||||
let adjacent = face.getAdjacentFace(i);
|
||||
if (!adjacent) continue;
|
||||
if (UVEditor.selected_faces.includes(adjacent.key)) continue;
|
||||
let epsilon = 0.2;
|
||||
let uv_a1 = adjacent.face.uv[adjacent.edge[0]];
|
||||
let uv_a2 = face.uv[adjacent.edge[0]];
|
||||
if (!Math.epsilon(uv_a1[0], uv_a2[0], epsilon) || !Math.epsilon(uv_a1[1], uv_a2[1], epsilon)) continue;
|
||||
let uv_b1 = adjacent.face.uv[adjacent.edge[1]];
|
||||
let uv_b2 = face.uv[adjacent.edge[1]];
|
||||
if (!Math.epsilon(uv_b1[0], uv_b2[0], epsilon) || !Math.epsilon(uv_b1[1], uv_b2[1], epsilon)) continue;
|
||||
UVEditor.selected_faces.push(adjacent.key);
|
||||
crawl(adjacent.face);
|
||||
}
|
||||
}
|
||||
crawl(mesh.faces[face_key]);
|
||||
} else {
|
||||
UVEditor.selected_faces.replace([face_key]);
|
||||
}
|
||||
}
|
||||
},
|
||||
moveSelection(offset, event) {
|
||||
Undo.initEdit({elements: UVEditor.getMappableElements()})
|
||||
let step = canvasGridSize(event.shiftKey || Pressing.overrides.shift, event.ctrlOrCmd || Pressing.overrides.ctrl) / UVEditor.grid;
|
||||
@ -1227,6 +1254,7 @@ const UVEditor = {
|
||||
Project.display_uv = UVEditor.vue.display_uv = option;
|
||||
if (option == 'selected_faces') settings.show_only_selected_uv.set(true);
|
||||
if (option == 'selected_elements') settings.show_only_selected_uv.set(false);
|
||||
Settings.saveLocalStorages();
|
||||
}
|
||||
}})
|
||||
}},
|
||||
@ -2179,6 +2207,7 @@ Interface.definePanels(function() {
|
||||
dragFace(face_key, event) {
|
||||
if (event.which == 2 || event.which == 3) return;
|
||||
|
||||
let face_selected_before = this.selected_faces[0];
|
||||
if (face_key) this.selectFace(face_key, event, true);
|
||||
let elements = UVEditor.getMappableElements();
|
||||
Undo.initEdit({
|
||||
@ -2346,6 +2375,14 @@ Interface.definePanels(function() {
|
||||
if (do_move_uv) {
|
||||
overlay_canvas.remove();
|
||||
}
|
||||
let selected_faces = this.selected_faces.slice()
|
||||
UVEditor.selectMeshUVIsland(face_key);
|
||||
if (
|
||||
(this.selected_faces.includes(face_selected_before) && face_selected_before !== face_key) ||
|
||||
(event.shiftKey || event.ctrlOrCmd || Pressing.overrides.shift || Pressing.overrides.ctrl)
|
||||
) {
|
||||
this.selected_faces.replace(selected_faces);
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
@ -2606,8 +2643,8 @@ Interface.definePanels(function() {
|
||||
face.getSortedVertices().forEach(key => {
|
||||
let UV = face.uv[key];
|
||||
coords.push(
|
||||
((UV[0] + uv_offset[0]) / this.project_resolution[0] * this.inner_width + 1) + ',' +
|
||||
((UV[1] + uv_offset[1]) / this.project_resolution[0] * this.inner_width + 1)
|
||||
Math.roundTo((UV[0] + uv_offset[0]) / this.project_resolution[0] * this.inner_width + 1, 4) + ',' +
|
||||
Math.roundTo((UV[1] + uv_offset[1]) / this.project_resolution[0] * this.inner_width + 1, 4)
|
||||
)
|
||||
})
|
||||
return coords.join(' ');
|
||||
|
33
js/util.js
33
js/util.js
@ -480,6 +480,39 @@ Array.prototype.V3_divide = function(x, y, z) {
|
||||
Array.prototype.V3_toThree = function() {
|
||||
return new THREE.Vector3(this[0], this[1], this[2]);
|
||||
}
|
||||
Array.prototype.V2_set = function(x, y) {
|
||||
if (x instanceof Array) return this.V2_set(...x);
|
||||
if (y === undefined) y = x;
|
||||
this[0] = parseFloat(x)||0;
|
||||
this[1] = parseFloat(y)||0;
|
||||
return this;
|
||||
}
|
||||
Array.prototype.V2_add = function(x, y) {
|
||||
if (x instanceof Array) return this.V2_add(...x);
|
||||
this[0] += parseFloat(x)||0;
|
||||
this[1] += parseFloat(y)||0;
|
||||
return this;
|
||||
}
|
||||
Array.prototype.V2_subtract = function(x, y) {
|
||||
if (x instanceof Array) return this.V2_subtract(...x);
|
||||
this[0] -= parseFloat(x)||0;
|
||||
this[1] -= parseFloat(y)||0;
|
||||
return this;
|
||||
}
|
||||
Array.prototype.V2_multiply = function(x, y) {
|
||||
if (x instanceof Array) return this.V2_multiply(...x);
|
||||
if (y === undefined) y = x;
|
||||
this[0] *= parseFloat(x)||0;
|
||||
this[1] *= parseFloat(y)||0;
|
||||
return this;
|
||||
}
|
||||
Array.prototype.V2_divide = function(x, y) {
|
||||
if (x instanceof Array) return this.V2_divide(...x);
|
||||
if (y === undefined) y = x;
|
||||
this[0] /= parseFloat(x)||1;
|
||||
this[1] /= parseFloat(y)||1;
|
||||
return this;
|
||||
}
|
||||
|
||||
//Object
|
||||
Object.defineProperty(Array.prototype, "equals", {enumerable: false});
|
||||
|
File diff suppressed because one or more lines are too long
15
lang/en.json
15
lang/en.json
@ -440,8 +440,12 @@
|
||||
"dialog.create_texture.double_use.desc": "If two elements already have the same UV space assigned, keep it that way in the new map",
|
||||
"dialog.create_texture.padding": "Padding",
|
||||
"dialog.create_texture.padding.desc": "Add a small padding between the individual parts of the template",
|
||||
"dialog.create_texture.combine_polys": "Combine Faces",
|
||||
"dialog.create_texture.combine_polys.desc": "Combine connected coplanar faces into one UV section",
|
||||
"dialog.create_texture.combine_polys": "Combine Islands",
|
||||
"dialog.create_texture.combine_polys.desc": "Combine faces into connected UV islands",
|
||||
"dialog.create_texture.max_edge_angle": "Edge Angle Threshold",
|
||||
"dialog.create_texture.max_edge_angle.desc": "The maximum angle between two faces at which they will still be combined",
|
||||
"dialog.create_texture.max_island_angle": "Island Angle Threshold",
|
||||
"dialog.create_texture.max_island_angle.desc": "The maximum angle that can be combined into the same UV island",
|
||||
"dialog.create_texture.resolution": "Resolution",
|
||||
"dialog.create_texture.resolution.desc": "The height and width of the texture",
|
||||
"dialog.create_texture.pixel_density": "Pixel Density",
|
||||
@ -845,6 +849,8 @@
|
||||
"action.rotate_tool.desc": "Tool to select and rotate elements",
|
||||
"action.pivot_tool": "Pivot Tool",
|
||||
"action.pivot_tool.desc": "Tool to change the pivot point of cubes and bones",
|
||||
"action.seam_tool": "Seam Tool",
|
||||
"action.seam_tool.desc": "Tool to define UV seams on mesh edges",
|
||||
"action.brush_tool": "Paint Brush",
|
||||
"action.brush_tool.desc": "Tool to paint on bitmap textures on surfaces or the UV editor",
|
||||
"action.fill_tool": "Paint Bucket",
|
||||
@ -1096,6 +1102,11 @@
|
||||
"action.selection_mode.face": "Face",
|
||||
"action.selection_mode.edge": "Edge",
|
||||
"action.selection_mode.vertex": "Vertex",
|
||||
"action.select_seam": "Select UV Seam",
|
||||
"action.select_seam.desc": "Select the UV seam mode for the selected edges",
|
||||
"action.select_seam.auto": "Auto",
|
||||
"action.select_seam.join": "Join",
|
||||
"action.select_seam.divide": "Divide",
|
||||
"action.create_face": "Create Face or Edge",
|
||||
"action.create_face.desc": "Creates a new face or edge between the selected vertices",
|
||||
"action.convert_to_mesh": "Convert to Mesh",
|
||||
|
Loading…
Reference in New Issue
Block a user