Add UV rotate tool

Improve array export of JSON compiler
Stop texture animations playing when switching tab
Fix duplicate keybinding from add mesh button
Improve transform space normal calculation
Fix interface issues
Add "Instance" property type
master
JannisX11 2021-09-20 18:45:02 +02:00
parent cc1c48fb56
commit 66f7ec2b44
13 changed files with 250 additions and 67 deletions

BIN
assets/rotate_cursor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -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);

View File

@ -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;

View File

@ -1,6 +1,6 @@
(function() {
let FORMATV = '3.6';
let FORMATV = '4.0';
function processHeader(model) {
if (!model.meta) {

View File

@ -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

View File

@ -254,6 +254,7 @@ class ModelProject {
}
})
if (TextureAnimator.isPlaying) TextureAnimator.stop();
this.selected = false;
Painter.current = {};
scene.remove(this.model_3d);

View File

@ -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])

View File

@ -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();

View File

@ -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;

View File

@ -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) {

View File

@ -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() {
<div class="uv_resize_side horizontal" @mousedown="resizeFace(key, $event, 0, 1)" style="top: var(--height); width: var(--width)"></div>
<div class="uv_resize_side vertical" @mousedown="resizeFace(key, $event, -1, 0)" style="height: var(--height)"></div>
<div class="uv_resize_side vertical" @mousedown="resizeFace(key, $event, 1, 0)" style="left: var(--width); height: var(--height)"></div>
<div class="uv_resize_corner uv_c_nw" @mousedown="resizeFace(key, $event, -1, -1)" style="left: 0; top: 0"></div>
<div class="uv_resize_corner uv_c_ne" @mousedown="resizeFace(key, $event, 1, -1)" style="left: var(--width); top: 0"></div>
<div class="uv_resize_corner uv_c_sw" @mousedown="resizeFace(key, $event, -1, 1)" style="left: 0; top: var(--height)"></div>
<div class="uv_resize_corner uv_c_se" @mousedown="resizeFace(key, $event, 1, 1)" style="left: var(--width); top: var(--height)"></div>
<div class="uv_resize_corner uv_c_nw" :class="{main_corner: !face.rotation}" @mousedown="resizeFace(key, $event, -1, -1)" style="left: 0; top: 0">
<div class="uv_rotate_field" v-if="!face.rotation" @mousedown.stop="rotateFace(key, $event)"></div>
</div>
<div class="uv_resize_corner uv_c_ne" :class="{main_corner: face.rotation == 270}" @mousedown="resizeFace(key, $event, 1, -1)" style="left: var(--width); top: 0">
<div class="uv_rotate_field" v-if="face.rotation == 270" @mousedown.stop="rotateFace(key, $event)"></div>
</div>
<div class="uv_resize_corner uv_c_sw" :class="{main_corner: face.rotation == 90}" @mousedown="resizeFace(key, $event, -1, 1)" style="left: 0; top: var(--height)">
<div class="uv_rotate_field" v-if="face.rotation == 90" @mousedown.stop="rotateFace(key, $event)"></div>
</div>
<div class="uv_resize_corner uv_c_se" :class="{main_corner: face.rotation == 180}" @mousedown="resizeFace(key, $event, 1, 1)" style="left: var(--width); top: var(--height)">
<div class="uv_rotate_field" v-if="face.rotation == 180" @mousedown.stop="rotateFace(key, $event)"></div>
</div>
</template>
</div>
</template>
@ -2113,11 +2249,13 @@ Interface.definePanels(function() {
<polygon :points="getMeshFaceOutline(face)" />
</svg>
<template v-if="selected_faces.includes(key)">
<div class="uv_mesh_vertex" v-for="key in face.vertices"
:class="{selected: selected_vertices[element.uuid] && selected_vertices[element.uuid].includes(key)}"
<div class="uv_mesh_vertex" v-for="(key, index) in face.vertices"
:class="{main_corner: index == 0, selected: selected_vertices[element.uuid] && selected_vertices[element.uuid].includes(key)}"
@mousedown.prevent.stop="dragVertices(element, key, $event)"
:style="{left: toPixels( face.uv[key][0] - getMeshFaceCorner(face, 0) ), top: toPixels( face.uv[key][1] - getMeshFaceCorner(face, 1) )}"
></div>
>
<div class="uv_rotate_field" @mousedown.stop="rotateFace(key, $event)" v-if="index == 0"></div>
</div>
</template>
</div>
</template>

32
package-lock.json generated
View File

@ -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
}
}

View File

@ -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",