diff --git a/assets/rotate_cursor.png b/assets/rotate_cursor.png new file mode 100644 index 00000000..36c6b2b7 Binary files /dev/null and b/assets/rotate_cursor.png differ diff --git a/css/general.css b/css/general.css index c11e1e9e..5abbd590 100644 --- a/css/general.css +++ b/css/general.css @@ -328,6 +328,12 @@ display: inline-block; margin-top: 4px; } + .tool:active .icon { + padding-top: 1px; + } + .tool:active .icon.fa_big { + padding-top: 2px; + } .tool.enabled { border-bottom: 3px solid var(--color-accent); diff --git a/css/panels.css b/css/panels.css index f638d847..a337510b 100644 --- a/css/panels.css +++ b/css/panels.css @@ -1182,7 +1182,7 @@ background-color: rgba(50, 70, 240, 0.14); } .cube_box_uv:hover > div { - border-color: var(--color-light); + border-color: white; z-index: 3; } .cube_uv_face { @@ -1198,12 +1198,12 @@ color: var(--color-subtle_text); } .cube_uv_face:hover { - border-color: var(--color-light); + border-color: white; background-color: rgba(50, 70, 240, 0.3); z-index: 3; } .cube_uv_face.selected:not(.unselected) { - border-color: var(--color-light); + border-color: white; z-index: 4; box-shadow: 0 0 4px #000000cc; } @@ -1273,7 +1273,7 @@ stroke-width: 2px; } .mesh_uv_face:hover polygon { - stroke: var(--color-light); + stroke: white; } .mesh_uv_face.selected polygon { stroke: var(--color-accent); @@ -1303,6 +1303,29 @@ text-align: center; } + .main_corner { + position: absolute; + } + .main_corner::after { + content: ""; + display: block; + margin: -2px; + height: 11px; + width: 11px; + border: 1px solid white; + } + .main_corner.selected::after { + border-color: var(--color-accent); + } + .uv_rotate_field { + position: absolute; + width: 15px; + height: 15px; + bottom: 6px; + right: 6px; + cursor: url('../assets/rotate_cursor.png') 9 9, auto; + } + .panel .bar.next_to_title { margin-top: -34px; margin-right: 32px; diff --git a/js/io/formats/bbmodel.js b/js/io/formats/bbmodel.js index a2c8ecda..d4bb1ec4 100644 --- a/js/io/formats/bbmodel.js +++ b/js/io/formats/bbmodel.js @@ -1,6 +1,6 @@ (function() { -let FORMATV = '3.6'; +let FORMATV = '4.0'; function processHeader(model) { if (!model.meta) { diff --git a/js/io/io.js b/js/io/io.js index 80e03844..1307f92e 100644 --- a/js/io/io.js +++ b/js/io/io.js @@ -392,21 +392,21 @@ function compileJSON(object, options) { //Number o = (Math.round(o*100000)/100000).toString() out += o - } else if (typeof o === 'object' && o instanceof Array) { + } else if (o instanceof Array) { //Array - var has_content = false + let has_content = false + let has_objects = !!o.find(item => typeof item === 'object'); out += '[' for (var i = 0; i < o.length; i++) { var compiled = handleVar(o[i], tabs+1) if (compiled) { - var breaks = typeof o[i] === 'object' if (has_content) {out += ',' + (breaks || options.small?'':' ')} - if (breaks) {out += newLine(tabs)} + if (has_objects) {out += newLine(tabs)} out += compiled has_content = true } } - if (typeof o[o.length-1] === 'object') {out += newLine(tabs-1)} + if (has_objects) {out += newLine(tabs-1)} out += ']' } else if (typeof o === 'object') { //Object diff --git a/js/io/project.js b/js/io/project.js index edcda64b..0d46cd06 100644 --- a/js/io/project.js +++ b/js/io/project.js @@ -254,6 +254,7 @@ class ModelProject { } }) + if (TextureAnimator.isPlaying) TextureAnimator.stop(); this.selected = false; Painter.current = {}; scene.remove(this.model_3d); diff --git a/js/outliner/cube.js b/js/outliner/cube.js index 762dd869..ec80177c 100644 --- a/js/outliner/cube.js +++ b/js/outliner/cube.js @@ -873,9 +873,15 @@ new NodePreviewController(Cube, { let {mesh} = cube; let indices = []; + let j = 0; + mesh.geometry.faces = []; + mesh.geometry.clearGroups(); Canvas.face_order.forEach((fkey, i) => { if (cube.faces[fkey].texture !== null) { indices.push(0 + i*4, 2 + i*4, 1 + i*4, 2 + i*4, 3 + i*4, 1 + i*4); + mesh.geometry.addGroup(j*6, 6, j) + mesh.geometry.faces.push(fkey) + j++; } }) mesh.geometry.setIndex(indices) @@ -899,11 +905,7 @@ new NodePreviewController(Cube, { } else { var materials = [] Canvas.face_order.forEach(function(face) { - - if (cube.faces[face].texture === null) { - materials.push(Canvas.transparentMaterial) - - } else { + if (cube.faces[face].texture !== null) { var tex = cube.faces[face].getTexture() if (tex && tex.uuid) { materials.push(Project.materials[tex.uuid]) diff --git a/js/outliner/mesh.js b/js/outliner/mesh.js index 5b8bfec6..28625ffa 100644 --- a/js/outliner/mesh.js +++ b/js/outliner/mesh.js @@ -279,15 +279,30 @@ class Mesh extends OutlinerElement { return faces; } getSelectionRotation() { - let [face] = this.getSelectedFaces().map(fkey => this.faces[fkey]); - if (face) { - let normal = face.getNormal(true) + let faces = this.getSelectedFaces().map(fkey => this.faces[fkey]); + if (!faces[0]) { + let selected_vertices = this.getSelectedVertices(); + this.forAllFaces((face) => { + if (face.vertices.find(vkey => selected_vertices.includes(vkey))) { + faces.push(face); + } + }) + } + if (faces[0]) { + let normal = [0, 0, 0]; + faces.forEach(face => normal.V3_add(face.getNormal(true))) + normal.V3_divide(faces.length); var y = Math.atan2(normal[0], normal[2]); var x = Math.atan2(normal[1], Math.sqrt(Math.pow(normal[0], 2) + Math.pow(normal[2], 2))); return new THREE.Euler(-x, y, 0, 'YXZ'); } } + forAllFaces(cb) { + for (let fkey in this.faces) { + cb(this.faces[fkey], fkey); + } + } transferOrigin(origin, update = true) { if (!this.mesh) return; var q = new THREE.Quaternion().copy(this.mesh.quaternion); @@ -814,7 +829,7 @@ BARS.defineActions(function() { }}, diameter: {label: 'dialog.add_primitive.diameter', type: 'number', value: 16}, height: {label: 'dialog.add_primitive.height', type: 'number', value: 8, condition: ({shape}) => ['cylinder', 'cone', 'cube', 'pyramid', 'tube'].includes(shape)}, - sides: {label: 'dialog.add_primitive.sides', type: 'number', value: 16, condition: ({shape}) => ['cylinder', 'cone', 'circle', 'torus', 'sphere', 'tube'].includes(shape)}, + sides: {label: 'dialog.add_primitive.sides', type: 'number', value: 12, condition: ({shape}) => ['cylinder', 'cone', 'circle', 'torus', 'sphere', 'tube'].includes(shape)}, minor_diameter: {label: 'dialog.add_primitive.minor_diameter', type: 'number', value: 4, condition: ({shape}) => ['torus', 'tube'].includes(shape)}, minor_sides: {label: 'dialog.add_primitive.minor_sides', type: 'number', value: 8, condition: ({shape}) => ['torus'].includes(shape)}, }, @@ -1066,7 +1081,6 @@ BARS.defineActions(function() { new Action('add_mesh', { icon: 'fa-gem', category: 'edit', - keybind: new Keybind({key: 'n', ctrl: true}), condition: () => (Modes.edit && Format.meshes), click: function () { add_mesh_dialog.show(); diff --git a/js/property.js b/js/property.js index 0d414265..5465964e 100644 --- a/js/property.js +++ b/js/property.js @@ -5,8 +5,6 @@ class Property { } target_class.properties[name] = this; - let scope = this; - this.class = target_class; this.name = name; this.type = type; @@ -20,6 +18,7 @@ class Property { case 'number': this.default = 0; break; case 'boolean': this.default = false; break; case 'array': this.default = []; break; + case 'instance': this.default = null; break; case 'vector': this.default = [0, 0, 0]; break; case 'vector2': this.default = [0, 0]; break; } @@ -30,6 +29,7 @@ class Property { case 'number': this.isNumber = true; break; case 'boolean': this.isBoolean = true; break; case 'array': this.isArray = true; break; + case 'instance': this.isInstance = true; break; case 'vector': this.isVector = true; break; case 'vector2': this.isVector2 = true; break; } @@ -87,6 +87,11 @@ class Property { instance[this.name].replace(data[this.name]); } } + else if (this.isInstance) { + if (typeof data[this.name] === 'object') { + instance[this.name] =data[this.name]; + } + } } copy(instance, target) { if (!Condition(this.condition, instance)) return; diff --git a/js/texturing/textures.js b/js/texturing/textures.js index 35a244c2..c28ad30a 100644 --- a/js/texturing/textures.js +++ b/js/texturing/textures.js @@ -442,9 +442,6 @@ class Texture { this.source = dataUrl; this.img.src = dataUrl; this.updateMaterial(); - if (this == UVEditor.texture) { - UVEditor.img.src = dataUrl; - }; if (open_dialog == 'UVEditor') { for (var key in UVEditor.editors) { var editor = UVEditor.editors[key]; @@ -654,7 +651,7 @@ class Texture { TextureAnimator.updateButton() hideDialog() if (UVEditor.texture == this) { - UVEditor.displayTexture(); + UVEditor.vue.updateTexture(); } BARS.updateConditions() Undo.finishEdit('Remove texture', {textures: []}) @@ -1381,9 +1378,6 @@ TextureAnimator = { $(`.texture[texid="${tex.uuid}"]`).find('img').css('margin-top', (tex.currentFrame*-48)+'px'); maxFrame = Math.max(maxFrame, tex.currentFrame); }) - if (animated_textures.includes(UVEditor.texture)) { - UVEditor.img.style.objectPosition = `0 -${UVEditor.texture.currentFrame * UVEditor.inner_height}px`; - } Cube.all.forEach(cube => { var update = false for (var face in cube.faces) { diff --git a/js/texturing/uv.js b/js/texturing/uv.js index 5c8d9ebe..5733e9ad 100644 --- a/js/texturing/uv.js +++ b/js/texturing/uv.js @@ -24,7 +24,6 @@ const UVEditor = { grid: 1, max_zoom: 16, auto_grid: true, - texture: false, panel: null, sliders: {}, @@ -373,6 +372,9 @@ const UVEditor = { 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 @@ -656,6 +658,18 @@ const UVEditor = { 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]; + } }) obj.autouv = 0; Canvas.updateUV(obj); @@ -896,11 +910,10 @@ const UVEditor = { this.message('uv_editor.mirrored') this.loadData() }, - applyAll(event) { - var scope = this; + applyAll() { this.forCubes(obj => { - UVEditor.cube_faces.forEach(function(side) { - $.extend(true, obj.faces[side], obj.faces[scope.face]) + UVEditor.cube_faces.forEach(side => { + obj.faces[side].extend(obj.faces[this.selected_faces[0]]) }) obj.autouv = 0 }) @@ -1582,14 +1595,14 @@ Interface.definePanels(function() { } } if (texture === null) { - this.texture = UVEditor.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 = UVEditor.texture = 0; + this.texture = 0; } }, updateMouseCoords(event) { @@ -1776,7 +1789,7 @@ Interface.definePanels(function() { } } }, - drag({event, onDrag, onEnd, onAbort}) { + drag({event, onDrag, onEnd, onAbort, snap}) { if (event.which == 2 || event.which == 3) return; let scope = this; @@ -1784,13 +1797,21 @@ Interface.definePanels(function() { let last_pos = [0, 0]; function drag(e1) { - let snap = UVEditor.grid / canvasGridSize(e1.shiftKey || Pressing.overrides.shift, e1.ctrlOrCmd || Pressing.overrides.ctrl); + 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); - 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; + 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]) { onDrag(pos[0] - last_pos[0], pos[1] - last_pos[1], e1) @@ -1904,6 +1925,113 @@ Interface.definePanels(function() { } }) }, + rotateFace(face_key, event) { + if (event.which == 2 || event.which == 3) return; + event.stopPropagation(); + let scope = this; + let elements = this.mappable_elements; + 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.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_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 snap = elements[0] instanceof Cube ? 90 : 1; + function drag(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 (last_angle == undefined) last_angle = angle; + if (Math.abs(angle - last_angle) > 300) last_angle = angle; + + if (angle != last_angle) { + + 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); + console.log(element.faces[key].rotation, 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; + console.log(element.faces[key].rotation) + console.log('-----') + } + }) + + } 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 - last_angle)); + let cos = Math.cos(Math.degToRad(angle - last_angle)); + face.uv[vkey][0] -= face_center[0]; + face.uv[vkey][1] -= face_center[1]; + face.uv[vkey][0] = (face.uv[vkey][0] * cos - face.uv[vkey][1] * sin) + face.uv[vkey][1] = (face.uv[vkey][0] * sin + face.uv[vkey][1] * cos) + face.uv[vkey][0] += face_center[0]; + face.uv[vkey][1] += face_center[1]; + }) + }) + } + }) + UVEditor.turnMapping() + + last_angle = angle; + UVEditor.displaySliders(); + 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) { + 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; @@ -2076,10 +2204,18 @@ Interface.definePanels(function() {
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
@@ -2113,11 +2249,13 @@ Interface.definePanels(function() { diff --git a/package-lock.json b/package-lock.json index 4719d501..c021d993 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "Blockbench", - "version": "4.0.0-beta.0", + "version": "4.0.0-beta.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1188,9 +1188,9 @@ "dev": true }, "@electron/get": { - "version": "1.12.4", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-1.12.4.tgz", - "integrity": "sha512-6nr9DbJPUR9Xujw6zD3y+rS95TyItEVM0NVjt1EehY2vUWfIgPiIPVHxCvaTS0xr2B+DRxovYVKbuOWqC35kjg==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-1.13.0.tgz", + "integrity": "sha512-+SjZhRuRo+STTO1Fdhzqnv9D2ZhjxXP6egsJ9kiO8dtP68cDx7dFCwWi64dlMQV7sWcfW1OYCW4wviEBzmRsfQ==", "dev": true, "requires": { "debug": "^4.1.1", @@ -2111,9 +2111,9 @@ } }, "boolean": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.1.2.tgz", - "integrity": "sha512-YN6UmV0FfLlBVvRvNPx3pz5W/mUoYB24J4WSXOKP/OOJpi+Oq6WYqPaNTHzjI0QzwWtnvEd5CGYyQPgp1jFxnw==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.1.4.tgz", + "integrity": "sha512-3hx0kwU3uzG6ReQ3pnaFQPSktpBw6RHN3/ivDKEuU8g1XSfafowyvDnadjv1xp8IZqhtSukxlwv9bF6FhX8m0w==", "dev": true, "optional": true }, @@ -2603,9 +2603,9 @@ } }, "core-js": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.14.0.tgz", - "integrity": "sha512-3s+ed8er9ahK+zJpp9ZtuVcDoFzHNiZsPbNAAE4KXgrRHbjSqqNN6xGSXq6bq7TZIbKj4NLrLb6bJ5i+vSVjHA==", + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.17.3.tgz", + "integrity": "sha512-lyvajs+wd8N1hXfzob1LdOCCHFU4bGMbqqmLn1Q4QlCpDqWPpGf+p0nj+LNrvDDG33j0hZXw2nsvvVpHysxyNw==", "dev": true, "optional": true }, @@ -2848,9 +2848,9 @@ } }, "electron": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/electron/-/electron-13.1.2.tgz", - "integrity": "sha512-aNT9t+LgdQaZ7FgN36pN7MjSEoj+EWc2T9yuOqBApbmR4HavGRadSz7u9N2Erw2ojdIXtei2RVIAvVm8mbDZ0g==", + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-13.3.0.tgz", + "integrity": "sha512-d/BvOLDjI4i7yf9tqCuLL2fFGA2TrM/D9PyRpua+rJolG0qrwp/FohP02L0m+44kmPpofIo4l3NPwLmzyKKimA==", "dev": true, "requires": { "@electron/get": "^1.0.1", @@ -2859,9 +2859,9 @@ }, "dependencies": { "@types/node": { - "version": "14.17.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.3.tgz", - "integrity": "sha512-e6ZowgGJmTuXa3GyaPbTGxX17tnThl2aSSizrFthQ7m9uLGZBXiGhgE55cjRZTF5kjZvYn9EOPOMljdjwbflxw==", + "version": "14.17.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.17.tgz", + "integrity": "sha512-niAjcewgEYvSPCZm3OaM9y6YQrL2SEPH9PymtE6fuZAvFiP6ereCcvApGl2jKTq7copTIguX3PBvfP08LN4LvQ==", "dev": true } } diff --git a/package.json b/package.json index 392e98c7..5926f1ba 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ }, "devDependencies": { "blockbench-types": "^3.9.0", - "electron": "^13.1.2", + "electron": "^13.3.0", "electron-builder": "^22.11.11", "electron-notarize": "^1.0.0", "webpack": "^5.21.2",