function switchBoxUV(state) { BARS.updateConditions() if (state) { Cube.all.forEach(cube => { if (cube.faces.west.uv[2] < cube.faces.east.uv[0]) { cube.mirror_uv = true; cube.uv_offset[0] = cube.faces.west.uv[2]; } else { cube.mirror_uv = false; cube.uv_offset[0] = cube.faces.east.uv[0]; } cube.uv_offset[1] = cube.faces.up.uv[3]; }) } else { Cube.all.forEach(cube => { for (let fkey in cube.faces) { cube.faces[fkey].rotation = 0; } }) } UVEditor.vue.box_uv = state; UVEditor.setGrid(1); Canvas.updateAllUVs(); } const UVEditor = { face: 'north', size: 320, zoom: 1, grid: 1, max_zoom: 16, auto_grid: true, panel: null, sliders: {}, get vue() { return this.panel.inside_vue; }, message(msg, vars) { msg = tl(msg, vars) let box = document.createElement('div'); box.className = 'uv_message_box' box.textContent = msg; this.vue.$refs.main.append(box) setTimeout(function() { box.remove() }, 1200) }, //Brush getBrushCoordinates(event, tex) { convertTouchEvent(event); let pixel_size = this.inner_width / tex.width let result = {}; let mouse_coords; if (event.target.id == 'uv_frame') { mouse_coords = [event.offsetX, event.offsetY]; } else { let frame_pos = $('#uv_frame').offset(); mouse_coords = [ event.clientX - frame_pos.left, event.clientY - frame_pos.top ]; } if (Toolbox.selected.id === 'copy_paste_tool') { result.x = Math.round(mouse_coords[0]/pixel_size*1); result.y = Math.round(mouse_coords[1]/pixel_size*1); } else { let offset = BarItems.slider_brush_size.get()%2 == 0 && Toolbox.selected.brushTool ? 0.5 : 0; result.x = Math.floor(mouse_coords[0]/pixel_size*1 + offset); result.y = Math.floor(mouse_coords[1]/pixel_size*1 + offset); } if (tex.frameCount) result.y += (tex.height / tex.frameCount) * tex.currentFrame; if (!tex.frameCount && tex.ratio != Project.texture_width / Project.texture_height) result.y /= tex.ratio; return result; }, startPaintTool(event) { delete Painter.current.face_matrices; delete Painter.current.element; var texture = this.getTexture() if (texture) { var coords = this.getBrushCoordinates(event, texture) if (Toolbox.selected.id !== 'copy_paste_tool') { Painter.startPaintTool(texture, coords.x, coords.y, undefined, event) } else { this.startSelection(coords.x, coords.y, event) } } if (Toolbox.selected.id !== 'color_picker' && Toolbox.selected.id !== 'copy_paste_tool' && texture) { addEventListeners(this.vue.$refs.frame, 'mousemove touchmove', UVEditor.movePaintTool, false ); addEventListeners(document, 'mouseup touchend', UVEditor.stopBrush, false ); } }, movePaintTool(event) { var texture = UVEditor.getTexture() if (!texture) { Blockbench.showQuickMessage('message.untextured') } else if (event.which === 1 || (event.touches && event.touches.length == 1)) { var new_face; var {x, y} = UVEditor.getBrushCoordinates(event, texture); if (texture.img.naturalWidth + texture.img.naturalHeight == 0) return; if (x === Painter.current.x && y === Painter.current.y) { return } if (Painter.current.face !== UVEditor.selected_faces[0]) { Painter.current.x = x Painter.current.y = y Painter.current.face = UVEditor.selected_faces[0]; new_face = true; if (texture !== Painter.current.texture && Undo.current_save) { Undo.current_save.addTexture(texture) } } if (Toolbox.selected.id !== 'copy_paste_tool') { Painter.movePaintTool(texture, x, y, event, new_face) } } }, stopBrush(event) { removeEventListeners( UVEditor.vue.$refs.frame, 'mousemove touchmove', UVEditor.movePaintTool, false ); removeEventListeners( document, 'mouseup touchend', UVEditor.stopBrush, false ); if (Toolbox.selected.id !== 'copy_paste_tool') { Painter.stopPaintTool() } else { UVEditor.stopSelection() } }, // Copy Paste Tool startSelection(x, y, event) { if (Painter.selection.overlay && event.target && event.target.id === 'uv_frame') { if (open_interface) { open_interface.confirm() } else { this.removePastingOverlay() } } delete Painter.selection.calcrect; if (!Painter.selection.overlay) { $(this.vue.$refs.frame).find('#texture_selection_rect').detach(); let rect = document.createElement('div'); rect.style.visibility = 'hidden'; rect.id = 'texture_selection_rect'; this.vue.$refs.frame.append(rect) Painter.selection.rect = rect; Painter.selection.start_x = Math.clamp(x, 0, UVEditor.texture ? UVEditor.texture.width : Project.texture_width); Painter.selection.start_y = Math.clamp(y, 0, UVEditor.texture ? UVEditor.texture.height : Project.texture_height); UVEditor.vue.copy_overlay.width = 0; UVEditor.vue.copy_overlay.height = 0; } else { Painter.selection.start_x = Painter.selection.x; Painter.selection.start_y = Painter.selection.y; Painter.selection.start_event = event; } function drag(e1) { let texture = UVEditor.texture; var {x, y} = UVEditor.getBrushCoordinates(e1, texture); if (texture.img.naturalWidth + texture.img.naturalHeight == 0) return; if (x === Painter.current.x && y === Painter.current.y) return; Painter.current.x = x = Math.clamp(x, 0, texture.img.naturalWidth); Painter.current.y = y = Math.clamp(y, 0, texture.img.naturalHeight); UVEditor.dragSelection(x, y, e1); } function stop() { removeEventListeners(document, 'mousemove touchmove', drag); removeEventListeners(document, 'mouseup touchend', stop); UVEditor.stopSelection(); } addEventListeners(document, 'mousemove touchmove', drag); addEventListeners(document, 'mouseup touchend', stop); }, dragSelection(x, y, event) { let m = UVEditor.inner_width / UVEditor.texture.width; if (!Painter.selection.overlay) { let calcrect = getRectangle(Painter.selection.start_x, Painter.selection.start_y, x, y) if (!calcrect.x && !calcrect.y) return; UVEditor.vue.copy_overlay.state = 'select'; Painter.selection.calcrect = calcrect; Painter.selection.x = calcrect.ax; Painter.selection.y = calcrect.ay; UVEditor.vue.copy_overlay.width = calcrect.x; UVEditor.vue.copy_overlay.height = calcrect.y; $(Painter.selection.rect) .css('left', calcrect.ax*m + 'px') .css('top', (calcrect.ay%UVEditor.texture.display_height)*m + 'px') .css('width', calcrect.x *m + 'px') .css('height', calcrect.y *m + 'px') .css('visibility', 'visible') } else if (UVEditor.texture && Painter.selection.canvas) { Painter.selection.x = Painter.selection.start_x + Math.round((event.clientX - Painter.selection.start_event.clientX) / m); Painter.selection.y = Painter.selection.start_y + Math.round((event.clientY - Painter.selection.start_event.clientY) / m); Painter.selection.x = Math.clamp(Painter.selection.x, 1-Painter.selection.canvas.width, UVEditor.texture.width -1) Painter.selection.y = Math.clamp(Painter.selection.y, 1-Painter.selection.canvas.height, UVEditor.texture.height-1) UVEditor.updatePastingOverlay() } }, stopSelection() { if (Painter.selection.rect) { Painter.selection.rect.remove() } if (Painter.selection.overlay || !Painter.selection.calcrect) return; UVEditor.vue.copy_overlay.state = 'off'; if (Painter.selection.calcrect.x == 0 || Painter.selection.calcrect.y == 0) return; let calcrect = Painter.selection.calcrect; var canvas = document.createElement('canvas') var ctx = canvas.getContext('2d'); canvas.width = calcrect.x; canvas.height = calcrect.y; ctx.drawImage(UVEditor.vue.texture.img, -calcrect.ax, -calcrect.ay) if (isApp) { let image = nativeImage.createFromDataURL(canvas.toDataURL()) clipboard.writeImage(image) } Painter.selection.canvas = canvas; UVEditor.addPastingOverlay(); }, addPastingOverlay() { if (Painter.selection.overlay) return; let scope = this; let overlay = $(Interface.createElement('div', {id: 'texture_pasting_overlay'})); UVEditor.vue.copy_overlay.state = 'move'; open_interface = { confirm() { scope.removePastingOverlay() if (scope.texture) { scope.texture.edit((canvas) => { var ctx = canvas.getContext('2d'); let y = (Painter.selection.y % scope.texture.display_height); if (scope.texture.frameCount > 1) y += scope.texture.currentFrame * scope.texture.display_height; ctx.drawImage(Painter.selection.canvas, Painter.selection.x, y) }) } }, hide() { scope.removePastingOverlay() } } overlay.append(Painter.selection.canvas) Painter.selection.overlay = overlay; $(UVEditor.vue.$refs.frame).append(overlay) Painter.selection.x = Math.clamp(Painter.selection.x, 0, this.texture.width-Painter.selection.canvas.width) Painter.selection.y = Math.clamp(Painter.selection.y, 0, this.texture.height-Painter.selection.canvas.height) UVEditor.updatePastingOverlay() function clickElsewhere(event) { if (event.button == 1) return; if (!Painter.selection.overlay) { removeEventListeners(document, 'mousedown touchstart', clickElsewhere) } else if (Painter.selection.overlay.has(event.target).length == 0 && $(scope.vue.$refs.copy_paste_tool_control).has(event.target).length == 0) { open_interface.confirm() } } addEventListeners(document, 'mousedown touchstart', clickElsewhere) }, removePastingOverlay() { Painter.selection.overlay.detach(); UVEditor.vue.copy_overlay.state = 'off'; delete Painter.selection.overlay; open_interface = false; }, updatePastingOverlay() { let m = this.inner_width/this.texture.width $(Painter.selection.canvas) .css('width', Painter.selection.canvas.width * m) .css('height', Painter.selection.canvas.height * m) Painter.selection.overlay .css('left', Painter.selection.x * m) .css('top', (Painter.selection.y%this.texture.display_height) * m); return this; }, focusOnSelection() { let min_x = Project.texture_width; let min_y = Project.texture_height; let max_x = 0; let max_y = 0; let elements = UVEditor.getMappableElements(); elements.forEach(element => { if (element instanceof Cube && Project.box_uv) { let size = element.size(undefined, true) min_x = Math.min(min_x, element.uv_offset[0]); min_y = Math.min(min_y, element.uv_offset[1]); max_x = Math.max(max_x, element.uv_offset[0] + (size[0] + size[2]) * 2); max_y = Math.max(max_y, element.uv_offset[1] + size[1] + size[2]); } else { for (let fkey in element.faces) { if (!UVEditor.selected_faces.includes(fkey)) continue; let face = element.faces[fkey]; if (element instanceof Cube) { min_x = Math.min(min_x, face.uv[0], face.uv[2]); min_y = Math.min(min_y, face.uv[1], face.uv[3]); max_x = Math.max(max_x, face.uv[0], face.uv[2]); max_y = Math.max(max_y, face.uv[1], face.uv[3]); } else if (element instanceof Mesh) { face.vertices.forEach(vkey => { if (!face.uv[vkey]) return; min_x = Math.min(min_x, face.uv[vkey][0]); min_y = Math.min(min_y, face.uv[vkey][1]); max_x = Math.max(max_x, face.uv[vkey][0]); max_y = Math.max(max_y, face.uv[vkey][1]); }) } } } }) let pixel_size = UVEditor.getPixelSize(); let focus = [min_x+max_x, min_y+max_y].map(v => v * 0.5 * pixel_size); let {viewport} = UVEditor.vue.$refs; let margin = UVEditor.vue.getFrameMargin(); $(viewport).animate({ scrollLeft: focus[0] + margin[0] - UVEditor.width / 2, scrollTop: focus[1] + margin[1] - UVEditor.height / 2, }, 100) }, //Get get width() { return this.vue.width; }, get height() { return this.vue.height; }, get zoom() { return this.vue.zoom; }, get inner_width() { return this.vue.inner_width; }, get inner_height() { return this.vue.inner_height; }, get selected_faces() { return this.vue.selected_faces; }, get texture() { return this.vue.texture; }, getPixelSize() { if (Project.box_uv) { return this.inner_width/Project.texture_width } else { return this.inner_width/ ( (typeof this.texture === 'object' && this.texture.width) ? this.texture.width : Project.texture_width ); } }, getFaces(obj, event) { let available = Object.keys(obj.faces) if (event && event.shiftKey) { return available; } else { return UVEditor.vue.selected_faces.filter(key => available.includes(key)); } }, getReferenceFace() { let el = this.getMappableElements()[0]; if (el) { for (let key in el.faces) { if (UVEditor.vue.selected_faces.includes(key)) { return el.faces[key]; } } } }, getMappableElements() { return Outliner.selected.filter(el => typeof el.faces == 'object'); }, getTexture() { if (Format.single_texture) return Texture.getDefault(); return this.vue.texture; }, //Set setZoom(zoom) { zoom = Math.clamp(zoom, 0.5, UVEditor.max_zoom) this.vue.zoom = zoom; Project.uv_viewport.zoom = this.zoom; Vue.nextTick(() => { if (Painter.selection.overlay) UVEditor.updatePastingOverlay() }) return this; }, setGrid(value) { if (value == 'auto') { this.auto_grid = true; this.vue.updateTexture() } else { value = parseInt(value); if (typeof value !== 'number') value = 1; this.grid = Math.clamp(value, 1, 1024); this.auto_grid = false; } this.updateSize(); return this; }, updateSize() { this.vue.updateSize(); }, setFace(face, update = true) { this.vue.selected_faces.replace([face]); return this; }, //Selection reverseSelect(event) { var scope = this; if (!this.vue.texture && !Format.single_texture) return this; if (!event.target.classList.contains('uv_size_handle') && !event.target.id === 'uv_frame') { return this; } var matches = []; var face_matches = []; var u = event.offsetX / this.vue.inner_width * this.getResolution(0); var v = event.offsetY / this.vue.inner_height * this.getResolution(1); Cube.all.forEach(cube => { if (cube.locked) return; for (var face in cube.faces) { var uv = cube.faces[face].uv if (uv && Math.isBetween(u, uv[0], uv[2]) && Math.isBetween(v, uv[1], uv[3]) && (cube.faces[face].getTexture() === scope.vue.texture || Format.single_texture)) { matches.safePush(cube); face_matches.safePush(face); break; } } }) Mesh.all.forEach(mesh => { if (mesh.locked) return; for (var fkey in mesh.faces) { let face = mesh.faces[fkey]; let rect = face.getBoundingRect(); if (face.uv && Math.isBetween(u, rect.ax, rect.bx) && Math.isBetween(v, rect.ay, rect.by) && (face.getTexture() === scope.vue.texture || Format.single_texture)) { matches.safePush(mesh); face_matches.safePush(fkey); break; } } }) if (matches.length) { if (!event.shiftKey && !Pressing.overrides.shift && !event.ctrlOrCmd && !Pressing.overrides.ctrl) { Project.selected_elements.empty(); UVEditor.vue.selected_faces.empty(); } if (!Project.box_uv) { UVEditor.vue.selected_faces.safePush(...face_matches); } matches.forEach(s => { Project.selected_elements.safePush(s) }); if (!event.shiftKey) UVEditor.selectMeshUVIsland(UVEditor.selected_faces[0]); updateSelection(); } return matches; }, forCubes(cb) { var i = 0; while (i < Cube.selected.length) { cb(Cube.selected[i]); i++; } }, forElements(cb) { this.getMappableElements().forEach(cb); }, //Load loadData() { this.vue.updateTexture(); this.displayTools(); this.displayTools(); this.vue.$forceUpdate(); return this; }, applyTexture(texture) { let elements = this.getMappableElements(); Undo.initEdit({elements, uv_only: true}) elements.forEach(el => { this.vue.selected_faces.forEach(face => { if (el.faces[face]) { el.faces[face].texture = texture.uuid; } }) }) this.loadData() Canvas.updateSelectedFaces() Undo.finishEdit('Apply texture') }, displayTools() { if (!this.getMappableElements().length) return; for (var id in UVEditor.sliders) { var slider = UVEditor.sliders[id]; slider.node.style.setProperty('display', BARS.condition(slider.condition)?'block':'none'); slider.update(); } var face = Cube.selected[0] && this.selected_faces[0] && Cube.selected[0].faces[this.selected_faces[0]] if (face) { BarItems.uv_rotation.set((face && face.rotation)||0); BarItems.cullface.set(face.cullface||'off') BarItems.face_tint.setIcon(face.tint !== -1 ? 'check_box' : 'check_box_outline_blank') BarItems.slider_face_tint.update() } }, slidePos(modify, axis) { var scope = this var limit = scope.getResolution(axis); Cube.selected.forEach(function(obj) { if (Project.box_uv === false) { scope.selected_faces.forEach(fkey => { if (!obj.faces[fkey]) return; let uvTag = obj.faces[fkey].uv; var size = uvTag[axis + 2] - uvTag[axis] var value = modify(uvTag[axis]) value = limitNumber(value, 0, limit) value = limitNumber(value + size, 0, limit) - size uvTag[axis] = value uvTag[axis+2] = value + size }) } else { let minimum = 0; if (axis === 0) { var size = (obj.size(0) + (obj.size(1) ? obj.size(2) : 0))*2 if (obj.size(1) == 0) minimum = -obj.size(2); } else { var size = obj.size(2) + obj.size(1) if (obj.size(0) == 0) minimum = -obj.size(2); } var value = modify(obj.uv_offset[axis]) value = limitNumber(value, minimum, limit) value = limitNumber(value + size, minimum, limit) - size obj.uv_offset[axis] = value } obj.preview_controller.updateUV(obj); }) Mesh.selected.forEach(mesh => { let selected_vertices = Project.selected_vertices[mesh.uuid]; if (!selected_vertices) { selected_vertices = []; UVEditor.vue.selected_faces.forEach(fkey => { if (mesh.faces[fkey]) { selected_vertices.safePush(...mesh.faces[fkey].vertices); } }) } UVEditor.vue.selected_faces.forEach(fkey => { if (!mesh.faces[fkey]) return selected_vertices.forEach(vkey => { if (!mesh.faces[fkey].vertices.includes(vkey)) return; mesh.faces[fkey].uv[vkey][axis] = modify(mesh.faces[fkey].uv[vkey][axis]); }) }) mesh.preview_controller.updateUV(mesh); }) this.displayTools() this.vue.$forceUpdate() }, slideSize(modify, axis) { var scope = this var limit = scope.getResolution(axis); Cube.selected.forEach(function(obj) { if (Project.box_uv === false) { scope.selected_faces.forEach(fkey => { if (!obj.faces[fkey]) return; var uvTag = obj.faces[fkey].uv; var difference = modify(uvTag[axis+2]-uvTag[axis]) + uvTag[axis]; uvTag[axis+2] = limitNumber(difference, 0, limit); Canvas.updateUV(obj); }) } }) Mesh.selected.forEach(mesh => { mesh.forAllFaces((face, fkey) => { if (!this.selected_faces.includes(fkey)) return; let rect = face.getBoundingRect(); let start = (axis ? rect.ay : rect.ax); let size = (axis ? rect.y : rect.x); let multiplier = modify(size) / size; face.vertices.forEach(vkey => { if (!face.uv[vkey]) return; face.uv[vkey][axis] = (face.uv[vkey][axis] - start) * multiplier + start; if (isNaN(face.uv[vkey][axis])) face.uv[vkey][axis] = start; }) }) }) this.displayTools() this.disableAutoUV() this.vue.$forceUpdate() }, getResolution(axis, texture) { return axis ? Project.texture_height : Project.texture_width; }, saveViewportOffset() { let uv_viewport = this.vue.$refs.viewport; if (!uv_viewport || Blockbench.hasFlag('switching_project')) return; Project.uv_viewport.offset[0] = (uv_viewport.scrollLeft - this.width/2) / this.vue.inner_width; Project.uv_viewport.offset[1] = (uv_viewport.scrollTop - this.height/2) / this.vue.inner_height; }, loadViewportOffset() { let uv_viewport = this.vue.$refs.viewport; if (!uv_viewport) return; UVEditor.setZoom(Project.uv_viewport.zoom); Vue.nextTick(() => { uv_viewport.scrollLeft = Project.uv_viewport.offset[0] * this.vue.inner_width + this.width/2; uv_viewport.scrollTop = Project.uv_viewport.offset[1] * this.vue.inner_height + this.height/2; }) }, //Events selectAll() { let selected_before = this.vue.selected_faces.length; this.vue.mappable_elements.forEach(element => { for (let key in element.faces) { this.vue.selected_faces.safePush(key); } }) if (selected_before == this.vue.selected_faces.length) { this.vue.selected_faces.empty(); } 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; UVEditor.slidePos((old_val) => { let sign = offset[offset[0] ? 0 : 1]; return old_val + step * sign; }, offset[0] ? 0 : 1); Undo.finishEdit('Move UV') }, disableAutoUV() { this.forCubes(obj => { obj.autouv = 0 }) }, toggleUV() { var state = Cube.selected[0].faces[this.selected_faces[0]].enabled === false this.forCubes(obj => { this.selected_faces.forEach(face => { obj.faces[face].enabled = state; }) }) }, maximize(event) { var scope = this; this.forCubes(obj => { scope.getFaces(obj, event).forEach(function(side) { obj.faces[side].uv = [0, 0, scope.getResolution(0, obj.faces[side]), scope.getResolution(1, obj.faces[side])] }) obj.autouv = 0; Canvas.updateUV(obj) }) this.message('uv_editor.maximized') this.loadData() }, turnMapping(event) { var scope = this; this.forCubes(obj => { scope.getFaces(obj, event).forEach(function(side) { var uv = obj.faces[side].uv_size; obj.faces[side].uv_size = [uv[1], uv[0]]; if (uv[0] < 0) { obj.faces[side].uv[0] += uv[0]; obj.faces[side].uv[2] += uv[0]; obj.faces[side].uv[1] -= uv[0]; obj.faces[side].uv[3] -= uv[0]; } if (uv[1] < 0) { obj.faces[side].uv[1] += uv[1]; obj.faces[side].uv[3] += uv[1]; obj.faces[side].uv[0] -= uv[1]; obj.faces[side].uv[2] -= uv[1]; } let overlap_px = Math.clamp(Math.max(obj.faces[side].uv[0], obj.faces[side].uv[2]) - Project.texture_width, 0, Infinity); obj.faces[side].uv[0] -= overlap_px; obj.faces[side].uv[2] -= overlap_px; let overlap_py = Math.clamp(Math.max(obj.faces[side].uv[1], obj.faces[side].uv[3]) - Project.texture_height, 0, Infinity); obj.faces[side].uv[1] -= overlap_py; obj.faces[side].uv[3] -= overlap_py; }) obj.autouv = 0; Canvas.updateUV(obj); }) this.message('uv_editor.turned'); this.loadData(); }, setAutoSize(event, silent, face_keys) { let vec1 = new THREE.Vector3(), vec2 = new THREE.Vector3(), vec3 = new THREE.Vector3(), vec4 = new THREE.Vector3(), plane = new THREE.Plane(); this.getMappableElements().forEach(obj => { var top2, left2; let faces = face_keys || this.getFaces(obj, event); if (obj instanceof Cube) { faces.forEach(function(side) { let face = obj.faces[side]; let mirror_x = face.uv[0] > face.uv[2]; let mirror_y = face.uv[1] > face.uv[3]; face.uv[0] = Math.min(face.uv[0], face.uv[2]); face.uv[1] = Math.min(face.uv[1], face.uv[3]); if (side == 'north' || side == 'south') { left2 = limitNumber(obj.size('0'), 0, Project.texture_width) top2 = limitNumber(obj.size('1'), 0, Project.texture_height) } else if (side == 'east' || side == 'west') { left2 = limitNumber(obj.size('2'), 0, Project.texture_width) top2 = limitNumber(obj.size('1'), 0, Project.texture_height) } else if (side == 'up' || side == 'down') { left2 = limitNumber(obj.size('0'), 0, Project.texture_width) top2 = limitNumber(obj.size('2'), 0, Project.texture_height) } if (face.rotation % 180) { [left2, top2] = [top2, left2]; } left2 *= UVEditor.getResolution(0, face) / Project.texture_width; top2 *= UVEditor.getResolution(1, face) / Project.texture_height; face.uv_size = [left2, top2]; if (mirror_x) [face.uv[0], face.uv[2]] = [face.uv[2], face.uv[0]]; if (mirror_y) [face.uv[1], face.uv[3]] = [face.uv[3], face.uv[1]]; }) obj.autouv = 0 } else if (obj instanceof Mesh) { faces.forEach(fkey => { let face = obj.faces[fkey]; let vertex_uvs = {}; let uv_center = [0, 0]; let new_uv_center = [0, 0]; let normal_vec = vec1.fromArray(face.getNormal(true)); plane.setFromNormalAndCoplanarPoint( normal_vec, vec2.fromArray(obj.vertices[face.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); face.vertices.forEach(vkey => { let coplanar_pos = plane.projectPoint(vec3.fromArray(obj.vertices[vkey]), vec4.set(0, 0, 0)); coplanar_pos.applyEuler(e); vertex_uvs[vkey] = [ Math.roundTo(coplanar_pos.x, 4), Math.roundTo(coplanar_pos.z, 4), ] }) // Rotate UV to match corners let rotation_angles = {}; let precise_rotation_angle = {}; 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 = 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; } // Find position on UV map let pmin_x = Infinity, pmin_y = Infinity; face.vertices.forEach(vkey => { pmin_x = Math.min(pmin_x, vertex_uvs[vkey][0]); pmin_y = Math.min(pmin_y, vertex_uvs[vkey][1]); }) face.vertices.forEach(vkey => { uv_center[0] += face.uv[vkey] ? face.uv[vkey][0] : 0; uv_center[1] += face.uv[vkey] ? face.uv[vkey][1] : 0; new_uv_center[0] += vertex_uvs[vkey][0]; new_uv_center[1] += vertex_uvs[vkey][1]; }) uv_center[0] = Math.round((uv_center[0] - new_uv_center[0]) / face.vertices.length); uv_center[1] = Math.round((uv_center[1] - new_uv_center[1]) / face.vertices.length); let min_x = Infinity, min_y = Infinity, max_x = 0, max_y = 0; for (let vkey in vertex_uvs) { vertex_uvs[vkey][0] = vertex_uvs[vkey][0] - (pmin_x % 1) + uv_center[0], vertex_uvs[vkey][1] = vertex_uvs[vkey][1] - (pmin_y % 1) + uv_center[1], min_x = Math.min(min_x, vertex_uvs[vkey][0]); min_y = Math.min(min_y, vertex_uvs[vkey][1]); max_x = Math.max(max_x, vertex_uvs[vkey][0]); max_y = Math.max(max_y, vertex_uvs[vkey][1]); } let offset = [ min_x < 0 ? -min_x : (max_x > Project.texture_width ? Math.floor(Project.texture_width - max_x) : 0), min_y < 0 ? -min_y : (max_y > Project.texture_height ? Math.floor(Project.texture_height - max_y) : 0), ]; face.vertices.forEach(vkey => { face.uv[vkey] = [ vertex_uvs[vkey][0] + offset[0], vertex_uvs[vkey][1] + offset[1], ] }) }) } obj.preview_controller.updateUV(obj); }) if (!silent) this.message('uv_editor.autouv'); this.loadData(); }, setRelativeAutoSize(event) { var scope = this; this.forCubes(obj => { scope.getFaces(obj, event).forEach(function(side) { var uv = obj.faces[side].uv, ru = scope.getResolution(0, obj.faces[side]), rv = scope.getResolution(1, obj.faces[side]); switch (side) { case 'north': uv = [ ru - obj.to[0], rv - obj.to[1], ru - obj.from[0], rv - obj.from[1], ]; break; case 'south': uv = [ obj.from[0], rv - obj.to[1], obj.to[0], rv - obj.from[1], ]; break; case 'west': uv = [ obj.from[2], rv - obj.to[1], obj.to[2], rv - obj.from[1], ]; break; case 'east': uv = [ ru - obj.to[2], rv - obj.to[1], ru - obj.from[2], rv - obj.from[1], ]; break; case 'up': uv = [ obj.from[0], obj.from[2], obj.to[0], obj.to[2], ]; break; case 'down': uv = [ obj.from[0], rv - obj.to[2], obj.to[0], rv - obj.from[2], ]; break; } uv.forEach(function(s, uvi) { uv[uvi] = limitNumber(s, 0, uvi%2 ? rv : ru); }) obj.faces[side].uv = uv }) obj.autouv = 0 Canvas.updateUV(obj) }) this.message('uv_editor.autouv') this.loadData() }, mirrorX(event) { var scope = this; this.forElements(obj => { scope.getFaces(obj, event).forEach(function(side) { 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]; }) } }) if (obj.autouv) obj.autouv = 0 obj.preview_controller.updateUV(obj); }) this.message('uv_editor.mirrored') this.loadData() }, mirrorY(event) { var scope = this; this.forElements(obj => { scope.getFaces(obj, event).forEach(function(side) { 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]; }) } }) if (obj.autouv) obj.autouv = 0; obj.preview_controller.updateUV(obj); }) this.message('uv_editor.mirrored') this.loadData() }, applyAll() { this.forCubes(obj => { UVEditor.cube_faces.forEach(side => { obj.faces[side].extend(obj.faces[this.selected_faces[0]]) }) obj.autouv = 0 }) Canvas.updateSelectedFaces() this.message('uv_editor.to_all') this.loadData() }, clear(event) { var scope = this; Undo.initEdit({elements: Cube.selected, uv_only: true}) this.forCubes(obj => { scope.getFaces(obj, event).forEach(function(side) { obj.faces[side].uv = [0, 0, 0, 0] obj.faces[side].texture = null; }) obj.preview_controller.updateFaces(obj); }) this.loadData() this.message('uv_editor.transparent') Undo.finishEdit('Remove face') Canvas.updateSelectedFaces() }, switchCullface(event) { var scope = this; Undo.initEdit({elements: Cube.selected, uv_only: true}) var val = BarItems.cullface.get() if (val === 'off') val = false this.forCubes(obj => { this.selected_faces.forEach(face => { obj.faces[face].cullface = val || ''; }) }) if (val) { this.message('uv_editor.cullface_on') } else { this.message('uv_editor.cullface_off') } Undo.finishEdit('Toggle cullface') }, switchTint(event) { var scope = this; var val = Cube.selected[0].faces[scope.selected_faces[0]].tint === -1 ? 0 : -1; if (event === 0 || event === false) val = event this.forCubes(obj => { this.selected_faces.forEach(face => { obj.faces[face].tint = val; }) }) if (val !== -1) { this.message('uv_editor.tint_on') } else { this.message('uv_editor.tint_off') } this.displayTools() }, setTint(event, val) { this.forCubes(obj => { this.selected_faces.forEach(face => { obj.faces[face].tint = val; }) }) this.displayTools() }, rotate(mesh_angle) { var value = parseInt(BarItems.uv_rotation.get()); if (Cube.selected[0] && Cube.selected[0].faces[this.selected_faces] && Math.abs(Cube.selected[0].faces[this.selected_faces].rotation - value) % 180 == 90) { UVEditor.turnMapping(); } this.forCubes(obj => { this.selected_faces.forEach(face => { obj.faces[face].rotation = value; }) Canvas.updateUV(obj); }) Mesh.selected.forEach(mesh => { mesh.forAllFaces((face, fkey) => { if (!UVEditor.selected_faces.includes(fkey)) return; if (face.vertices.length < 3) return; let center = [0, 0]; face.vertices.forEach(vkey => { if (!face.uv[vkey]) return; center[0] += face.uv[vkey][0]; center[1] += face.uv[vkey][1]; }) center[0] /= face.vertices.length; center[1] /= face.vertices.length; face.vertices.forEach(vkey => { if (!face.uv[vkey]) return; let sin = Math.sin(Math.degToRad(mesh_angle)); let cos = Math.cos(Math.degToRad(mesh_angle)); face.uv[vkey][0] -= center[0]; face.uv[vkey][1] -= center[1]; let a = (face.uv[vkey][0] * cos - face.uv[vkey][1] * sin); let b = (face.uv[vkey][0] * sin + face.uv[vkey][1] * cos); face.uv[vkey][0] = Math.clamp(a + center[0], 0, Project.texture_width); face.uv[vkey][1] = Math.clamp(b + center[1], 0, Project.texture_height); }) }) Mesh.preview_controller.updateUV(mesh); }) this.loadData(); this.message('uv_editor.rotated') }, setRotation(value) { var scope = this; value = parseInt(value) this.forCubes(obj => { this.selected_faces.forEach(face => { obj.faces[face].rotation = value; }) Canvas.updateUV(obj) }) this.loadData() this.message('uv_editor.rotated') }, selectGridSize(event) { }, autoCullface(event) { var scope = this; this.forCubes(obj => { scope.getFaces(obj, event).forEach(function(side) { obj.faces[side].cullface = side }) }) this.loadData() this.message('uv_editor.auto_cull') }, copy(event) { let elements = this.getMappableElements(); if (!elements.length) return; UVEditor.clipboard = [] if (Project.box_uv && Cube.selected[0]) { var new_tag = { uv: Cube.selected[0].uv_offset } UVEditor.clipboard.push(new_tag) this.message('uv_editor.copied') return; } function addToClipboard(key) { let element = elements.find(el => el.faces[key]); if (!element) return; var tag = element.faces[key]; var new_face; if (element instanceof Mesh) { new_face = new MeshFace(null, tag); new_face.vertices = tag.getSortedVertices(); new_face.direction = key; } else { new_face = new CubeFace(key, tag); } UVEditor.clipboard.push(new_face); } if (event.shiftKey) { for (let key in elements[0].faces) { addToClipboard(key) } } else { UVEditor.vue.selected_faces.forEach(key => { addToClipboard(key); }) } this.message('uv_editor.copied_x', [UVEditor.clipboard.length]) }, paste(event) { let elements = UVEditor.getMappableElements(); if (UVEditor.clipboard === null || elements.length === 0) return; Undo.initEdit({elements, uv_only: true}) if (Project.box_uv && UVEditor.clipboard[0].uv instanceof Array) { Cube.selected.forEach(function(el) { el.uv_offset = UVEditor.clipboard[0].uv.slice() el.preview_controller.updateUV(el); }) } function mergeFace(element, key, tag) { if (!element.faces[key]) return; let face = element.faces[key]; if (element instanceof Mesh) { let uv_points = []; tag.vertices.forEach(vkey => { uv_points.push(tag.uv[vkey]); }) face.getSortedVertices().forEach((vkey, i) => { if (uv_points[i]) face.uv[vkey].replace(uv_points[i]); }) } else { face.extend(tag); } } let shifting = (event && event.shiftKey) || Pressing.overrides.shift; if (shifting || UVEditor.clipboard.length === 1) { let tag = UVEditor.clipboard[0]; elements.forEach(el => { if (Project.box_uv && el instanceof Cube) return; if ((el instanceof Cube && tag instanceof CubeFace) || (el instanceof Mesh && tag instanceof MeshFace)) { for (let key in el.faces) { if (shifting || UVEditor.vue.selected_faces.includes(key)) { mergeFace(el, key, tag); } } el.preview_controller.updateUV(el); el.preview_controller.updateFaces(el); } }) } else { UVEditor.clipboard.forEach(tag => { elements.forEach(el => { if (Project.box_uv && el instanceof Cube) return; if ((el instanceof Cube && tag instanceof CubeFace) || (el instanceof Mesh && tag instanceof MeshFace)) { let key = tag.direction; if (el.faces[key]) { mergeFace(el, key, tag); } } }) }) elements.forEach(el => { el.preview_controller.updateUV(el); el.preview_controller.updateFaces(el); }) } this.loadData() Canvas.updateSelectedFaces() this.message('uv_editor.pasted') Undo.finishEdit('Paste UV') }, reset(event) { var scope = this; this.forCubes(obj => { scope.getFaces(obj, event).forEach(function(side) { obj.faces[side].reset() }) obj.preview_controller.updateFaces(obj); obj.preview_controller.updateUV(obj); }) this.loadData() this.message('uv_editor.reset') }, // Dialog clipboard: null, cube_faces: ['north', 'south', 'west', 'east', 'up', 'down'], forSelection(cb, event, ...args) { UVEditor[cb](event, ...args); }, menu: new Menu([ {name: 'menu.view.zoom', id: 'zoom', icon: 'search', children: [ 'zoom_in', 'zoom_out', 'zoom_reset' ]}, {name: 'menu.uv.display_uv', id: 'display_uv', icon: 'visibility', children: () => { let options = ['selected_faces', 'selected_elements', 'all_elements']; return options.map(option => {return { id: option, name: `menu.uv.display_uv.${option}`, icon: UVEditor.vue.display_uv == option ? 'radio_button_checked' : 'radio_button_unchecked', condition: !(option == 'selected_faces' && Project.box_uv && !Mesh.selected.length), click() { 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(); } }}) }}, 'focus_on_selection', 'uv_checkerboard', 'paint_mode_uv_overlay', '_', 'copy', 'paste', {icon: 'photo_size_select_large', name: 'menu.uv.mapping', condition: () => !Project.box_uv && UVEditor.getReferenceFace(), 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]; } } } 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.toggleUV(event) Undo.finishEdit('Toggle UV export') }}, 'uv_maximize', 'uv_auto', 'uv_rel_auto', 'snap_uv_to_pixels', 'uv_rotate_left', 'uv_rotate_right', {icon: 'rotate_90_degrees_ccw', condition: () => reference_face instanceof CubeFace && 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°', 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°', 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°', 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°', 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'; var on = 'radio_button_checked'; function setCullface(cullface) { Undo.initEdit({elements: Cube.selected, uv_only: true}) UVEditor.forCubes(obj => { UVEditor.selected_faces.forEach(face => { obj.faces[face].cullface = cullface; }) }) Undo.finishEdit(cullface ? `Set cullface to ${cullface}` : 'Disable cullface'); } return [ {icon: (!UVEditor.getReferenceFace().cullface ? on : off), name: 'uv_editor.no_faces', click: () => setCullface('')}, {icon: (UVEditor.getReferenceFace().cullface == 'north' ? on : off), name: 'face.north', click: () => setCullface('north')}, {icon: (UVEditor.getReferenceFace().cullface == 'south' ? on : off), name: 'face.south', click: () => setCullface('south')}, {icon: (UVEditor.getReferenceFace().cullface == 'west' ? on : off), name: 'face.west', click: () => setCullface('west')}, {icon: (UVEditor.getReferenceFace().cullface == 'east' ? on : off), name: 'face.east', click: () => setCullface('east')}, {icon: (UVEditor.getReferenceFace().cullface == 'up' ? on : off), name: 'face.up', click: () => setCullface('up')}, {icon: (UVEditor.getReferenceFace().cullface == 'down' ? on : off), name: 'face.down', click: () => setCullface('down')}, 'auto_cullface' ] }}, {icon: 'collections', name: 'menu.uv.texture', condition: () => UVEditor.getReferenceFace() && !Project.single_texture, children: function() { var arr = [ {icon: 'crop_square', name: 'menu.cube.texture.blank', click: function(context, event) { let elements = UVEditor.vue.mappable_elements; Undo.initEdit({elements}) elements.forEach((obj) => { UVEditor.getFaces(obj, event).forEach(function(side) { obj.faces[side].texture = false; }) obj.preview_controller.updateFaces(obj); }) UVEditor.loadData() UVEditor.message('uv_editor.reset') Undo.finishEdit('Apply blank texture') }}, {icon: 'clear', name: 'menu.cube.texture.transparent', condition: () => UVEditor.getReferenceFace() instanceof CubeFace, click: function() {UVEditor.clear(event)}}, ] Texture.all.forEach(function(t) { arr.push({ name: t.name, icon: (t.mode === 'link' ? t.img : t.source), click: function() {UVEditor.applyTexture(t)} }) }) return arr; }} ]) } BARS.defineActions(function() { new BarSlider('uv_rotation', { category: 'uv', condition: () => !Project.box_uv && Format.uv_rotation && Cube.selected.length, min: 0, max: 270, step: 90, width: 80, onBefore: () => { Undo.initEdit({elements: Cube.selected, uv_only: true}) }, onChange: function(slider) { UVEditor.rotate(); }, onAfter: () => { Undo.finishEdit('Rotate UV') } }) new BarSelect('uv_grid', { category: 'uv', condition: () => !Project.box_uv && Cube.selected.length, min_width: 68, value: 'auto', options: { 'auto': 'Pixel', '1x': '1x', '2x': '2x', '3x': '3x', '4x': '4x', '6x': '6x', '8x': '8x', }, onChange: function(slider) { var value = slider.get().replace(/x/, ''); UVEditor.setGrid(value); } }) new Action('uv_maximize', { icon: 'zoom_out_map', category: 'uv', condition: () => !Project.box_uv && Cube.selected.length, click: function (event) { Undo.initEdit({elements: Cube.selected, uv_only: true}) UVEditor.forSelection('maximize', event) Undo.finishEdit('Maximize UV') } }) new Action('uv_turn_mapping', { icon: 'screen_rotation', category: 'uv', condition: () => !Project.box_uv && Cube.selected.length, click: function (event) { Undo.initEdit({elements: Cube.selected, uv_only: true}) UVEditor.forSelection('turnMapping', event) Undo.finishEdit('Turn UV mapping') } }) new Action('uv_auto', { icon: 'brightness_auto', category: 'uv', condition: () => (!Project.box_uv && Cube.selected.length) || Mesh.selected.length, click: function (event) { Undo.initEdit({elements: UVEditor.getMappableElements(), uv_only: true}) UVEditor.forSelection('setAutoSize', event) Undo.finishEdit('Auto UV') } }) new Action('uv_rel_auto', { icon: 'brightness_auto', category: 'uv', condition: () => !Project.box_uv && Cube.selected.length, click: function (event) { Undo.initEdit({elements: Cube.selected, uv_only: true}) UVEditor.forSelection('setRelativeAutoSize', event) Undo.finishEdit('Auto UV') } }) new Action('uv_mirror_x', { icon: 'icon-mirror_x', category: 'uv', condition: () => !Project.box_uv && UVEditor.getMappableElements().length, click: function (event) { Undo.initEdit({elements: Cube.selected, uv_only: true}) UVEditor.forSelection('mirrorX', event) Undo.finishEdit('Mirror UV') } }) new Action('uv_mirror_y', { icon: 'icon-mirror_y', category: 'uv', condition: () => !Project.box_uv && UVEditor.getMappableElements().length, click: function (event) { Undo.initEdit({elements: Cube.selected, uv_only: true}) UVEditor.forSelection('mirrorY', event) Undo.finishEdit('Mirror UV') } }) new Action('uv_rotate_left', { icon: 'rotate_left', category: 'uv', condition: () => Mesh.selected.length, click: function (event) { Undo.initEdit({elements: Mesh.selected, uv_only: true}) UVEditor.rotate(-90); Undo.finishEdit('Rotate UV left'); } }) new Action('uv_rotate_right', { icon: 'rotate_right', category: 'uv', condition: () => Mesh.selected.length, click: function (event) { Undo.initEdit({elements: Mesh.selected, uv_only: true}) UVEditor.rotate(90); Undo.finishEdit('Rotate UV right'); } }) new Action('uv_transparent', { icon: 'clear', category: 'uv', condition: () => !Project.box_uv && Cube.selected.length, click: function (event) { UVEditor.forSelection('clear', event) } }) new Action('uv_reset', { icon: 'replay', category: 'uv', condition: () => !Project.box_uv && Cube.selected.length, click: function (event) { Undo.initEdit({elements: Cube.selected, uv_only: true}) UVEditor.forSelection('reset', event) Undo.finishEdit('Reset UV') } }) new Action('uv_apply_all', { icon: 'format_color_fill', category: 'uv', condition: () => !Project.box_uv && Cube.selected.length, click: function (e) { Undo.initEdit({elements: Cube.selected, uv_only: true}) UVEditor.applyAll(e) Undo.finishEdit('Apply UV to all faces') } }) new BarSelect('cullface', { category: 'uv', condition: () => !Project.box_uv && Format.id == 'java_block' && Cube.selected.length && UVEditor.selected_faces[0], label: true, options: { off: tl('uv_editor.no_faces'), north: tl('face.north'), south: tl('face.south'), west: tl('face.west'), east: tl('face.east'), up: tl('face.up'), down: tl('face.down'), }, onChange: function(sel, event) { Undo.initEdit({elements: Cube.selected, uv_only: true}); UVEditor.forSelection('switchCullface'); Undo.finishEdit('Set cullface'); } }) new Action('auto_cullface', { icon: 'block', category: 'uv', condition: () => !Project.box_uv && Format.id == 'java_block' && Cube.selected.length && UVEditor.selected_faces[0], click: function (event) { Undo.initEdit({elements: Cube.selected, uv_only: true}) UVEditor.forSelection('autoCullface', event) Undo.finishEdit('Set automatic cullface') } }) new Action('face_tint', { category: 'uv', condition: () => !Project.box_uv && Format.id == 'java_block' && Cube.selected.length && UVEditor.selected_faces[0], click: function (event) { Undo.initEdit({elements: Cube.selected, uv_only: true}) UVEditor.forSelection('switchTint', event) Undo.finishEdit('Toggle face tint') } }) new NumSlider('slider_face_tint', { category: 'uv', condition: () => !Project.box_uv && Format.id == 'java_block' && Cube.selected.length && UVEditor.selected_faces[0] && Cube.selected[0].faces[UVEditor.selected_faces[0]], getInterval(event) { return 1; }, get: function() { return Cube.selected[0] && Cube.selected[0].faces[UVEditor.selected_faces[0]].tint }, change: function(modify) { let number = Math.clamp(Math.round(modify(this.get())), -1) UVEditor.setTint(null, number); }, onBefore: function() { Undo.initEdit({elements: Cube.selected, uv_only: true}) }, onAfter: function() { Undo.finishEdit('Set face tint') } }) new Action('snap_uv_to_pixels', { icon: 'grid_goldenratio', category: 'uv', condition: () => !Project.box_uv && UVEditor.getMappableElements().length, click: function (event) { let elements = UVEditor.getMappableElements(); Undo.initEdit({elements, uv_only: true}) elements.forEach(element => { let selected_vertices = element instanceof Mesh && element.getSelectedVertices(); UVEditor.selected_faces.forEach(fkey => { if (!element.faces[fkey]) return; let face = element.faces[fkey]; if (element instanceof Mesh) { face.vertices.forEach(vkey => { if ((!selected_vertices.length || selected_vertices.includes(vkey)) && face.uv[vkey]) { face.uv[vkey][0] = Math.clamp(Math.round(face.uv[vkey][0]), 0, Project.texture_width); face.uv[vkey][1] = Math.clamp(Math.round(face.uv[vkey][1]), 0, Project.texture_height); } }) } else if (element instanceof Cube) { face.uv[0] = Math.clamp(Math.round(face.uv[0]), 0, Project.texture_width); face.uv[1] = Math.clamp(Math.round(face.uv[1]), 0, Project.texture_height); face.uv[2] = Math.clamp(Math.round(face.uv[2]), 0, Project.texture_width); face.uv[3] = Math.clamp(Math.round(face.uv[3]), 0, Project.texture_height); } }) element.preview_controller.updateUV(element); }) UVEditor.loadData(); Undo.finishEdit('Set automatic cullface') } }) new Toggle('paint_mode_uv_overlay', { icon: 'splitscreen', category: 'animation', condition: {modes: ['paint']}, onChange(value) { UVEditor.vue.uv_overlay = value; } }) new Toggle('move_texture_with_uv', { icon: 'fas.fa-link', category: 'uv', condition: {modes: ['edit']} }) }) Interface.definePanels(function() { function getCanvasCopy() { var temp_canvas = document.createElement('canvas') var temp_ctx = temp_canvas.getContext('2d'); temp_canvas.width = Painter.selection.canvas.width; temp_canvas.height = Painter.selection.canvas.height; temp_ctx.drawImage(Painter.selection.canvas, 0, 0) return temp_canvas } let copy_overlay = { state: 'off', width: 0, height: 0, doPlace() { open_interface.confirm(); }, doCancel() { open_interface.hide(); }, doCut(e) { UVEditor.removePastingOverlay() UVEditor.texture.edit((canvas) => { var ctx = canvas.getContext('2d'); ctx.clearRect(Painter.selection.x, Painter.selection.y, Painter.selection.canvas.width, Painter.selection.canvas.height); }) }, doMirror_x(e) { let temp_canvas = getCanvasCopy() let ctx = Painter.selection.canvas.getContext('2d'); ctx.save(); ctx.translate(ctx.canvas.width, 0); ctx.scale(-1, 1); ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.drawImage(temp_canvas, ctx.canvas.width, 0, -ctx.canvas.width, ctx.canvas.height); ctx.restore(); UVEditor.updatePastingOverlay() }, doMirror_y(e) { let temp_canvas = getCanvasCopy() let ctx = Painter.selection.canvas.getContext('2d'); ctx.save(); ctx.translate(0, ctx.canvas.height); ctx.scale(1, -1); ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.drawImage(temp_canvas, 0, ctx.canvas.height, ctx.canvas.width, -ctx.canvas.height); ctx.restore(); }, doRotate(e) { let temp_canvas = getCanvasCopy() let ctx = Painter.selection.canvas.getContext('2d'); [ctx.canvas.width, ctx.canvas.height] = [ctx.canvas.height, ctx.canvas.width] ctx.save(); ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.translate(ctx.canvas.width/2,ctx.canvas.height/2); ctx.rotate(Math.PI/2); ctx.drawImage(temp_canvas,-temp_canvas.width/2,-temp_canvas.height/2); ctx.restore(); UVEditor.updateSize() }, } UVEditor.panel = new Panel('uv', { icon: 'photo_size_select_large', selection_only: true, expand_button: true, condition: {modes: ['edit', 'paint']}, display_condition: () => UVEditor.getMappableElements().length || Modes.paint, default_position: { slot: 'left_bar', float_position: [300, 0], float_size: [500, 600], height: 500 }, toolbars: { bottom: Toolbars.UVEditor }, onResize: function() { UVEditor.vue.hidden = !this.isVisible(); Vue.nextTick(() => { UVEditor.vue.updateSize(); }) }, onFold: function() { Vue.nextTick(() => { UVEditor.vue.hidden = !this.isVisible(); }) }, component: { data() {return { mode: 'uv', hidden: false, box_uv: false, width: 320, height: 320, zoom: 1, centered_view: true, checkerboard: settings.uv_checkerboard.value, uv_overlay: false, texture: 0, mouse_coords: {x: -1, y: -1}, helper_lines: {x: -1, y: -1}, selection_rect: { pos_x: 0, pos_y: 0, width: 0, height: 0, active: false }, copy_overlay, project_resolution: [16, 16], elements: [], all_elements: [], selected_vertices: {}, selected_faces: [], display_uv: 'selected_elements', face_names: { north: tl('face.north'), south: tl('face.south'), west: tl('face.west'), east: tl('face.east'), up: tl('face.up'), down: tl('face.down'), } }}, computed: { inner_width() { let axis = this.project_resolution[0] / this.project_resolution[1] < this.width / this.height; if (axis) { return this.height * this.zoom * (this.project_resolution[0] / this.project_resolution[1]); } else { return this.width * this.zoom; } }, inner_height() { return this.height * this.zoom; }, mappable_elements() { return this.elements.filter(element => element.faces && !element.locked); }, all_mappable_elements() { return this.all_elements.filter(element => element.faces && !element.locked); } }, watch: { project_resolution: { deep: true, handler() { let min_zoom = Math.min(1, this.inner_width/this.inner_height); if (this.zoom < min_zoom) UVEditor.setZoom(1); } }, mode() { Vue.nextTick(() => { this.updateSize(); }) } }, methods: { projectResolution() { BarItems.project_window.trigger() }, updateSize() { if (!this.$refs.viewport) return; let old_size = this.width; let size = Math.floor(Math.clamp(UVEditor.panel.width - 10, 64, 1e5)); this.width = size; if (Panels.uv.slot.includes('_bar')) { this.height = size * Math.clamp(this.project_resolution[1] / this.project_resolution[0], 0.5, 1); } else { this.height = Math.clamp( UVEditor.panel.height -UVEditor.panel.handle.clientHeight - 8 -(this.$refs.uv_cube_face_bar ? this.$refs.uv_cube_face_bar.clientHeight : 0) -(this.$refs.uv_toolbars ? this.$refs.uv_toolbars.clientHeight : 0) -(this.mode == 'paint' ? 30 : 0), 64, 1e5); } this.$refs.viewport.scrollLeft = Math.round(this.$refs.viewport.scrollLeft * (size / old_size)); this.$refs.viewport.scrollTop = Math.round(this.$refs.viewport.scrollTop * (size / old_size)); let slider_bar_width = UVEditor.vue.$refs.slider_bar.clientWidth - 6; for (var id in UVEditor.sliders) { var slider = UVEditor.sliders[id]; slider.setWidth(slider_bar_width / (Project.box_uv?2:4)-1); } if (this.$refs.viewport && this.zoom == 1 && ((!this.$refs.viewport.scrollLeft && !this.$refs.viewport.scrollTop) || this.centered_view)) { this.centerView(); } }, centerView() { this.$refs.viewport.scrollLeft = this.width/2; this.$refs.viewport.scrollTop = this.height/2; this.centered_view = true; }, setMode(mode) { this.mouse_coords.x = this.mouse_coords.y = -1; this.mode = mode; this.updateTexture(); }, updateTexture() { let texture; if (Format.single_texture) { texture = Texture.getDefault(); } else { let elements = UVEditor.getMappableElements(); if (elements.length) { for (let element of elements) { let face = element.faces[ this.selected_faces[0] || Object.keys(element.faces)[0] ]; if (face) texture = face.getTexture() || texture; if (texture) break; } } else if (Modes.paint) { texture = Texture.getDefault(); } } if (texture === null) { this.texture = null; } else if (texture instanceof Texture) { this.texture = texture; if (!Project.box_uv && UVEditor.auto_grid) { UVEditor.grid = texture.width / Project.texture_width; } } else { this.texture = 0; } }, updateMouseCoords(event) { convertTouchEvent(event); var pixel_size = this.inner_width / (this.texture ? this.texture.width : this.project_resolution[0]); if (Toolbox.selected.id === 'copy_paste_tool') { this.mouse_coords.x = Math.round(event.offsetX/pixel_size*1); this.mouse_coords.y = Math.round(event.offsetY/pixel_size*1); } else { let offset = BarItems.slider_brush_size.get()%2 == 0 && Toolbox.selected.brushTool ? 0.5 : 0; this.mouse_coords.x = Math.floor(event.offsetX/pixel_size*1 + offset); this.mouse_coords.y = Math.floor(event.offsetY/pixel_size*1 + offset); } if (this.texture && this.texture.frameCount) { this.mouse_coords.y += (this.texture.height / this.texture.frameCount) * this.texture.currentFrame } }, onMouseWheel(event) { if (event.ctrlOrCmd) { event.stopPropagation() event.preventDefault() let original_margin = this.getFrameMargin(); let old_zoom = this.zoom; var n = (event.deltaY < 0) ? 0.1 : -0.1; n *= this.zoom let zoom = this.zoom + n; if (zoom > 0.91 && zoom < 1.1) zoom = 1; UVEditor.setZoom(zoom); let updateScroll = () => { let {viewport} = this.$refs; let offset = $(this.$refs.viewport).offset() let margin = this.getFrameMargin(); let offsetX = event.clientX - offset.left - margin[0]; let offsetY = event.clientY - offset.top - margin[1]; // Make it a bit easier to scroll into corners offsetX = (offsetX - this.width/2 + margin[0]) * 1.1 + this.width/2 - margin[0]; offsetY = (offsetY - this.height/2 + margin[1]) * 1.1 + this.height/2 - margin[1]; let zoom_diff = this.zoom - old_zoom; viewport.scrollLeft += ((viewport.scrollLeft + offsetX) * zoom_diff) / old_zoom + margin[0] - original_margin[0]; viewport.scrollTop += ((viewport.scrollTop + offsetY) * zoom_diff) / old_zoom + margin[1] - original_margin[1]; if (this.zoom == 1 && Panels.uv.isInSidebar()) { this.centerView(); } this.updateMouseCoords(event) //if (Painter.selection.overlay) UVEditor.updatePastingOverlay() } if (n > 0) { Vue.nextTick(updateScroll); } else { updateScroll(); } return false; } }, onMouseDown(event) { setActivePanel('uv'); if (event.target && event.target.id === 'uv_viewport') return; let scope = this; let second_touch; let original_zoom = this.zoom; let original_margin = scope.getFrameMargin(); let offset = $(scope.$refs.viewport).offset(); UVEditor.total_zoom_offset = [6, 6]; if (event.which === 2 || (event.touches && !Toolbox.selected.paintTool && event.target.id == 'uv_frame')) { // Drag if (event.touches) { event.clientX = event.touches[0].clientX; event.clientY = event.touches[0].clientY; } let {viewport} = this.$refs; let margin = this.getFrameMargin(); let margin_center = [this.width/2, this.height/2]; let original = [ viewport.scrollLeft, viewport.scrollTop ]; function dragMouseWheel(e2) { if (e2.touches) { e2.clientX = e2.touches[0].clientX; e2.clientY = e2.touches[0].clientY; if (!second_touch && e2.touches[1]) { second_touch = e2.touches[1]; } if (second_touch && e2.touches[1]) { let factor = Math.sqrt(Math.pow(e2.touches[0].clientX - e2.touches[1].clientX, 2) + Math.pow(e2.touches[0].clientY - e2.touches[1].clientY, 2)) / Math.sqrt(Math.pow(event.touches[0].clientX - second_touch.clientX, 2) + Math.pow(event.touches[0].clientY - second_touch.clientY, 2)); if (!Math.epsilon(scope.zoom, original_zoom * factor, 0.01)) { UVEditor.setZoom(original_zoom * factor); let margin = scope.getFrameMargin(); let offsetX = e2.clientX - offset.left - margin[0]; let offsetY = e2.clientY - offset.top - margin[1]; let zoom_diff = scope.zoom - original_zoom; UVEditor.total_zoom_offset[0] = ((original[0] + event.clientX - e2.clientX + offsetX) * zoom_diff) / original_zoom + margin[0] - original_margin[0]; UVEditor.total_zoom_offset[1] = ((original[1] + event.clientY - e2.clientY + offsetY) * zoom_diff) / original_zoom + margin[1] - original_margin[1]; } } } viewport.scrollLeft = Math.snapToValues(original[0] + event.clientX - e2.clientX + UVEditor.total_zoom_offset[0], [margin[0], margin_center[0]], 10); viewport.scrollTop = Math.snapToValues(original[1] + event.clientY - e2.clientY + UVEditor.total_zoom_offset[1], [margin[1], margin_center[1]], 10); UVEditor.vue.centered_view = (viewport.scrollLeft == margin[0] || viewport.scrollLeft == margin_center[0]) && (viewport.scrollTop == margin[1] || viewport.scrollTop == margin_center[1]); } function dragMouseWheelStop(e) { removeEventListeners(document, 'mousemove touchmove', dragMouseWheel); removeEventListeners(document, 'mouseup touchend', dragMouseWheelStop); } addEventListeners(document, 'mousemove touchmove', dragMouseWheel); addEventListeners(document, 'mouseup touchend', dragMouseWheelStop); event.preventDefault(); return false; } else if (this.mode == 'paint' && Toolbox.selected.paintTool && (event.which === 1 || (event.touches && event.touches.length == 1))) { // Paint UVEditor.startPaintTool(event); event.preventDefault(); return false; } else if (this.mode == 'uv' && event.target.id == 'uv_frame' && (event.which === 1 || (event.touches && event.touches.length == 1))) { if (event.altKey || Pressing.overrides.alt) { return this.dragFace(null, event); } let {selection_rect} = this; let scope = this; let old_faces = this.selected_faces.slice(); let old_selected_vertices = {}; Mesh.selected.forEach(mesh => { old_selected_vertices[mesh.uuid] = mesh.getSelectedVertices().slice(); }) let old_elements; if (Project.box_uv) { old_elements = UVEditor.getMappableElements().slice(); } function drag(e1) { selection_rect.active = true; let rect = getRectangle( event.offsetX / scope.inner_width * scope.project_resolution[0], event.offsetY / scope.inner_height * scope.project_resolution[1], (event.offsetX - event.clientX + e1.clientX) / scope.inner_width * scope.project_resolution[0], (event.offsetY - event.clientY + e1.clientY) / scope.inner_height * scope.project_resolution[1], ) selection_rect.pos_x = rect.ax; selection_rect.pos_y = rect.ay; selection_rect.width = rect.x; selection_rect.height = rect.y; if (!e1.shiftKey) { scope.selected_faces.empty(); if (old_elements) Outliner.selected.empty(); } else { scope.selected_faces.replace(old_faces); if (old_elements) Outliner.selected.replace(old_elements); } let elements; if (Project.box_uv) { elements = Cube.all.filter(cube => !cube.locked); elements.safePush(UVEditor.getMappableElements()); } else { elements = UVEditor.getMappableElements(); } elements.forEach(element => { if (element instanceof Cube && !Project.box_uv) { for (let fkey in element.faces) { let face_rect = getRectangle(...element.faces[fkey].uv); if (doRectanglesOverlap(rect, face_rect)) { scope.selected_faces.safePush(fkey); } } } else if (element instanceof Cube) { let overlaps = false; for (let fkey in element.faces) { let face_rect = getRectangle(...element.faces[fkey].uv); if (doRectanglesOverlap(rect, face_rect)) { overlaps = true; break; } } if (overlaps) { Outliner.selected.safePush(element); } } else if (element instanceof Mesh) { let selected_vertices = element.getSelectedVertices(true); if (!e1.shiftKey) { selected_vertices.empty(); } else { selected_vertices.replace(old_selected_vertices[element.uuid]); } for (let fkey in element.faces) { let face = element.faces[fkey]; let vertices = face.getSortedVertices(); if (vertices.length >= 3) { let i = 0; for (let vkey of vertices) { i++; let vkey2 = vertices[i] || vertices[0]; if (lineIntersectsReactangle(face.uv[vkey], face.uv[vkey2], [rect.ax, rect.ay], [rect.bx, rect.by])) { scope.selected_faces.safePush(fkey); } if (pointInRectangle(face.uv[vkey], [rect.ax, rect.ay], [rect.bx, rect.by])) { selected_vertices.safePush(vkey); } } } } } }) if (old_elements) updateSelection(); UVEditor.displayTools(); } function stop() { removeEventListeners(document, 'mousemove touchmove', drag); removeEventListeners(document, 'mouseup touchend', stop); setTimeout(() => { selection_rect.active = false; }, 1) } addEventListeners(document, 'mousemove touchmove', drag, false); addEventListeners(document, 'mouseup touchend', stop, false); } }, onMouseLeave(event) { if (this.mode == 'paint') { this.mouse_coords.x = -1; } }, contextMenu(event) { setActivePanel('uv'); UVEditor.menu.open(event); }, selectTextureMenu(event) { let menu = new Menu(Texture.all.map(tex => { return { name: tex.name, icon: tex.img, click(event) { tex.select(event); } } })) menu.open(event.target); }, selectFace(key, event, keep_selection, support_dragging) { if (keep_selection && this.selected_faces.includes(key)) { } else if (event.shiftKey || event.ctrlOrCmd || Pressing.overrides.shift || Pressing.overrides.ctrl) { if (this.selected_faces.includes(key)) { this.selected_faces.remove(key); } else { this.selected_faces.push(key); } } else { this.selected_faces.replace([key]); } UVEditor.vue.updateTexture(); UVEditor.displayTools(); if (support_dragging) { let scope = this; function drag(e1) { if (e1.target && e1.target.nodeName == 'LI' && e1.target.parentElement.id == 'uv_cube_face_bar') { let face = e1.target.attributes.face.value; scope.selected_faces.safePush(face); UVEditor.displayTools(); } } function stop() { removeEventListeners(document, 'mousemove touchmove', drag); removeEventListeners(document, 'mouseup touchend', stop); } addEventListeners(document, 'mousemove touchmove', drag); addEventListeners(document, 'mouseup touchend', stop); } }, selectCube(cube, event) { if (!this.dragging_uv) { cube.select(event); } UVEditor.vue.updateTexture() }, reverseSelect(event) { if (this.mode !== 'uv') return; var offset = $(this.$refs.frame).offset(); event.offsetX = event.clientX - offset.left; event.offsetY = event.clientY - offset.top; if (!this.dragging_uv && !this.selection_rect.active && event.target.id == 'uv_frame') { let results = UVEditor.reverseSelect(event) if (!(results && results.length)) { if (!this.box_uv) { this.selected_faces.empty(); } } } }, drag({event, onDrag, onEnd, onAbort, snap}) { if (event.which == 2 || event.which == 3) return; convertTouchEvent(event); let scope = this; let pos = [0, 0]; let last_pos = [0, 0]; function drag(e1) { convertTouchEvent(e1); if (snap == undefined) { let snap = UVEditor.grid / canvasGridSize(e1.shiftKey || Pressing.overrides.shift, e1.ctrlOrCmd || Pressing.overrides.ctrl); let step_x = (scope.inner_width / UVEditor.getResolution(0) / snap); let step_y = (scope.inner_height / UVEditor.getResolution(1) / snap); pos[0] = Math.round((e1.clientX - event.clientX) / step_x) / snap; pos[1] = Math.round((e1.clientY - event.clientY) / step_y) / snap; } else { let step_x = snap let step_y = snap pos[0] = Math.round((e1.clientX - event.clientX) / step_x) / snap; pos[1] = Math.round((e1.clientY - event.clientY) / step_y) / snap; } if (pos[0] != last_pos[0] || pos[1] != last_pos[1]) { let applied_difference = onDrag(pos[0] - last_pos[0], pos[1] - last_pos[1], e1) last_pos[0] += applied_difference[0]; last_pos[1] += applied_difference[1]; UVEditor.displayTools(); UVEditor.loadData(); UVEditor.vue.$forceUpdate(); Canvas.updateView({elements, element_aspects: {uv: true}}); scope.dragging_uv = true; } } function stop() { removeEventListeners(document, 'mousemove touchmove', drag); removeEventListeners(document, 'mouseup touchend', stop); if (scope.dragging_uv) { onEnd(); setTimeout(() => scope.dragging_uv = false, 10); } else { if (onAbort) onAbort(); Undo.cancelEdit(); } } addEventListeners(document, 'mousemove touchmove', drag); addEventListeners(document, 'mouseup touchend', stop); }, 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({ elements, uv_only: true, }); let total_diff = [0, 0]; let do_move_uv = !!(BarItems.move_texture_with_uv.value && this.texture); UVEditor.getMappableElements().forEach(el => { if (el instanceof Mesh) { delete Project.selected_vertices[el.uuid]; } }) let overlay_canvas; if (do_move_uv) { Undo.initEdit({ elements, uv_only: true, bitmap: true, textures: [this.texture] }); overlay_canvas = Interface.createElement('canvas', {class: 'move_texture_with_uv'}); let ctx = overlay_canvas.getContext('2d'); overlay_canvas.width = this.texture.width; overlay_canvas.height = this.texture.height; this.texture.edit(canvas => { let tex_ctx = canvas.getContext('2d'); ctx.beginPath(); tex_ctx.beginPath(); UVEditor.getMappableElements().forEach(el => { if (el instanceof Mesh) { for (var fkey in el.faces) { var face = el.faces[fkey]; if (!this.selected_faces.includes(fkey)) continue; if (face.vertices.length <= 2 || face.getTexture() !== this.texture) continue; let matrix = face.getOccupationMatrix(true, [0, 0]); for (let x in matrix) { for (let y in matrix[x]) { if (!matrix[x][y]) continue; x = parseInt(x); y = parseInt(y); ctx.rect(x, y, 1, 1); tex_ctx.rect(x, y, 1, 1); } } } } else { let factor_x = this.texture.width / Project.texture_width; let factor_y = this.texture.height / Project.texture_height; for (var fkey in el.faces) { var face = el.faces[fkey]; if (!this.selected_faces.includes(fkey) && !this.box_uv) continue; if (face.getTexture() !== this.texture) continue; let rect = face.getBoundingRect(); let canvasRect = [ Math.floor(rect.ax * factor_x), Math.floor(rect.ay * factor_y), Math.ceil(rect.bx * factor_x) - Math.floor(rect.ax * factor_x), Math.ceil(rect.by * factor_y) - Math.floor(rect.ay * factor_y), ] ctx.rect(...canvasRect); tex_ctx.rect(...canvasRect); } } }) ctx.clip(); ctx.drawImage(this.texture.img, 0, 0); tex_ctx.clip(); tex_ctx.clearRect(0, 0, canvas.width, canvas.height); }, {no_undo: true}) UVEditor.vue.$refs.frame.append(overlay_canvas); } else { Undo.initEdit({elements, uv_only: true}); } this.drag({ event, onDrag: (diff_x, diff_y) => { elements.forEach(element => { if (element instanceof Mesh) { this.selected_faces.forEach(key => { let face = element.faces[key]; if (!face) return; face.vertices.forEach(vertex_key => { diff_x = Math.clamp(diff_x, -face.uv[vertex_key][0], Project.texture_width - face.uv[vertex_key][0]); diff_y = Math.clamp(diff_y, -face.uv[vertex_key][1], Project.texture_height - face.uv[vertex_key][1]); }) }) } else if (Project.box_uv) { let size = element.size(undefined, true); let uv_size = [ size[2] + size[0] + (size[1] ? size[2] : 0) + size[0], size[2] + size[1], ] diff_x = Math.clamp(diff_x, -element.uv_offset[0] - (size[1] ? 0 : size[2]), Project.texture_width - element.uv_offset[0] - uv_size[0]); diff_y = Math.clamp(diff_y, -element.uv_offset[1] - (size[0] ? 0 : size[2]), Project.texture_height - element.uv_offset[1] - uv_size[1]); } else { this.selected_faces.forEach(key => { if (element.faces[key] && element instanceof Cube) { diff_x = Math.clamp(diff_x, -element.faces[key].uv[0], Project.texture_width - element.faces[key].uv[0]); diff_y = Math.clamp(diff_y, -element.faces[key].uv[1], Project.texture_height - element.faces[key].uv[1]); diff_x = Math.clamp(diff_x, -element.faces[key].uv[2], Project.texture_width - element.faces[key].uv[2]); diff_y = Math.clamp(diff_y, -element.faces[key].uv[3], Project.texture_height - element.faces[key].uv[3]); } }) } }) elements.forEach(element => { if (element instanceof Mesh) { this.selected_faces.forEach(key => { let face = element.faces[key]; if (!face) return; face.vertices.forEach(vertex_key => { face.uv[vertex_key][0] += diff_x; face.uv[vertex_key][1] += diff_y; }) }) } else if (Project.box_uv) { element.uv_offset[0] += diff_x; element.uv_offset[1] += diff_y; } else { this.selected_faces.forEach(key => { if (element.faces[key] && element instanceof Cube) { element.faces[key].uv[0] += diff_x; element.faces[key].uv[1] += diff_y; element.faces[key].uv[2] += diff_x; element.faces[key].uv[3] += diff_y; } }) } }) if (do_move_uv) { total_diff[0] += diff_x; total_diff[1] += diff_y; overlay_canvas.style.left = this.toPixels(total_diff[0]); overlay_canvas.style.top = this.toPixels(total_diff[1]); } return [diff_x, diff_y] }, onEnd: () => { UVEditor.disableAutoUV() if (do_move_uv) { this.texture.edit((canvas) => { canvas.getContext('2d').drawImage( overlay_canvas, total_diff[0] * this.texture.width / Project.texture_width, total_diff[1] * this.texture.height / Project.texture_height ); }, {no_undo: true}) overlay_canvas.remove(); Canvas.updateView({elements, element_aspects: {uv: true}}); } Undo.finishEdit('Move UV'); }, onAbort: () => { 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); } } }) }, resizeFace(face_key, event, x_side, y_side) { if (event.which == 2 || event.which == 3) return; event.stopPropagation(); let elements = UVEditor.getMappableElements(); Undo.initEdit({elements, uv_only: true}) let inverted = {}; elements.forEach(element => { let faces = inverted[element.uuid] = {}; this.selected_faces.forEach(key => { if (element.faces[key] && element instanceof Cube) { faces[key] = [ element.faces[key].uv[0] > element.faces[key].uv[2], element.faces[key].uv[1] > element.faces[key].uv[3], ] } }) }) this.drag({ event, onDrag: (x, y) => { elements.forEach(element => { this.selected_faces.forEach(key => { if (element.faces[key] && element instanceof Cube) { if (x_side && (x_side == -1) != inverted[element.uuid][key][0]) element.faces[key].uv[0] = Math.clamp(element.faces[key].uv[0] + x, 0, Project.texture_width); if (y_side && (y_side == -1) != inverted[element.uuid][key][1]) element.faces[key].uv[1] = Math.clamp(element.faces[key].uv[1] + y, 0, Project.texture_height); if (x_side && (x_side == 1) != inverted[element.uuid][key][0]) element.faces[key].uv[2] = Math.clamp(element.faces[key].uv[2] + x, 0, Project.texture_width); if (y_side && (y_side == 1) != inverted[element.uuid][key][1]) element.faces[key].uv[3] = Math.clamp(element.faces[key].uv[3] + y, 0, Project.texture_height); } }) }) return [x, y] }, onEnd: () => { UVEditor.disableAutoUV() Undo.finishEdit('Resize UV') } }) }, rotateFace(face_key, event) { if (event.which == 2 || event.which == 3) return; event.stopPropagation(); convertTouchEvent(event); let scope = this; let elements = UVEditor.getMappableElements(); Undo.initEdit({elements, uv_only: true}) let face_center = [0, 0]; let points = 0; elements.forEach(element => { this.selected_faces.forEach(fkey => { let face = element.faces[fkey]; if (!face) return; if (element instanceof Cube) { face_center[0] += face.uv[0] + face.uv[2]; face_center[1] += face.uv[1] + face.uv[3]; points += 2; } else if (element instanceof Mesh) { face.old_uv = {}; face.vertices.forEach(vkey => { if (!face.uv[vkey]) return; face_center[0] += face.uv[vkey][0]; face_center[1] += face.uv[vkey][1]; points += 1; face.old_uv[vkey] = face.uv[vkey].slice(); }) } }) }) face_center.forEach((v, i) => face_center[i] = v / points); let offset = $(UVEditor.vue.$refs.frame).offset(); let center_on_screen = [ face_center[0] * UVEditor.getPixelSize() + offset.left, face_center[1] * UVEditor.getPixelSize() + offset.top, ] let angle = 0; let last_angle; let original_angle; let straight_angle; let snap = elements[0] instanceof Cube ? 90 : 1; function drag(e1) { convertTouchEvent(e1); angle = Math.atan2( (e1.clientY - center_on_screen[1]), (e1.clientX - center_on_screen[0]), ) angle = Math.round(Math.radToDeg(angle) / snap) * snap; if (original_angle == undefined) original_angle = angle; angle -= original_angle; if (last_angle == undefined) last_angle = angle; if (Math.abs(angle - last_angle) > 300) last_angle = angle; if (angle != last_angle && (straight_angle == undefined || Math.abs(straight_angle - angle) > 6 || e1.ctrlOrCmd || Pressing.overrides.ctrl)) { straight_angle = undefined; scope.helper_lines.x = scope.helper_lines.y = -1; elements.forEach(element => { if (element instanceof Cube && Format.uv_rotation) { scope.selected_faces.forEach(key => { if (element.faces[key]) { element.faces[key].rotation += 90 * Math.sign(last_angle - angle); if (element.faces[key].rotation == 360) element.faces[key].rotation = 0; if (element.faces[key].rotation < 0) element.faces[key].rotation += 360; } }) } else if (element instanceof Mesh) { scope.selected_faces.forEach(fkey => { let face = element.faces[fkey]; if (!face) return; face.vertices.forEach(vkey => { if (!face.uv[vkey]) return; let sin = Math.sin(Math.degToRad(angle)); let cos = Math.cos(Math.degToRad(angle)); face.uv[vkey][0] = face.old_uv[vkey][0] - face_center[0]; face.uv[vkey][1] = face.old_uv[vkey][1] - face_center[1]; let a = (face.uv[vkey][0] * cos - face.uv[vkey][1] * sin); let b = (face.uv[vkey][0] * sin + face.uv[vkey][1] * cos); face.uv[vkey][0] = Math.clamp(a + face_center[0], 0, Project.texture_width); face.uv[vkey][1] = Math.clamp(b + face_center[1], 0, Project.texture_height); }) let e = 0.6; face.vertices.forEach((vkey, i) => { for (let j = i+1; j < face.vertices.length; j++) { let relative_angle = Math.radToDeg(Math.PI + Math.atan2( face.uv[vkey][1] - face.uv[face.vertices[j]][1], face.uv[vkey][0] - face.uv[face.vertices[j]][0], )) % 180; if (Math.abs(relative_angle - 90) < e) { straight_angle = angle; if (scope.helper_lines.x == -1) scope.helper_lines.x = face.uv[vkey][0]; } if (relative_angle < e || 180 - relative_angle < e) { straight_angle = angle; if (scope.helper_lines.y == -1) scope.helper_lines.y = face.uv[vkey][1]; } } }) }) } }) UVEditor.turnMapping() last_angle = angle; UVEditor.displayTools(); UVEditor.loadData(); UVEditor.vue.$forceUpdate(); Canvas.updateView({elements, element_aspects: {uv: true}}); scope.dragging_uv = true; } } function stop() { removeEventListeners(document, 'mousemove touchmove', drag); removeEventListeners(document, 'mouseup touchend', stop); scope.helper_lines.x = scope.helper_lines.y = -1; if (scope.dragging_uv) { UVEditor.disableAutoUV() Undo.finishEdit('Rotate UV') setTimeout(() => scope.dragging_uv = false, 10); } else { Undo.cancelEdit(); } } addEventListeners(document, 'mousemove touchmove', drag); addEventListeners(document, 'mouseup touchend', stop); }, dragVertices(element, vertex_key, event) { if (event.which == 2 || event.which == 3) return; if (!this.selected_vertices[element.uuid]) this.selected_vertices[element.uuid] = []; let sel_vertices = this.selected_vertices[element.uuid]; let add_to_selection = (event.shiftKey || event.ctrlOrCmd || Pressing.overrides.shift || Pressing.overrides.ctrl); if (sel_vertices.includes(vertex_key)) { } else if (add_to_selection) { if (sel_vertices.includes(vertex_key)) { sel_vertices.remove(vertex_key); } else { sel_vertices.push(vertex_key); } } else { sel_vertices.replace([vertex_key]); } let elements = UVEditor.getMappableElements(); Undo.initEdit({elements, uv_only: true}) this.drag({ event, onDrag: (x, y, event) => { elements.forEach(element => { this.selected_faces.forEach(key => { let face = element.faces[key]; if (!face) return; face.vertices.forEach(vertex_key => { if (this.selected_vertices[element.uuid] && this.selected_vertices[element.uuid].includes(vertex_key)) { x = Math.clamp(x, -face.uv[vertex_key][0], Project.texture_width - face.uv[vertex_key][0]); y = Math.clamp(y, -face.uv[vertex_key][1], Project.texture_width - face.uv[vertex_key][1]); } }) }) }) elements.forEach(element => { this.selected_faces.forEach(key => { let face = element.faces[key]; let old_uv_coords = face.vertices.map(vkey => face.uv[vkey].slice()) face.vertices.forEach((vertex_key, i) => { if (this.selected_vertices[element.uuid] && this.selected_vertices[element.uuid].includes(vertex_key)) { let is_duplicate = face.vertices.find((vkey2, j) => { return j > i && face.uv[vertex_key].equals(old_uv_coords[j]) }) if (is_duplicate) { this.selected_vertices[element.uuid].remove(vertex_key); return; } face.uv[vertex_key][0] += x; face.uv[vertex_key][1] += y; if ((event.shiftKey || Pressing.overrides.shift) && !(event.ctrlOrCmd || Pressing.overrides.ctrl)) { let multiplier = settings.shift_size.value / 16 face.uv[vertex_key][0] = Math.round(face.uv[vertex_key][0] * multiplier) / multiplier; face.uv[vertex_key][1] = Math.round(face.uv[vertex_key][1] * multiplier) / multiplier; } } }) }) }) return [x, y] }, onEnd: () => { Undo.finishEdit('Move UV'); }, onAbort() { if (!add_to_selection) { sel_vertices.replace([vertex_key]); } } }) }, toPixels(uv_coord, offset = 0) { return (uv_coord / this.project_resolution[0] * this.inner_width + offset) + 'px' }, getMeshFaceOutline(face) { let coords = []; let uv_offset = [ -this.getMeshFaceCorner(face, 0), -this.getMeshFaceCorner(face, 1), ] face.getSortedVertices().forEach(key => { let UV = face.uv[key]; coords.push( 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(' '); }, getMeshFaceCorner(face, axis) { let val = Infinity; face.vertices.forEach(key => { let UV = face.uv[key]; val = Math.min(val, UV[axis]); }) return val; }, getMeshFaceWidth(face, axis) { let min = Infinity; let max = 0; face.vertices.forEach(key => { let UV = face.uv[key]; min = Math.min(min, UV[axis]); max = Math.max(max, UV[axis]); }) return max - min; }, filterMeshFaces(faces) { let keys = Object.keys(faces); if (keys.length > 800) { let result = {}; this.selected_faces.forEach(key => { if (faces[key]) result[key] = faces[key]; }) return result; } else { return faces; } }, getBrushOutlineStyle() { if (Toolbox.selected.brushTool) { var pixel_size = this.inner_width / (this.texture ? this.texture.width : Project.texture_width); //pos let offset = BarItems.slider_brush_size.get()%2 == 0 && Toolbox.selected.brushTool ? 0 : 0.5; let left = (this.mouse_coords.x + offset) * pixel_size; let top = (this.mouse_coords.y + offset) * pixel_size; //size var radius = (BarItems.slider_brush_size.get()/2) * pixel_size; return { '--radius': radius, left: left+'px', top: top+'px' } } else { return {display: 'none'}; } }, getFrameMargin(style) { let gap_x = Math.max((this.width - this.inner_width) / 2, 0); let gap_y = Math.max((this.height - this.inner_height) / 2, 0); let margin = [ Math.floor(gap_x + this.width/2), Math.floor(gap_y + this.height/2), ]; return style ? `${margin[1]}px ${margin[0]}px` : margin; } }, template: `
{{ project_resolution[0] + ' ⨉ ' + project_resolution[1] }}
  • {{ face_names[key] }}
  • ${tl('uv_editor.transparent_face')}
    ${tl('uv_editor.copy_paste_tool.cut')}
    ${tl('uv_editor.copy_paste_tool.mirror_x')}
    ${tl('uv_editor.copy_paste_tool.mirror_y')}
    ${tl('uv_editor.copy_paste_tool.rotate')}
    rotate_right
    ${tl('dialog.cancel')}
    clear
    ${tl('uv_editor.copy_paste_tool.place')}
    check
    ` } }) UVEditor.panel.on('move_to', (data) => { if (!Blockbench.isMobile) { UVEditor.saveViewportOffset(); } }) UVEditor.panel.on('moved_to', (data) => { Vue.nextTick(() => { UVEditor.loadViewportOffset(); }) }) Toolbars.uv_editor.toPlace() BarItems.paint_mode_uv_overlay.toElement('#toggle_uv_overlay_anchor'); let {slider_bar} = UVEditor.vue.$refs; var onBefore = function() { Undo.initEdit({elements: UVEditor.getMappableElements()}) } var onAfter = function() { Undo.finishEdit('Edit UV') } var getInterval = function(event) { return canvasGridSize(event.shiftKey || Pressing.overrides.shift, event.ctrlOrCmd || Pressing.overrides.ctrl) / UVEditor.grid; } function getPos(axis) { let elements = UVEditor.getMappableElements(); if (!elements[0]) return 0; if (Project.box_uv && elements[0] instanceof Cube) { return trimFloatNumber(elements[0].uv_offset[axis]) } else if (elements[0] instanceof Cube) { var face = UVEditor.getReferenceFace(); if (face) { return trimFloatNumber(face.uv[axis]) } } else if (elements[0] instanceof Mesh) { var face = UVEditor.getReferenceFace(); if (face) { let selected_vertices = Project.selected_vertices[elements[0].uuid]; let has_selected_vertices = selected_vertices && face.vertices.find(vkey => selected_vertices.includes(vkey)) let min = Infinity; face.vertices.forEach(vkey => { if ((!has_selected_vertices || selected_vertices.includes(vkey)) && face.uv[vkey]) { min = Math.min(min, face.uv[vkey][axis]); } }) if (min == Infinity) min = 0; return trimFloatNumber(min) } } return 0 } UVEditor.sliders.pos_x = new NumSlider({ id: 'uv_slider_pos_x', private: true, condition: () => UVEditor.vue.selected_faces.length || Project.box_uv, get: function() { return getPos(0); }, change: function(modify) { UVEditor.slidePos(modify, 0); }, getInterval, onBefore, onAfter }).toElement(slider_bar); UVEditor.sliders.pos_y = new NumSlider({ id: 'uv_slider_pos_y', private: true, condition: () => UVEditor.vue.selected_faces.length || Project.box_uv, get: function() { return getPos(1); }, change: function(modify) { UVEditor.slidePos(modify, 1); }, getInterval, onBefore, onAfter }).toElement(slider_bar); UVEditor.sliders.size_x = new NumSlider({ id: 'uv_slider_size_x', private: true, condition: () => (!Project.box_uv && UVEditor.vue.selected_faces.length), get: function() { if (!Project.box_uv) { let ref_face = UVEditor.getReferenceFace(); if (ref_face instanceof CubeFace) { return trimFloatNumber(ref_face.uv[2] - ref_face.uv[0]); } else if (ref_face instanceof MeshFace) { let rect = ref_face.getBoundingRect(); return trimFloatNumber(rect.x); } } return 0 }, change: function(modify) { UVEditor.slideSize(modify, 0) }, getInterval, onBefore, onAfter }).toElement(slider_bar); UVEditor.sliders.size_y = new NumSlider({ id: 'uv_slider_size_y', private: true, condition: () => (!Project.box_uv && UVEditor.vue.selected_faces.length), get: function() { if (!Project.box_uv) { let ref_face = UVEditor.getReferenceFace(); if (ref_face instanceof CubeFace) { return trimFloatNumber(ref_face.uv[3] - ref_face.uv[1]); } else if (ref_face instanceof MeshFace) { let rect = ref_face.getBoundingRect(); return trimFloatNumber(rect.y); } } return 0 }, change: function(modify) { UVEditor.slideSize(modify, 1) }, getInterval, onBefore, onAfter }).toElement(slider_bar); })