mirror of
https://github.com/JannisX11/blockbench.git
synced 2024-11-21 01:13:37 +08:00
Merge branch 'next' into preview-scenes-2
This commit is contained in:
commit
5020dd3a56
BIN
assets/brush_outline.png
Normal file
BIN
assets/brush_outline.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 111 B |
BIN
assets/brush_outline_circle.png
Normal file
BIN
assets/brush_outline_circle.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 429 B |
BIN
assets/plugins.png
Normal file
BIN
assets/plugins.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
295
css/dialogs.css
295
css/dialogs.css
@ -97,6 +97,7 @@
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
font-family: var(--font-code);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
@ -217,11 +218,8 @@
|
||||
color: var(--color-light);
|
||||
}
|
||||
|
||||
.dialog p {
|
||||
margin: 4px 0;
|
||||
}
|
||||
.dialog h3 {
|
||||
margin-left: 16px;
|
||||
margin-left: 0;
|
||||
}
|
||||
.dialog_bar label.in_toolbar {
|
||||
padding-left: 0;
|
||||
@ -1115,6 +1113,37 @@ dialog#edit_bedrock_binding > .dialog_wrapper > .dialog_content {
|
||||
}
|
||||
|
||||
/*Plugin Menu*/
|
||||
dialog#plugins {
|
||||
max-width: min(1400px, 100%);
|
||||
height: calc(96% - 108px);
|
||||
}
|
||||
dialog#plugins .dialog_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
dialog#plugins content.dialog_content {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
max-height: initial;
|
||||
}
|
||||
#plugin_browser_sidebar {
|
||||
width: 38.2%;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 10px;
|
||||
border-right: 1px solid var(--color-border);
|
||||
}
|
||||
#plugin_browser_page,
|
||||
#plugin_browser_start_page {
|
||||
width: 61.8%;
|
||||
flex-grow: 1;
|
||||
padding: 16px 24px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.bar.next_to_title {
|
||||
display: inline-block;
|
||||
vertical-align: text-bottom;
|
||||
@ -1126,37 +1155,67 @@ dialog#edit_bedrock_binding > .dialog_wrapper > .dialog_content {
|
||||
float: left;
|
||||
z-index: inherit;
|
||||
}
|
||||
#plugin_search_bar {
|
||||
flex-grow: 1;
|
||||
}
|
||||
#plugins .tab_bar {
|
||||
width: calc(100% - 300px);
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
#plugin_browser_sidebar > .pagination_numbers {
|
||||
padding: 8px;
|
||||
}
|
||||
#plugin_list {
|
||||
max-height: 600px;
|
||||
overflow-y: scroll;
|
||||
min-height: 80px;
|
||||
}
|
||||
#plugin_list > li {
|
||||
min-height: 128px;
|
||||
overflow-y: hidden;
|
||||
margin: 8px;
|
||||
background-color: var(--color-ui);
|
||||
margin: 12px;
|
||||
padding: 8px 12px;
|
||||
padding-bottom: 12px;
|
||||
margin-right: 2px;
|
||||
cursor: pointer;
|
||||
background-color: var(--color-ui);
|
||||
}
|
||||
#plugin_list > li.selected {
|
||||
background-color: var(--color-button);
|
||||
}
|
||||
#plugin_list > li.incompatible {
|
||||
color: var(--color-subtle_text);
|
||||
}
|
||||
body.theme_borders #plugin_list > li {
|
||||
border: 1px solid var(--color-border);
|
||||
margin: 0;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
#plugin_list > li.expanded {
|
||||
min-height: 128px;
|
||||
height: auto;
|
||||
#plugin_list > li > div:first-child {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
#plugin_list > li .icon_wrapper {
|
||||
display: inline;
|
||||
#plugin_list > li:hover:not(.incompatible) .title {
|
||||
color: var(--color-light);
|
||||
}
|
||||
#plugin_list > li > * {
|
||||
margin: 0;
|
||||
margin-left: 12px;
|
||||
cursor: default;
|
||||
.plugin_icon_area {
|
||||
flex: 0 0 48px;
|
||||
padding-top: 2px;
|
||||
text-align: center;
|
||||
margin-left: -2px;
|
||||
height: 52px;
|
||||
}
|
||||
.plugin_icon_area .icon {
|
||||
font-size: 32px;
|
||||
width: 48px;
|
||||
max-width: unset;
|
||||
margin-top: 8px;
|
||||
display: inline-block;
|
||||
}
|
||||
.plugin_icon_area img {
|
||||
border-radius: 8px;
|
||||
pointer-events: none;
|
||||
}
|
||||
#plugin_list > li * {
|
||||
cursor: inherit;
|
||||
}
|
||||
#plugin_list > li .button_bar {
|
||||
height: auto;
|
||||
@ -1165,10 +1224,11 @@ dialog#edit_bedrock_binding > .dialog_wrapper > .dialog_content {
|
||||
margin-top: 0;
|
||||
text-align: right;
|
||||
}
|
||||
#plugin_list > li .button_bar.tiny {
|
||||
color: var(--color-subtle_text);
|
||||
font-size: 0.86em;
|
||||
padding-right: 2px;
|
||||
.plugin_compatibility_issue {
|
||||
color: var(--color-error);
|
||||
}
|
||||
.plugin_compatibility_issue > .icon {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
#plugin_list > li button {
|
||||
min-width: 100px;
|
||||
@ -1186,29 +1246,7 @@ dialog#edit_bedrock_binding > .dialog_wrapper > .dialog_content {
|
||||
margin-top: 2px;
|
||||
}
|
||||
#plugin_list > li .title {
|
||||
width: auto;
|
||||
float: left;
|
||||
font-size: 1.34em;
|
||||
padding-top: 8px;
|
||||
margin-bottom: -5px;
|
||||
height: 48px;
|
||||
}
|
||||
#plugin_list > li .title i {
|
||||
width: 22px;
|
||||
font-size: 0.9em;
|
||||
padding: 3px;
|
||||
float: left;
|
||||
margin-top: 5px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
#plugin_list > li .title i.plugin_expand_icon {
|
||||
display: none;
|
||||
}
|
||||
#plugin_list > li.has_about_text .title:hover i.plugin_expand_icon {
|
||||
display: inline-block;
|
||||
}
|
||||
#plugin_list > li.has_about_text .title:hover .plugin_icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#plugin_list .plugin_version {
|
||||
@ -1228,32 +1266,53 @@ dialog#edit_bedrock_binding > .dialog_wrapper > .dialog_content {
|
||||
max-height: 148px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
#plugin_list .about {
|
||||
overflow-y: auto;
|
||||
max-height: 480px;
|
||||
margin: 6px 0;
|
||||
padding: 6px 12px;
|
||||
background: var(--color-button);
|
||||
.plugin_installed_tag,
|
||||
.plugin_disabled_tag {
|
||||
display: inline-block;
|
||||
background-color: var(--color-back);
|
||||
height: 25px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 5px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.plugin_installed_tag {
|
||||
color: var(--color-confirm);
|
||||
}
|
||||
dialog#plugins .version {
|
||||
display: inline-block;
|
||||
color: var(--color-subtle_text);
|
||||
background-color: var(--color-back);
|
||||
font-size: 15px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 5px;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
dialog#plugins .author {
|
||||
color: var(--color-subtle_text);
|
||||
margin-top: -6px;
|
||||
}
|
||||
#plugin_list > li ul.plugin_tag_list {
|
||||
margin: 5px 8px;
|
||||
margin-top: 4px;
|
||||
line-height: 0;
|
||||
}
|
||||
.plugin_tag_list li {
|
||||
display: inline-block;
|
||||
background-color: var(--color-accent);
|
||||
color: var(--color-accent_text);
|
||||
max-width: 180px;
|
||||
height: 22px;
|
||||
padding: 1px 4px;
|
||||
height: 25px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 5px;
|
||||
font-size: 0.9em;
|
||||
margin: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
overflow: hidden;
|
||||
line-height: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
#plugin_list .plugin_tag_list li {
|
||||
height: 22px;
|
||||
font-size: 0.9em;
|
||||
padding: 1px 4px;
|
||||
}
|
||||
.plugin_tag_list li.plugin_tag_source {
|
||||
background-color: #ff7a52;
|
||||
color: #111625;
|
||||
@ -1271,6 +1330,127 @@ dialog#edit_bedrock_binding > .dialog_wrapper > .dialog_content {
|
||||
margin-top: 30px;
|
||||
color: var(--color-subtle_text);
|
||||
}
|
||||
#plugin_browser_page .button_bar {
|
||||
margin: 8px 0px;
|
||||
float: right;
|
||||
}
|
||||
#plugin_browser_page .button_bar button {
|
||||
height: 70px;
|
||||
background-color: transparent;
|
||||
margin: 0;
|
||||
min-width: 72px;
|
||||
width: auto;
|
||||
margin-right: 0;
|
||||
padding: 0 2px;
|
||||
text-decoration: none;
|
||||
}
|
||||
#plugin_browser_page .button_bar button:hover {
|
||||
background-color: var(--color-accent);
|
||||
}
|
||||
#plugin_browser_page .button_bar button i {
|
||||
display: block;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
max-width: unset;
|
||||
font-size: 35px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.plugin_dependencies {
|
||||
color: var(--color-subtle_text);
|
||||
margin: 10px 0;
|
||||
}
|
||||
.plugin_dependencies > a {
|
||||
background-color: var(--color-back);
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
padding: 1px 4px;
|
||||
border-radius: 5px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.plugin_dependencies > a:hover {
|
||||
color: var(--color-light);
|
||||
}
|
||||
.disabled_plugin {
|
||||
color: var(--color-subtle_text);
|
||||
}
|
||||
.plugin_browser_page_header {
|
||||
display: flex;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.plugin_browser_page_header .plugin_icon_area {
|
||||
flex-basis: 64px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.plugin_browser_page_header h1 {
|
||||
margin: 0;
|
||||
}
|
||||
#plugin_browser_page .about, #plugin_browser_page .description {
|
||||
user-select: text;
|
||||
}
|
||||
#plugin_page_background_decoration {
|
||||
pointer-events: none;
|
||||
color: black;
|
||||
opacity: 0.1;
|
||||
font-size: 700px;
|
||||
height: 614px;
|
||||
width: 584px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
overflow: hidden;
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
|
||||
#plugin_browser_start_page > img {
|
||||
float: right;
|
||||
width: 320px;
|
||||
margin-bottom: -26px;
|
||||
margin-top: -20px;
|
||||
}
|
||||
.plugins_suggested_row {
|
||||
width: 100%;
|
||||
clear: both;
|
||||
margin-top: 30px;
|
||||
}
|
||||
.plugins_suggested_row > ul {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
background-color: var(--color-back);
|
||||
overflow-x: scroll;
|
||||
padding: 12px 32px;
|
||||
width: calc(100% + 48px);
|
||||
margin-right: -24px;
|
||||
margin-left: -24px;
|
||||
}
|
||||
.plugins_suggested_row > ul > li {
|
||||
width: 211px;
|
||||
height: 130px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
padding-top: 5px;
|
||||
background-color: var(--color-ui);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
padding: 4px 4px;
|
||||
}
|
||||
.plugins_suggested_row > ul > li:hover {
|
||||
background-color: var(--color-button);
|
||||
}
|
||||
.plugins_suggested_row > ul > li * {
|
||||
cursor: inherit;
|
||||
}
|
||||
.plugins_suggested_row > ul > li .title {
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
line-height: normal;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Search Bar */
|
||||
.search_bar {
|
||||
float: right;
|
||||
position: relative;
|
||||
@ -1301,7 +1481,6 @@ dialog#edit_bedrock_binding > .dialog_wrapper > .dialog_content {
|
||||
color: var(--color-light);
|
||||
}
|
||||
|
||||
|
||||
/*Toolbar Dialog*/
|
||||
dialog#toolbar_edit .search_bar {
|
||||
margin-top: 10px;
|
||||
|
@ -283,9 +283,19 @@
|
||||
@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } }
|
||||
@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } }
|
||||
/*Markdown*/
|
||||
.markdown h1 {
|
||||
margin: 8px 0 8px 0;
|
||||
}
|
||||
.markdown h2 {
|
||||
margin: 14px 0 8px 0;
|
||||
}
|
||||
.markdown p {
|
||||
margin: 12px 0;
|
||||
}
|
||||
.markdown li {
|
||||
list-style: initial;
|
||||
cursor: inherit;
|
||||
margin: 8px 0;
|
||||
}
|
||||
.markdown ul {
|
||||
padding-left: 24px;
|
||||
@ -313,6 +323,26 @@
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
blockquote {
|
||||
border-left: 4px solid var(--color-accent);
|
||||
padding: 4px;
|
||||
padding-left: 16px;
|
||||
background: var(--color-back);
|
||||
}
|
||||
.markdown table {
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-back);
|
||||
}
|
||||
.markdown th {
|
||||
padding: 4px;
|
||||
}
|
||||
.markdown td {
|
||||
padding: 3px 4px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
.markdown img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/*Actions*/
|
||||
.toolbar {
|
||||
@ -664,6 +694,18 @@
|
||||
padding: 0;
|
||||
background-color: var(--color-menu_separator);
|
||||
}
|
||||
li.menu_separator.has_label {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
li.menu_separator.has_label > label {
|
||||
background-color: var(--color-bright_ui);
|
||||
color: color-mix(in srgb, var(--color-bright_ui_text) 70%, transparent);
|
||||
margin-top: -12px;
|
||||
margin-left: 12px;
|
||||
padding: 0 5px;
|
||||
height: 20px;
|
||||
}
|
||||
.contextMenu li.highlighted {
|
||||
animation: menu_item_highlight 1s infinite ease-in-out;
|
||||
}
|
||||
@ -713,7 +755,8 @@
|
||||
body.theme_borders .dialog_close_button,
|
||||
body.theme_borders #start_screen > content,
|
||||
body.theme_borders #quick_message_box,
|
||||
body.theme_borders action_selector > #action_selector_list
|
||||
body.theme_borders action_selector > #action_selector_list,
|
||||
body.theme_borders .plugins_suggested_row > ul > li
|
||||
{
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
@ -3,11 +3,17 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
outline: none;
|
||||
outline-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
body {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
body.is_mobile * {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
/*Webkit*/
|
||||
a[href] {
|
||||
color: inherit;
|
||||
@ -402,19 +408,21 @@
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: var(--font-headline);
|
||||
margin: 12px 0 8px 0;
|
||||
}
|
||||
h1 {
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
h2 {
|
||||
font-weight: lighter;
|
||||
margin: 0;
|
||||
}
|
||||
h3 {
|
||||
display: inline-block;
|
||||
font-size: 1.28em;
|
||||
padding-bottom: 4px;
|
||||
font-weight: inherit;
|
||||
margin-left: 16px;
|
||||
min-width: 10px;
|
||||
height: 32px;
|
||||
}
|
||||
h4 {
|
||||
font-size: 1.2em;
|
||||
@ -471,7 +479,8 @@
|
||||
}
|
||||
button > i {
|
||||
pointer-events: none;
|
||||
vertical-align: middle;
|
||||
vertical-align: sub;
|
||||
margin-right: 4px;
|
||||
}
|
||||
select {
|
||||
-webkit-appearance: none;
|
||||
|
@ -139,6 +139,7 @@
|
||||
<script src="js/modeling/transform.js"></script>
|
||||
<script src="js/modeling/scale.js"></script>
|
||||
<script src="js/modeling/mesh_editing.js"></script>
|
||||
<script src="js/modeling/mirror_modeling.js"></script>
|
||||
<script src="js/texturing/textures.js"></script>
|
||||
<script src="js/texturing/uv.js"></script>
|
||||
<script src="js/texturing/painter.js"></script>
|
||||
|
@ -785,13 +785,13 @@ class Animation extends AnimationItem {
|
||||
'copy',
|
||||
'paste',
|
||||
'duplicate',
|
||||
'_',
|
||||
new MenuSeparator('settings'),
|
||||
{name: 'menu.animation.loop', icon: 'loop', children: [
|
||||
{name: 'menu.animation.loop.once', icon: animation => (animation.loop == 'once' ? 'radio_button_checked' : 'radio_button_unchecked'), click(animation) {animation.setLoop('once', true)}},
|
||||
{name: 'menu.animation.loop.hold', icon: animation => (animation.loop == 'hold' ? 'radio_button_checked' : 'radio_button_unchecked'), click(animation) {animation.setLoop('hold', true)}},
|
||||
{name: 'menu.animation.loop.loop', icon: animation => (animation.loop == 'loop' ? 'radio_button_checked' : 'radio_button_unchecked'), click(animation) {animation.setLoop('loop', true)}},
|
||||
]},
|
||||
'_',
|
||||
new MenuSeparator('manage'),
|
||||
{
|
||||
name: 'menu.animation.save',
|
||||
id: 'save',
|
||||
@ -823,7 +823,7 @@ class Animation extends AnimationItem {
|
||||
}
|
||||
},
|
||||
'delete',
|
||||
'_',
|
||||
new MenuSeparator('properties'),
|
||||
{name: 'menu.animation.properties', icon: 'list', click(animation) {
|
||||
animation.propertiesDialog();
|
||||
}}
|
||||
|
@ -412,7 +412,7 @@ AnimationControllerState.prototype.menu = new Menu([
|
||||
Undo.finishEdit('Change animation controller initial state');
|
||||
}
|
||||
},
|
||||
'_',
|
||||
new MenuSeparator('manage'),
|
||||
'duplicate',
|
||||
'rename',
|
||||
'delete',
|
||||
@ -833,10 +833,11 @@ class AnimationController extends AnimationItem {
|
||||
})
|
||||
AnimationController.selected = null;
|
||||
AnimationController.prototype.menu = new Menu([
|
||||
new MenuSeparator('copypaste'),
|
||||
'copy',
|
||||
'paste',
|
||||
'duplicate',
|
||||
'_',
|
||||
new MenuSeparator('manage'),
|
||||
{
|
||||
name: 'menu.animation.save',
|
||||
id: 'save',
|
||||
@ -868,7 +869,7 @@ class AnimationController extends AnimationItem {
|
||||
}
|
||||
},
|
||||
'delete',
|
||||
'_',
|
||||
new MenuSeparator('properties'),
|
||||
{name: 'menu.animation.properties', icon: 'cable', click(animation) {
|
||||
animation.propertiesDialog();
|
||||
}}
|
||||
|
@ -112,13 +112,13 @@ class Keyframe {
|
||||
}
|
||||
offset(axis, amount, data_point = 0) {
|
||||
if (data_point) data_point = Math.clamp(data_point, 0, this.data_points.length-1);
|
||||
var value = this.get(axis)
|
||||
var value = this.get(axis, data_point);
|
||||
if (!value || value === '0') {
|
||||
this.set(axis, amount, data_point)
|
||||
this.set(axis, amount, data_point);
|
||||
return amount;
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
this.set(axis, value+amount, data_point)
|
||||
this.set(axis, value+amount, data_point);
|
||||
return value+amount
|
||||
}
|
||||
var start = value.match(/^-?\s*\d+(\.\d+)?\s*(\+|-)/)
|
||||
@ -520,8 +520,7 @@ class Keyframe {
|
||||
}
|
||||
}
|
||||
Keyframe.prototype.menu = new Menu([
|
||||
'change_keyframe_file',
|
||||
'_',
|
||||
new MenuSeparator('settings'),
|
||||
'keyframe_uniform',
|
||||
'keyframe_interpolation',
|
||||
'keyframe_bezier_linked',
|
||||
@ -540,7 +539,7 @@ class Keyframe {
|
||||
}})
|
||||
];
|
||||
}},
|
||||
'_',
|
||||
new MenuSeparator('copypaste'),
|
||||
'copy',
|
||||
'delete',
|
||||
])
|
||||
@ -1297,7 +1296,7 @@ Interface.definePanels(function() {
|
||||
Timeline.vue.graph_editor_axis = axis;
|
||||
}
|
||||
},
|
||||
slideValue(axis, e1) {
|
||||
slideValue(axis, e1, data_point) {
|
||||
convertTouchEvent(e1);
|
||||
let last_event = e1;
|
||||
let started = false;
|
||||
@ -1333,7 +1332,7 @@ Interface.definePanels(function() {
|
||||
}
|
||||
|
||||
Keyframe.selected.forEach(kf => {
|
||||
kf.offset(axis, difference);
|
||||
kf.offset(axis, difference, data_point);
|
||||
})
|
||||
|
||||
last_val = val;
|
||||
@ -1507,7 +1506,7 @@ Interface.definePanels(function() {
|
||||
class="bar flex"
|
||||
:id="'keyframe_bar_' + property.name"
|
||||
>
|
||||
<label :class="{[channel_colors[key]]: true, slidable_input: property.type == 'molang'}" :style="{'font-weight': channel_colors[key] ? 'bolder' : 'unset'}" @mousedown="slideValue(key, $event)" @touchstart="slideValue(key, $event)">{{ property.label }}</label>
|
||||
<label :class="{[channel_colors[key]]: true, slidable_input: property.type == 'molang'}" :style="{'font-weight': channel_colors[key] ? 'bolder' : 'unset'}" @mousedown="slideValue(key, $event, data_point_i)" @touchstart="slideValue(key, $event, data_point_i)">{{ property.label }}</label>
|
||||
<vue-prism-editor
|
||||
v-if="property.type == 'molang'"
|
||||
class="molang_input keyframe_input tab_target"
|
||||
|
@ -27,6 +27,7 @@ class TimelineMarker {
|
||||
}
|
||||
}
|
||||
TimelineMarker.prototype.menu = new Menu([
|
||||
new MenuSeparator('settings'),
|
||||
{name: 'menu.cube.color', icon: 'color_lens', children() {
|
||||
return [
|
||||
...markerColors.map((color, i) => {return {
|
||||
@ -53,7 +54,7 @@ TimelineMarker.prototype.menu = new Menu([
|
||||
}).show();
|
||||
}
|
||||
},
|
||||
'_',
|
||||
new MenuSeparator('manage'),
|
||||
{icon: 'delete', name: 'generic.delete', click: function(marker) {
|
||||
if (Animation.selected) Animation.selected.markers.remove(marker);
|
||||
}}
|
||||
@ -585,8 +586,9 @@ const Timeline = {
|
||||
Timeline.menu.open(event, event);
|
||||
},
|
||||
menu: new Menu([
|
||||
new MenuSeparator('copypaste'),
|
||||
'paste',
|
||||
'_',
|
||||
new MenuSeparator('view'),
|
||||
{name: 'menu.view.zoom', id: 'zoom', condition: isApp, icon: 'search', children: [
|
||||
'zoom_in',
|
||||
'zoom_out',
|
||||
@ -594,12 +596,12 @@ const Timeline = {
|
||||
]},
|
||||
'select_all',
|
||||
'fold_all_animations',
|
||||
'_',
|
||||
'timeline_setups',
|
||||
'save_timeline_setup',
|
||||
'bring_up_all_animations',
|
||||
'clear_timeline',
|
||||
'_',
|
||||
new MenuSeparator('timeline_setups'),
|
||||
'timeline_setups',
|
||||
'save_timeline_setup',
|
||||
new MenuSeparator('graph_editor'),
|
||||
'graph_editor_other_graphs',
|
||||
'graph_editor_include_other_graphs',
|
||||
'graph_editor_zero_line',
|
||||
|
@ -253,6 +253,8 @@ const Clipbench = {
|
||||
new_mesh = new Mesh({name: 'pasted', vertices: []});
|
||||
elements.push(new_mesh);
|
||||
}
|
||||
let selection_mode_before = BarItems.selection_mode.value;
|
||||
BarItems.selection_mode.change('vertex');
|
||||
elements.forEach(mesh => {
|
||||
let old_vertices = Object.keys(this.vertices);
|
||||
let vertices_positions = old_vertices.map(vkey => this.vertices[vkey]);
|
||||
@ -276,6 +278,9 @@ const Clipbench = {
|
||||
if (new_mesh) {
|
||||
new_mesh.init().select();
|
||||
}
|
||||
// Update vertex selection to appropriate selection mode
|
||||
BarItems.selection_mode.change(selection_mode_before);
|
||||
|
||||
Undo.finishEdit('Paste mesh selection');
|
||||
Canvas.updateView({elements: Mesh.selected, selection: true})
|
||||
},
|
||||
|
@ -180,7 +180,9 @@ Object.assign(Blockbench, {
|
||||
if (!errant && options.errorbox !== false) {
|
||||
Blockbench.showMessageBox({
|
||||
translateKey: 'file_not_found',
|
||||
icon: 'error_outline'
|
||||
message: tl('message.file_not_found.message') + '\n\n```' + file.replace(/[`"<>]/g, '') + '```',
|
||||
icon: 'error_outline',
|
||||
width: 520
|
||||
})
|
||||
}
|
||||
errant = true;
|
||||
|
@ -48,7 +48,10 @@ const ActionControl = {
|
||||
$('body').effect('shake');
|
||||
Blockbench.showQuickMessage('Congratulations! You have discovered recursion!', 3000)
|
||||
}
|
||||
if (action.type == 'recent_project') {
|
||||
if (action instanceof BarSelect) {
|
||||
action.open({target: ActionControl.vue.$el.childNodes[2]});
|
||||
|
||||
} else if (action.type == 'recent_project') {
|
||||
Blockbench.read([action.description], {}, files => {
|
||||
loadModelFile(files[0]);
|
||||
})
|
||||
@ -190,14 +193,13 @@ BARS.defineActions(function() {
|
||||
}
|
||||
}
|
||||
if (!type) {
|
||||
for (var i = 0; i < Keybinds.actions.length; i++) {
|
||||
var item = Keybinds.actions[i];
|
||||
for (let item of Keybinds.actions) {
|
||||
if (
|
||||
search_input.length == 0 ||
|
||||
item.name.toLowerCase().includes(search_input) ||
|
||||
item.id.toLowerCase().includes(search_input)
|
||||
) {
|
||||
if (item instanceof Action && Condition(item.condition) && !item.linked_setting) {
|
||||
if ((item instanceof Action || item instanceof BarSelect) && Condition(item.condition) && !item.linked_setting) {
|
||||
list.safePush(item)
|
||||
if (list.length > ActionControl.max_length) break;
|
||||
}
|
||||
@ -392,7 +394,7 @@ BARS.defineActions(function() {
|
||||
template: `
|
||||
<dialog id="action_selector" v-if="open">
|
||||
<div class="tool" ref="search_type_menu" @click="openTypeMenu($event)">
|
||||
<div class="icon_wrapper normal" v-html="getIconNode(search_types[search_type] ? search_types[search_type].icon : 'fullscreen').outerHTML"></div>
|
||||
<dynamic-icon :icon="search_types[search_type] ? search_types[search_type].icon : 'fullscreen'" />
|
||||
</div>
|
||||
<input type="text" v-model="search_input" inputmode="search" @input="e => search_input = e.target.value" autocomplete="off" autosave="off" autocorrect="off" spellcheck="false" autocapitalize="off">
|
||||
<i class="material-icons" id="action_search_bar_icon" @click="search_input = ''">{{ search_input ? 'clear' : 'search' }}</i>
|
||||
@ -405,7 +407,7 @@ BARS.defineActions(function() {
|
||||
@click="click(item, $event)"
|
||||
@mouseenter="index = i"
|
||||
>
|
||||
<div class="icon_wrapper normal" v-html="getIconNode(item.icon, item.color).outerHTML"></div>
|
||||
<dynamic-icon :icon="item.icon" :color="item.color" />
|
||||
<span>{{ item.name }}</span>
|
||||
<label class="keybinding_label">{{ item.keybind_label || (item.keybind ? item.keybind.label : '') }}</label>
|
||||
</li>
|
||||
|
@ -1,12 +1,8 @@
|
||||
var Toolbars, BarItems, Toolbox;
|
||||
//Bars
|
||||
class MenuSeparator {
|
||||
constructor() {
|
||||
this.menu_node = Interface.createElement('li', {class: 'menu_separator'});
|
||||
}
|
||||
}
|
||||
class BarItem {
|
||||
class BarItem extends EventSystem {
|
||||
constructor(id, data) {
|
||||
super();
|
||||
this.id = id;
|
||||
if (!data.private) {
|
||||
if (this.id && !BarItems[this.id]) {
|
||||
@ -166,7 +162,7 @@ class BarItem {
|
||||
var scope = this;
|
||||
if (scope.uniqueNode && scope.toolbars.length) {
|
||||
for (var i = scope.toolbars.length-1; i >= 0; i--) {
|
||||
scope.toolbars[i].remove(scope)
|
||||
scope.toolbars[i].remove(scope, false);
|
||||
}
|
||||
}
|
||||
if (idx !== undefined) {
|
||||
@ -194,6 +190,7 @@ class BarItem {
|
||||
this.sub_keybinds[id].keybind.setAction(this.id, id);
|
||||
}
|
||||
delete() {
|
||||
this.dispatchEvent('delete');
|
||||
var scope = this;
|
||||
this.toolbars.forEach(bar => {
|
||||
bar.remove(scope);
|
||||
@ -261,7 +258,14 @@ class Action extends BarItem {
|
||||
this.searchable = data.searchable;
|
||||
|
||||
//Node
|
||||
if (!this.click) this.click = data.click
|
||||
if (!this.click && data.click) {
|
||||
this.onClick = data.click;
|
||||
this.click = (...args) => {
|
||||
this.dispatchEvent('use');
|
||||
this.onClick(...args);
|
||||
this.dispatchEvent('used');
|
||||
};
|
||||
}
|
||||
this.icon_node = Blockbench.getIconNode(this.icon, this.color)
|
||||
this.icon_states = data.icon_states;
|
||||
this.node = document.createElement('div');
|
||||
@ -275,6 +279,17 @@ class Action extends BarItem {
|
||||
Interface.createElement('span', {}, this.name),
|
||||
Interface.createElement('label', {class: 'keybinding_label'}, this.keybind || '')
|
||||
]);
|
||||
addEventListeners(this.menu_node, 'mouseenter mousedown', event => {
|
||||
if (event.target == this.menu_node) {
|
||||
Menu.open.hover(this.menu_node, event);
|
||||
}
|
||||
});
|
||||
if (!this.children) {
|
||||
this.menu_node.addEventListener('click', event => {
|
||||
if (!(event.target == this.menu_node || event.target.parentElement == this.menu_node)) return;
|
||||
this.trigger(event);
|
||||
});
|
||||
}
|
||||
|
||||
this.addLabel(data.label)
|
||||
this.updateKeybindingLabel()
|
||||
@ -302,7 +317,9 @@ class Action extends BarItem {
|
||||
}
|
||||
trigger(event) {
|
||||
var scope = this;
|
||||
if (BARS.condition(scope.condition, scope)) {
|
||||
let condition_met = BARS.condition(scope.condition, this);
|
||||
this.dispatchEvent('trigger', {condition_met});
|
||||
if (condition_met) {
|
||||
if (event && event.type === 'click' && event.altKey && scope.keybind) {
|
||||
var record = function() {
|
||||
document.removeEventListener('keyup', record)
|
||||
@ -352,6 +369,7 @@ class Action extends BarItem {
|
||||
}
|
||||
}
|
||||
}
|
||||
this.dispatchEvent('get_node', {node: clone});
|
||||
return clone;
|
||||
}
|
||||
setIcon(icon) {
|
||||
@ -423,6 +441,7 @@ class Tool extends Action {
|
||||
}
|
||||
select() {
|
||||
if (this === Toolbox.selected) return;
|
||||
let previous_tool = Toolbox.selected;
|
||||
if (Toolbox.selected) {
|
||||
Toolbox.selected.nodes.forEach(node => {
|
||||
node.classList.remove('enabled')
|
||||
@ -431,6 +450,9 @@ class Tool extends Action {
|
||||
if (typeof Toolbox.selected.onUnselect == 'function') {
|
||||
Toolbox.selected.onUnselect()
|
||||
}
|
||||
if (Toolbox.selected.brush?.size && !this.brush?.size) {
|
||||
scene.remove(Canvas.brush_outline);
|
||||
}
|
||||
if (Transformer.dragging) {
|
||||
Transformer.cancelMovement({}, true);
|
||||
}
|
||||
@ -458,6 +480,7 @@ class Tool extends Action {
|
||||
if (typeof this.onSelect == 'function') {
|
||||
this.onSelect()
|
||||
}
|
||||
this.dispatchEvent('select', {previous_tool});
|
||||
Interface.preview.style.cursor = this.cursor ? this.cursor : 'default';
|
||||
this.nodes.forEach(node => {
|
||||
node.classList.add('enabled')
|
||||
@ -507,6 +530,7 @@ class Toggle extends Action {
|
||||
Settings.saveLocalStorages();
|
||||
}
|
||||
if (this.onChange) this.onChange(this.value);
|
||||
this.dispatchEvent('change', {state: this.value});
|
||||
|
||||
this.updateEnabledState();
|
||||
}
|
||||
@ -563,7 +587,12 @@ class NumSlider extends Widget {
|
||||
this.onBefore = data.onBefore;
|
||||
this.onChange = data.onChange;
|
||||
this.onAfter = data.onAfter;
|
||||
if (typeof data.change === 'function') this.change = data.change;
|
||||
if (typeof data.change === 'function') {
|
||||
this.change = (modify, ...args) => {
|
||||
data.change(modify, ...args)
|
||||
this.dispatchEvent('changed', {modify});
|
||||
};
|
||||
}
|
||||
if (data.settings) {
|
||||
this.settings = data.settings;
|
||||
if (this.settings.default) {
|
||||
@ -729,6 +758,7 @@ class NumSlider extends Widget {
|
||||
})
|
||||
.on('contextmenu', event => {
|
||||
new Menu([
|
||||
new MenuSeparator('copypaste'),
|
||||
{
|
||||
id: 'copy',
|
||||
name: 'action.copy',
|
||||
@ -760,7 +790,7 @@ class NumSlider extends Widget {
|
||||
}, 20);
|
||||
}
|
||||
},
|
||||
'_',
|
||||
new MenuSeparator('edit'),
|
||||
{
|
||||
id: 'round',
|
||||
name: 'menu.slider.round_value',
|
||||
@ -967,6 +997,7 @@ class NumSlider extends Widget {
|
||||
if (typeof this.onChange === 'function') {
|
||||
this.onChange(num);
|
||||
}
|
||||
this.dispatchEvent('change', {number: num});
|
||||
}
|
||||
get() {
|
||||
//Solo Sliders only
|
||||
@ -985,6 +1016,7 @@ class NumSlider extends Widget {
|
||||
if (isNaN(number) && !this.jq_inner.hasClass('editing') && this.jq_inner[0].textContent) {
|
||||
this.jq_inner.text('')
|
||||
}
|
||||
this.dispatchEvent('update');
|
||||
}
|
||||
}
|
||||
NumSlider.MolangParser = new Molang()
|
||||
@ -1039,6 +1071,7 @@ class BarSlider extends Widget {
|
||||
if (this.onChange) {
|
||||
this.onChange(this, event)
|
||||
}
|
||||
this.dispatchEvent('change', {value: this.value});
|
||||
}
|
||||
set(value) {
|
||||
this.value = value
|
||||
@ -1187,13 +1220,16 @@ class BarSelect extends Widget {
|
||||
}
|
||||
}
|
||||
let menu = new Menu(this.id, items);
|
||||
this.dispatchEvent('open', {menu, items});
|
||||
menu.node.style['min-width'] = this.node.clientWidth+'px';
|
||||
menu.open(event.target, this);
|
||||
}
|
||||
trigger(event) {
|
||||
if (!event) event = 0;
|
||||
var scope = this;
|
||||
if (BARS.condition(scope.condition, scope)) {
|
||||
let condition_met = BARS.condition(this.condition, this);
|
||||
this.dispatchEvent('trigger', {condition_met});
|
||||
if (condition_met) {
|
||||
if (event && event.type === 'click' && event.altKey && scope.keybind) {
|
||||
var record = function() {
|
||||
document.removeEventListener('keyup', record)
|
||||
@ -1233,6 +1269,7 @@ class BarSelect extends Widget {
|
||||
if (this.onChange) {
|
||||
this.onChange(this, event);
|
||||
}
|
||||
this.dispatchEvent('change', {value, event});
|
||||
return this;
|
||||
}
|
||||
getNameFor(key) {
|
||||
@ -1299,10 +1336,12 @@ class BarText extends Widget {
|
||||
if (typeof this.onUpdate === 'function') {
|
||||
this.onUpdate()
|
||||
}
|
||||
this.dispatchEvent('update');
|
||||
return this;
|
||||
}
|
||||
trigger(event) {
|
||||
if (!Condition(this.condition)) return false;
|
||||
this.dispatchEvent('trigger');
|
||||
Blockbench.showQuickMessage(this.text)
|
||||
return true;
|
||||
}
|
||||
@ -1352,6 +1391,7 @@ class ColorPicker extends Widget {
|
||||
if (this.onChange) {
|
||||
this.onChange()
|
||||
}
|
||||
this.dispatchEvent('change', {color});
|
||||
}
|
||||
hide() {
|
||||
this.jq.spectrum('cancel');
|
||||
@ -1448,9 +1488,6 @@ class Toolbar {
|
||||
|
||||
if (item) {
|
||||
item.pushToolbar(this);
|
||||
/*if (BARS.condition(item.condition)) {
|
||||
content.append(item.getNode())
|
||||
}*/
|
||||
this.positionLookup[itemPosition] = item;
|
||||
} else {
|
||||
var postloadAction = [items[itemPosition], itemPosition];
|
||||
@ -1467,6 +1504,7 @@ class Toolbar {
|
||||
if (data.default_place) {
|
||||
this.toPlace(this.id)
|
||||
}
|
||||
this.condition_cache.empty();
|
||||
return this;
|
||||
}
|
||||
contextmenu(event) {
|
||||
@ -1501,14 +1539,14 @@ class Toolbar {
|
||||
this.update().save();
|
||||
return this;
|
||||
}
|
||||
remove(action) {
|
||||
remove(action, update = true) {
|
||||
var i = this.children.length-1;
|
||||
while (i >= 0) {
|
||||
var item = this.children[i]
|
||||
if (item === action || item.id === action) {
|
||||
item.toolbars.remove(this)
|
||||
this.children.splice(i, 1)
|
||||
this.update(true).save();
|
||||
if (update != false) this.update(true).save();
|
||||
return this;
|
||||
}
|
||||
i--;
|
||||
@ -1553,7 +1591,6 @@ class Toolbar {
|
||||
}
|
||||
}
|
||||
|
||||
//scope.condition_cache.empty();
|
||||
let needsUpdate = force === true || scope.condition_cache.length !== scope.children.length;
|
||||
scope.condition_cache.length = scope.children.length;
|
||||
|
||||
@ -1854,7 +1891,7 @@ const BARS = {
|
||||
} else if (Modes.edit && Mesh.selected.length && mesh_selection) {
|
||||
|
||||
let meshes = Mesh.selected.slice();
|
||||
Undo.initEdit({elements: meshes})
|
||||
Undo.initEdit({elements: meshes, outliner: true})
|
||||
|
||||
Mesh.selected.forEach(mesh => {
|
||||
let selected_vertices = mesh.getSelectedVertices();
|
||||
@ -2514,7 +2551,7 @@ const BARS = {
|
||||
|
||||
<ul class="list" id="bar_item_list">
|
||||
<li v-for="item in searchedBarItems" v-on:click="addItem(item)" :class="{separator_item: item.type == 'separator'}">
|
||||
<div class="icon_wrapper normal" v-html="getIconNode(item.icon, item.color).outerHTML"></div>
|
||||
<dynamic-icon :icon="item.icon" :color="item.color" />
|
||||
<div class="icon_wrapper add"><i class="material-icons">add</i></div>
|
||||
{{ item.name }}
|
||||
</li>
|
||||
|
@ -296,7 +296,7 @@ function buildForm(dialog) {
|
||||
if (data.type == 'folder' && !isApp) break;
|
||||
|
||||
let input = $(`<input class="dark_bordered half" class="focusable_input" type="text" id="${form_id}" disabled>`);
|
||||
input[0].value = data.value || '';
|
||||
input[0].value = settings.streamer_mode.value ? `[${tl('generic.redacted')}]` : data.value || '';
|
||||
let input_wrapper = $('<div class="input_wrapper"></div>');
|
||||
input_wrapper.append(input);
|
||||
bar.append(input_wrapper);
|
||||
@ -319,7 +319,7 @@ function buildForm(dialog) {
|
||||
function fileCB(files) {
|
||||
data.value = files[0].path;
|
||||
data.content = files[0].content;
|
||||
input.val(data.value);
|
||||
input.val(settings.streamer_mode.value ? `[${tl('generic.redacted')}]` : data.value);
|
||||
dialog.updateFormValues()
|
||||
}
|
||||
switch (data.type) {
|
||||
@ -399,10 +399,17 @@ function buildLines(dialog) {
|
||||
})
|
||||
}
|
||||
function buildComponent(dialog) {
|
||||
let dialog_content = $(dialog.object).find('.dialog_content')
|
||||
let mount = $(`<div />`).appendTo(dialog_content)
|
||||
let dialog_content = $(dialog.object).find('.dialog_content').get(0);
|
||||
let mount;
|
||||
// mount_directly, if enabled, skips one layer of wrapper. Class "dialog_content" must be added the the root element of the vue component.
|
||||
if (dialog.component.mount_directly) {
|
||||
mount = dialog_content;
|
||||
} else {
|
||||
mount = Interface.createElement('div');
|
||||
dialog_content.append(mount);
|
||||
}
|
||||
dialog.component.name = 'dialog-content'
|
||||
dialog.content_vue = new Vue(dialog.component).$mount(mount.get(0));
|
||||
dialog.content_vue = new Vue(dialog.component).$mount(mount);
|
||||
}
|
||||
function getStringWidth(string, size) {
|
||||
let node = Interface.createElement('label', {style: 'position: absolute; visibility: hidden;'}, string);
|
||||
@ -604,7 +611,7 @@ window.Dialog = class Dialog {
|
||||
} else {
|
||||
data.content = value;
|
||||
}
|
||||
data.bar.find('input').val(value);
|
||||
data.bar.find('input').val(settings.streamer_mode.value ? `[${tl('generic.redacted')}]` : value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,15 @@
|
||||
var open_menu = null;
|
||||
|
||||
class MenuSeparator {
|
||||
constructor(id, label) {
|
||||
this.id = id || '';
|
||||
this.menu_node = Interface.createElement('li', {class: 'menu_separator', menu_separator_id: id});
|
||||
if (label) {
|
||||
label = tl(label);
|
||||
this.menu_node.append(Interface.createElement('label', {}, label));
|
||||
this.menu_node.classList.add('has_label');
|
||||
}
|
||||
}
|
||||
}
|
||||
function handleMenuOverflow(node) {
|
||||
node = node.get(0);
|
||||
if (!node) return;
|
||||
@ -56,13 +66,13 @@ class Menu {
|
||||
$(open_menu.node).find('li.focused').removeClass('focused')
|
||||
$(open_menu.node).find('li.opened').removeClass('opened')
|
||||
var obj = $(node)
|
||||
obj.addClass('focused')
|
||||
node.classList.add('focused')
|
||||
obj.parents('li.parent, li.hybrid_parent').addClass('opened')
|
||||
|
||||
if (obj.hasClass('parent') || (expand && obj.hasClass('hybrid_parent'))) {
|
||||
var childlist = obj.find('> ul.contextMenu.sub')
|
||||
|
||||
if (expand) obj.addClass('opened');
|
||||
if (expand) node.classList.add('opened');
|
||||
|
||||
var p_width = obj.outerWidth()
|
||||
childlist.css('left', p_width + 'px')
|
||||
@ -142,7 +152,7 @@ class Menu {
|
||||
}
|
||||
used = true;
|
||||
} else if (Keybinds.extra.confirm.keybind.isTriggered(e)) {
|
||||
obj.find('li.focused').click()
|
||||
obj.find('li.focused').trigger('click');
|
||||
if (scope && !this.options.keep_open) {
|
||||
//scope.hide()
|
||||
}
|
||||
@ -165,7 +175,7 @@ class Menu {
|
||||
if (open_menu) {
|
||||
open_menu.hide()
|
||||
}
|
||||
$('body').append(ctxmenu)
|
||||
document.body.append(this.node);
|
||||
|
||||
ctxmenu.children().detach()
|
||||
|
||||
@ -175,9 +185,10 @@ class Menu {
|
||||
} else if (!list) {
|
||||
list = object.children
|
||||
}
|
||||
node = $(node);
|
||||
node.find('ul.contextMenu.sub').detach();
|
||||
if (list.length) {
|
||||
var childlist = $('<ul class="contextMenu sub"></ul>')
|
||||
var childlist = $(Interface.createElement('ul', {class: 'contextMenu sub'}));
|
||||
|
||||
populateList(list, childlist, object.searchable);
|
||||
|
||||
@ -191,7 +202,7 @@ class Menu {
|
||||
})
|
||||
more_button.addEventListener('mouseleave', e => {
|
||||
if (node.is(':hover') && !childlist.is(':hover')) {
|
||||
scope.hover(node, e);
|
||||
scope.hover(node.get(0), e);
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -214,11 +225,11 @@ class Menu {
|
||||
|
||||
let object_list = [];
|
||||
list.forEach(function(s2, i) {
|
||||
let jq_node = getEntry(s2, menu_node);
|
||||
if (!jq_node) return;
|
||||
let node = getEntry(s2, menu_node);
|
||||
if (!node) return;
|
||||
object_list.push({
|
||||
object: s2,
|
||||
node: jq_node[0] || jq_node,
|
||||
node: node,
|
||||
id: s2.id,
|
||||
name: s2.name,
|
||||
description: s2.description,
|
||||
@ -261,9 +272,8 @@ class Menu {
|
||||
nodes.last().remove();
|
||||
}
|
||||
|
||||
if (!nodes.toArray().find(node => node.classList.contains('parent') || node.classList.contains('hybrid_parent'))) {
|
||||
menu_node.addClass('scrollable');
|
||||
}
|
||||
let is_scrollable = !nodes.toArray().find(node => node.classList.contains('parent') || node.classList.contains('hybrid_parent'));
|
||||
menu_node.toggleClass('scrollable', is_scrollable);
|
||||
}
|
||||
|
||||
function getEntry(s, parent) {
|
||||
@ -275,7 +285,12 @@ class Menu {
|
||||
let scope_context = context;
|
||||
var entry;
|
||||
if (s === '_') {
|
||||
entry = new MenuSeparator().menu_node
|
||||
s = new MenuSeparator();
|
||||
} else if (typeof s == 'string' && s.startsWith('#')) {
|
||||
s = new MenuSeparator(s.substring(1));
|
||||
}
|
||||
if (s instanceof MenuSeparator) {
|
||||
entry = s.menu_node;
|
||||
var last = parent.children().last()
|
||||
if (last.length && !last.hasClass('menu_separator')) {
|
||||
parent.append(entry)
|
||||
@ -289,24 +304,14 @@ class Menu {
|
||||
|
||||
if (s instanceof Action) {
|
||||
|
||||
entry = $(s.menu_node)
|
||||
entry = s.menu_node
|
||||
|
||||
entry.classList.remove('focused');
|
||||
|
||||
entry.removeClass('focused')
|
||||
entry.off('click')
|
||||
entry.off('mouseenter mousedown')
|
||||
entry.on('mouseenter mousedown', function(e) {
|
||||
if (this == e.target) {
|
||||
scope.hover(this, e)
|
||||
}
|
||||
})
|
||||
//Submenu
|
||||
if (typeof s.children == 'function' || typeof s.children == 'object') {
|
||||
createChildList(s, entry)
|
||||
} else {
|
||||
entry.on('click', (e) => {
|
||||
if (!(e.target == entry[0] || e.target.parentElement == entry[0])) return;
|
||||
s.trigger(e)
|
||||
});
|
||||
if (s.side_menu instanceof Menu) {
|
||||
let content_list = typeof s.side_menu.structure == 'function' ? s.side_menu.structure(scope_context) : s.side_menu.structure;
|
||||
createChildList(s, entry, content_list);
|
||||
@ -333,7 +338,7 @@ class Menu {
|
||||
} else {
|
||||
var icon = Blockbench.getIconNode(s.icon, s.color)
|
||||
}
|
||||
entry = $(Interface.createElement('li', {title: s.description && tl(s.description), menu_item: s.id}, Interface.createElement('span', {}, tl(s.name))));
|
||||
entry = Interface.createElement('li', {title: s.description && tl(s.description), menu_item: s.id}, Interface.createElement('span', {}, tl(s.name)));
|
||||
entry.prepend(icon)
|
||||
|
||||
//Submenu
|
||||
@ -365,8 +370,8 @@ class Menu {
|
||||
if (child_count !== 0 || typeof s.click === 'function') {
|
||||
parent.append(entry)
|
||||
}
|
||||
entry.mouseenter(function(e) {
|
||||
scope.hover(this, e)
|
||||
entry.addEventListener('mouseenter', function(e) {
|
||||
scope.hover(entry, e);
|
||||
})
|
||||
|
||||
/*} else if (s instanceof NumSlider) {
|
||||
@ -409,7 +414,7 @@ class Menu {
|
||||
} else {
|
||||
var icon = Blockbench.getIconNode(s.icon, s.color)
|
||||
}
|
||||
entry = $(Interface.createElement('li', {title: s.description && tl(s.description), menu_item: s.id}, Interface.createElement('span', {}, tl(s.name))));
|
||||
entry = Interface.createElement('li', {title: s.description && tl(s.description), menu_item: s.id}, Interface.createElement('span', {}, tl(s.name)));
|
||||
entry.prepend(icon);
|
||||
if (s.keybind) {
|
||||
let label = document.createElement('label');
|
||||
@ -418,8 +423,8 @@ class Menu {
|
||||
entry.append(label);
|
||||
}
|
||||
if (typeof s.click === 'function') {
|
||||
entry.on('click', e => {
|
||||
if (e.target == entry.get(0)) {
|
||||
entry.addEventListener('click', e => {
|
||||
if (e.target == entry) {
|
||||
s.click(scope_context, e)
|
||||
}
|
||||
})
|
||||
@ -431,16 +436,16 @@ class Menu {
|
||||
if (child_count !== 0 || typeof s.click === 'function') {
|
||||
parent.append(entry)
|
||||
}
|
||||
entry.mouseenter(function(e) {
|
||||
scope.hover(this, e)
|
||||
entry.addEventListener('mouseenter', (e) => {
|
||||
scope.hover(entry, e);
|
||||
})
|
||||
}
|
||||
//Highlight
|
||||
if (scope.highlight_action == s && entry) {
|
||||
let obj = entry;
|
||||
while (obj[0] && obj[0].nodeName == 'LI') {
|
||||
obj.addClass('highlighted');
|
||||
obj = obj.parent().parent();
|
||||
while (obj && obj.nodeName == 'LI') {
|
||||
obj.classList.add('highlighted');
|
||||
obj = obj.parentElement.parentElement;
|
||||
}
|
||||
}
|
||||
if (s.context && last_context != context) context = last_context;
|
||||
@ -520,9 +525,10 @@ class Menu {
|
||||
|
||||
if (scope.type === 'bar_menu') {
|
||||
MenuBar.open = scope
|
||||
$(scope.label).addClass('opened')
|
||||
scope.label.classList.add('opened');
|
||||
}
|
||||
open_menu = scope;
|
||||
Menu.open = this;
|
||||
return scope;
|
||||
}
|
||||
show(...args) {
|
||||
@ -531,8 +537,9 @@ class Menu {
|
||||
hide() {
|
||||
if (this.onClose) this.onClose();
|
||||
$(this.node).find('li.highlighted').removeClass('highlighted');
|
||||
$(this.node).detach()
|
||||
this.node.remove()
|
||||
open_menu = null;
|
||||
Menu.open = null;
|
||||
return this;
|
||||
}
|
||||
conditionMet() {
|
||||
@ -542,21 +549,38 @@ class Menu {
|
||||
if (this.structure instanceof Array == false) return;
|
||||
if (path === undefined) path = '';
|
||||
if (typeof path !== 'string') path = path.toString();
|
||||
var track = path.split('.')
|
||||
let track = path.split('.')
|
||||
|
||||
function traverse(arr, layer) {
|
||||
if (track.length === layer || track[layer] === '' || !isNaN(parseInt(track[layer]))) {
|
||||
var index = arr.length;
|
||||
if (track.length === layer || !track[layer] === '' || !isNaN(parseInt(track[layer])) || (track[layer][0] == '#')) {
|
||||
let index = arr.length;
|
||||
if (track[layer] !== '' && track.length !== layer) {
|
||||
index = parseInt(track[layer])
|
||||
if (track[layer].startsWith('#')) {
|
||||
// Group Anchor
|
||||
let group = track[layer].substring(1);
|
||||
let group_match = false;
|
||||
index = 0;
|
||||
for (let item of arr) {
|
||||
if (item instanceof MenuSeparator) {
|
||||
if (item.id == group) {
|
||||
group_match = true;
|
||||
} else if (group_match && item.id != group) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
index++;
|
||||
}
|
||||
} else {
|
||||
index = parseInt(track[layer])
|
||||
}
|
||||
}
|
||||
arr.splice(index, 0, action)
|
||||
} else {
|
||||
for (var i = 0; i < arr.length; i++) {
|
||||
var item = arr[i]
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
let item = arr[i]
|
||||
if (item.children instanceof Array && item.id === track[layer] && layer < 20) {
|
||||
traverse(item.children, layer+1)
|
||||
i = 1000
|
||||
traverse(item.children, layer+1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -48,8 +48,9 @@ const MenuBar = {
|
||||
setup() {
|
||||
MenuBar.menues = MenuBar.menus;
|
||||
new BarMenu('file', [
|
||||
new MenuSeparator('file_options'),
|
||||
'project_window',
|
||||
'_',
|
||||
new MenuSeparator('open'),
|
||||
{name: 'menu.file.new', id: 'new', icon: 'insert_drive_file',
|
||||
children: function() {
|
||||
let arr = [];
|
||||
@ -67,7 +68,7 @@ const MenuBar = {
|
||||
}
|
||||
})
|
||||
}
|
||||
arr.push('_');
|
||||
arr.push(new MenuSeparator('loaders'));
|
||||
for (let key in ModelLoader.loaders) {
|
||||
let loader = ModelLoader.loaders[key];
|
||||
arr.push({
|
||||
@ -130,12 +131,12 @@ const MenuBar = {
|
||||
'open_model',
|
||||
'open_from_link',
|
||||
'new_window',
|
||||
'_',
|
||||
new MenuSeparator('project'),
|
||||
'save_project',
|
||||
'save_project_as',
|
||||
'convert_project',
|
||||
'close_project',
|
||||
'_',
|
||||
new MenuSeparator('import_export'),
|
||||
{name: 'menu.file.import', id: 'import', icon: 'insert_drive_file', condition: () => Format && !Format.pose_mode, children: [
|
||||
{
|
||||
id: 'import_open_project',
|
||||
@ -185,7 +186,7 @@ const MenuBar = {
|
||||
]},
|
||||
'export_over',
|
||||
'export_asset_archive',
|
||||
'_',
|
||||
new MenuSeparator('options'),
|
||||
{name: 'menu.file.preferences', id: 'preferences', icon: 'tune', children: [
|
||||
'settings_window',
|
||||
'keybindings_window',
|
||||
@ -225,23 +226,24 @@ const MenuBar = {
|
||||
'edit_session'
|
||||
])
|
||||
new BarMenu('edit', [
|
||||
new MenuSeparator('undo'),
|
||||
'undo',
|
||||
'redo',
|
||||
'edit_history',
|
||||
'_',
|
||||
new MenuSeparator('add_element'),
|
||||
'add_cube',
|
||||
'add_mesh',
|
||||
'add_group',
|
||||
'add_locator',
|
||||
'add_null_object',
|
||||
'add_texture_mesh',
|
||||
'_',
|
||||
new MenuSeparator('modify_elements'),
|
||||
'duplicate',
|
||||
'rename',
|
||||
'find_replace',
|
||||
'unlock_everything',
|
||||
'delete',
|
||||
'_',
|
||||
new MenuSeparator('mesh_specific'),
|
||||
{name: 'data.mesh', id: 'mesh', icon: 'fa-gem', children: [
|
||||
'extrude_mesh_selection',
|
||||
'inset_mesh_selection',
|
||||
@ -253,10 +255,10 @@ const MenuBar = {
|
||||
'dissolve_edges',
|
||||
'split_mesh',
|
||||
'merge_meshes',
|
||||
'_',
|
||||
new MenuSeparator('editing_mode'),
|
||||
'proportional_editing',
|
||||
]},
|
||||
'_',
|
||||
new MenuSeparator('selection'),
|
||||
'select_window',
|
||||
'select_all',
|
||||
'unselect_all',
|
||||
@ -304,15 +306,16 @@ const MenuBar = {
|
||||
})
|
||||
|
||||
new BarMenu('texture', [
|
||||
new MenuSeparator('adjustment'),
|
||||
'adjust_brightness_contrast',
|
||||
'adjust_saturation_hue',
|
||||
'adjust_opacity',
|
||||
'invert_colors',
|
||||
'adjust_curves',
|
||||
'_',
|
||||
new MenuSeparator('filters'),
|
||||
'limit_to_palette',
|
||||
'clear_unused_texture_space',
|
||||
'_',
|
||||
new MenuSeparator('transform'),
|
||||
'flip_texture_x',
|
||||
'flip_texture_y',
|
||||
'rotate_texture_cw',
|
||||
@ -323,14 +326,15 @@ const MenuBar = {
|
||||
})
|
||||
|
||||
new BarMenu('animation', [
|
||||
new MenuSeparator('edit_options'),
|
||||
'looped_animation_playback',
|
||||
'lock_motion_trail',
|
||||
'_',
|
||||
new MenuSeparator('edit'),
|
||||
'add_marker',
|
||||
'select_effect_animator',
|
||||
'flip_animation',
|
||||
'bake_animation_into_model',
|
||||
'_',
|
||||
new MenuSeparator('file'),
|
||||
'load_animation_file',
|
||||
'save_all_animations',
|
||||
'export_animation_file'
|
||||
@ -339,9 +343,10 @@ const MenuBar = {
|
||||
})
|
||||
|
||||
new BarMenu('keyframe', [
|
||||
new MenuSeparator('copypaste'),
|
||||
'copy',
|
||||
'paste',
|
||||
'_',
|
||||
new MenuSeparator('edit'),
|
||||
'add_keyframe',
|
||||
'keyframe_column_create',
|
||||
'select_all',
|
||||
@ -369,9 +374,10 @@ const MenuBar = {
|
||||
})
|
||||
|
||||
new BarMenu('display', [
|
||||
new MenuSeparator('copypaste'),
|
||||
'copy',
|
||||
'paste',
|
||||
'_',
|
||||
new MenuSeparator('presets'),
|
||||
'add_display_preset',
|
||||
'apply_display_preset'
|
||||
], {
|
||||
@ -379,6 +385,7 @@ const MenuBar = {
|
||||
})
|
||||
|
||||
new BarMenu('tools', [
|
||||
new MenuSeparator('overview'),
|
||||
{id: 'main_tools', icon: 'construction', name: 'Toolbox', condition: () => Project, children() {
|
||||
let tools = Toolbox.children.filter(tool => tool instanceof Tool && tool.condition !== false);
|
||||
tools.forEach(tool => {
|
||||
@ -402,7 +409,7 @@ const MenuBar = {
|
||||
}},
|
||||
'swap_tools',
|
||||
'action_control',
|
||||
'_',
|
||||
new MenuSeparator('tools'),
|
||||
'predicate_overrides',
|
||||
'convert_to_mesh',
|
||||
'auto_set_cullfaces',
|
||||
@ -412,31 +419,33 @@ const MenuBar = {
|
||||
|
||||
new BarMenu('view', [
|
||||
'fullscreen',
|
||||
'_',
|
||||
new MenuSeparator('viewport'),
|
||||
'view_mode',
|
||||
'toggle_shading',
|
||||
'toggle_motion_trails',
|
||||
'toggle_ground_plane',
|
||||
'preview_checkerboard',
|
||||
'painting_grid',
|
||||
'_',
|
||||
new MenuSeparator('references'),
|
||||
'preview_scene',
|
||||
'edit_reference_images',
|
||||
'_',
|
||||
new MenuSeparator('interface'),
|
||||
'toggle_sidebars',
|
||||
'split_screen',
|
||||
new MenuSeparator('model'),
|
||||
'hide_everything_except_selection',
|
||||
'focus_on_selection',
|
||||
{name: 'menu.view.screenshot', id: 'screenshot', icon: 'camera_alt', children: []},
|
||||
'_',
|
||||
new MenuSeparator('media'),
|
||||
'screenshot_model',
|
||||
'screenshot_app',
|
||||
'record_model_gif',
|
||||
'timelapse',
|
||||
])
|
||||
new BarMenu('help', [
|
||||
new MenuSeparator('search'),
|
||||
{name: 'menu.help.search_action', description: BarItems.action_control.description, keybind: BarItems.action_control.keybind, id: 'search_action', icon: 'search', click: ActionControl.select},
|
||||
'_',
|
||||
new MenuSeparator('links'),
|
||||
{name: 'menu.help.quickstart', id: 'quickstart', icon: 'fas.fa-directions', click: () => {
|
||||
Blockbench.openLink('https://blockbench.net/quickstart/');
|
||||
}},
|
||||
@ -449,9 +458,9 @@ const MenuBar = {
|
||||
{name: 'menu.help.report_issue', id: 'report_issue', icon: 'bug_report', click: () => {
|
||||
Blockbench.openLink('https://github.com/JannisX11/blockbench/issues');
|
||||
}},
|
||||
'_',
|
||||
new MenuSeparator('backups'),
|
||||
'view_backups',
|
||||
'_',
|
||||
new MenuSeparator('about'),
|
||||
{name: 'menu.help.developer', id: 'developer', icon: 'fas.fa-wrench', children: [
|
||||
'reload_plugins',
|
||||
{name: 'menu.help.plugin_documentation', id: 'plugin_documentation', icon: 'fa-book', click: () => {
|
||||
|
@ -424,7 +424,7 @@ const Settings = {
|
||||
preview.camPers.updateProjectionMatrix();
|
||||
});
|
||||
}});
|
||||
new Setting('render_sides', {category: 'preview', value: 'auto', type: 'select', options: {
|
||||
new Setting('render_sides', {category: 'preview', value: 'auto', type: 'select', options: {
|
||||
'auto': tl('settings.render_sides.auto'),
|
||||
'front': tl('settings.render_sides.front'),
|
||||
'double': tl('settings.render_sides.double'),
|
||||
@ -432,46 +432,47 @@ const Settings = {
|
||||
Canvas.updateRenderSides();
|
||||
}});
|
||||
new Setting('background_rendering', {category: 'preview', value: true});
|
||||
new Setting('texture_fps', {category: 'preview', value: 7, type: 'number', min: 0, max: 120, onChange() {
|
||||
new Setting('texture_fps', {category: 'preview', value: 7, type: 'number', min: 0, max: 120, onChange() {
|
||||
TextureAnimator.updateSpeed()
|
||||
}});
|
||||
new Setting('particle_tick_rate',{category: 'preview', value: 30, type: 'number', min: 1, max: 1000, onChange() {
|
||||
new Setting('particle_tick_rate', {category: 'preview', value: 30, type: 'number', min: 1, max: 1000, onChange() {
|
||||
WinterskyScene.global_options.tick_rate = this.value;
|
||||
}});
|
||||
new Setting('volume', {category: 'preview', value: 80, min: 0, max: 200, type: 'number'});
|
||||
new Setting('display_skin', {category: 'preview', value: false, type: 'click', icon: 'icon-player', click: function() { changeDisplaySkin() }});
|
||||
new Setting('volume', {category: 'preview', value: 80, min: 0, max: 200, type: 'number'});
|
||||
new Setting('display_skin', {category: 'preview', value: false, type: 'click', icon: 'icon-player', click: function() { changeDisplaySkin() }});
|
||||
|
||||
//Edit
|
||||
new Setting('undo_limit', {category: 'edit', value: 256, type: 'number', min: 1});
|
||||
new Setting('canvas_unselect', {category: 'edit', value: false});
|
||||
new Setting('highlight_cubes', {category: 'edit', value: true, onChange() {
|
||||
new Setting('undo_limit', {category: 'edit', value: 256, type: 'number', min: 1});
|
||||
new Setting('canvas_unselect', {category: 'edit', value: false});
|
||||
new Setting('double_click_switch_tools',{category: 'edit', value: true});
|
||||
new Setting('highlight_cubes', {category: 'edit', value: true, onChange() {
|
||||
updateCubeHighlights();
|
||||
}});
|
||||
new Setting('allow_display_slot_mirror', {category: 'edit', value: false, onChange(value) {
|
||||
DisplayMode.vue.allow_mirroring = value;
|
||||
}})
|
||||
new Setting('deactivate_size_limit',{category: 'edit', value: false});
|
||||
new Setting('vertex_merge_distance',{category: 'edit', value: 0.1, step: 0.01, type: 'number', min: 0});
|
||||
new Setting('preview_paste_behavior',{category: 'edit', value: 'always_ask', type: 'select', options: {
|
||||
new Setting('deactivate_size_limit', {category: 'edit', value: false});
|
||||
new Setting('vertex_merge_distance', {category: 'edit', value: 0.1, step: 0.01, type: 'number', min: 0});
|
||||
new Setting('preview_paste_behavior', {category: 'edit', value: 'always_ask', type: 'select', options: {
|
||||
'always_ask': tl('settings.preview_paste_behavior.always_ask'),
|
||||
'outliner': tl('menu.paste.outliner'),
|
||||
'face': tl('menu.paste.face'),
|
||||
'mesh_selection': tl('menu.paste.mesh_selection'),
|
||||
}});
|
||||
new Setting('stretch_linked',{category: 'edit', value: true});
|
||||
new Setting('stretch_linked', {category: 'edit', value: true});
|
||||
|
||||
//Grid
|
||||
new Setting('base_grid', {category: 'grid', value: true,});
|
||||
new Setting('large_grid', {category: 'grid', value: true});
|
||||
new Setting('full_grid', {category: 'grid', value: false});
|
||||
new Setting('large_box', {category: 'grid', value: false});
|
||||
new Setting('large_grid_size', {category: 'grid', value: 3, type: 'number', min: 0, max: 2000});
|
||||
new Setting('base_grid', {category: 'grid', value: true,});
|
||||
new Setting('large_grid', {category: 'grid', value: true});
|
||||
new Setting('full_grid', {category: 'grid', value: false});
|
||||
new Setting('large_box', {category: 'grid', value: false});
|
||||
new Setting('large_grid_size', {category: 'grid', value: 3, type: 'number', min: 0, max: 2000});
|
||||
//new Setting('display_grid', {category: 'grid', value: false});
|
||||
new Setting('painting_grid', {category: 'grid', value: true, onChange(value) {
|
||||
new Setting('painting_grid', {category: 'grid', value: true, onChange(value) {
|
||||
Canvas.updatePaintingGrid();
|
||||
UVEditor.vue.pixel_grid = value;
|
||||
}});
|
||||
new Setting('ground_plane', {category: 'grid', value: false, onChange() {
|
||||
new Setting('ground_plane', {category: 'grid', value: false, onChange() {
|
||||
Canvas.ground_plane.visible = this.value;
|
||||
}});
|
||||
|
||||
|
@ -136,3 +136,13 @@ Vue.component('numeric-input', {
|
||||
</div>
|
||||
`
|
||||
})
|
||||
Vue.component('dynamic-icon', {
|
||||
props: {
|
||||
icon: String,
|
||||
color: String,
|
||||
},
|
||||
render(h) {
|
||||
let node = Blockbench.getIconNode(this.icon, this.color);
|
||||
return h(node.tagName, {class: node.className, src: node.attributes.src?.value}, node.textContent);
|
||||
}
|
||||
})
|
||||
|
@ -58,6 +58,9 @@ let codec = new Codec('image', {
|
||||
Project.texture_height = last.display_height;
|
||||
Project.texture_width = last.width;
|
||||
}
|
||||
let pixel_size_limit = Math.min(32 / UVEditor.getPixelSize(), 1);
|
||||
if (pixel_size_limit < 1) UVEditor.setZoom(pixel_size_limit)
|
||||
|
||||
if (isApp) updateRecentProjectThumbnail();
|
||||
}
|
||||
}
|
||||
|
@ -525,13 +525,7 @@ var format = new ModelFormat({
|
||||
}
|
||||
]
|
||||
},
|
||||
render_sides() {
|
||||
if (Modes.display && ['thirdperson_righthand', 'thirdperson_lefthand', 'head'].includes(display_slot)) {
|
||||
return 'double';
|
||||
} else {
|
||||
return 'front';
|
||||
}
|
||||
},
|
||||
render_sides: 'front',
|
||||
model_identifier: false,
|
||||
parent_model_id: true,
|
||||
vertex_color_ambient_occlusion: true,
|
||||
|
@ -1123,7 +1123,44 @@ skin_presets.axolotl = {
|
||||
}
|
||||
]
|
||||
}`
|
||||
}
|
||||
};
|
||||
skin_presets.bamboo_raft = {
|
||||
display_name: 'Bamboo Raft',
|
||||
model: `{
|
||||
"name": "",
|
||||
"texturewidth": 128,
|
||||
"textureheight": 64,
|
||||
"bones": [
|
||||
{
|
||||
"name": "raft",
|
||||
"pivot": [0, 1, -2],
|
||||
"rotation": [90, -90, 0],
|
||||
"cubes": [
|
||||
{"origin": [-14, -11, 1], "size": [28, 20, 4], "uv": [0, 0]},
|
||||
{"origin": [-14, -9, -3], "size": [28, 16, 4], "uv": [0, 0]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "paddle_left",
|
||||
"pivot": [-11.5, 12, 1],
|
||||
"rotation": [-50, -75, 0],
|
||||
"cubes": [
|
||||
{"origin": [-12.5, 11, -4.5], "size": [2, 2, 18], "uv": [0, 24]},
|
||||
{"origin": [-12.51, 10, 8.5], "size": [1, 6, 7], "uv": [0, 24]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "paddle_right",
|
||||
"pivot": [7.5, 12, 0],
|
||||
"rotation": [-50, 75, 0],
|
||||
"cubes": [
|
||||
{"origin": [5.5, 11, -5.5], "size": [2, 2, 18], "uv": [40, 24]},
|
||||
{"origin": [6.51, 10, 7.5], "size": [1, 6, 7], "uv": [40, 24]}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`
|
||||
};
|
||||
skin_presets.bat = {
|
||||
display_name: 'Bat',
|
||||
pose: true,
|
||||
@ -1555,6 +1592,138 @@ skin_presets.boat = {
|
||||
]
|
||||
}`
|
||||
};
|
||||
skin_presets.camel = {
|
||||
display_name: 'Camel',
|
||||
model: `{
|
||||
"name": "camel",
|
||||
"texturewidth": 128,
|
||||
"textureheight": 128,
|
||||
"eyes": [
|
||||
[26, 8, 3, 1],
|
||||
[34, 8, 3, 1]
|
||||
],
|
||||
"bones": [
|
||||
{
|
||||
"name": "root",
|
||||
"pivot": [0, 0, 0]
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"parent": "root",
|
||||
"pivot": [0.5, 20, 9.5],
|
||||
"cubes": [
|
||||
{"origin": [-7.5, 20, -14], "size": [15, 12, 27], "uv": [0, 25]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "saddle",
|
||||
"parent": "body",
|
||||
"pivot": [0.5, 20, 9.5],
|
||||
"cubes": [
|
||||
{"origin": [-4.5, 32, -6], "size": [9, 5, 11], "inflate": 0.1, "layer": true, "visibility": false, "uv": [74, 64]},
|
||||
{"origin": [-3.5, 37, -6], "size": [7, 3, 11], "inflate": 0.1, "layer": true, "visibility": false, "uv": [92, 114]},
|
||||
{"origin": [-7.5, 20, -14], "size": [15, 12, 27], "inflate": 0.1, "layer": true, "visibility": false, "uv": [0, 89]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "tail",
|
||||
"parent": "body",
|
||||
"pivot": [0, 29, 13],
|
||||
"cubes": [
|
||||
{"origin": [-1.5, 15, 13], "size": [3, 14, 0], "pivot": [0, 29, 13], "rotation": [0, 180, 0], "uv": [122, 0]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "head",
|
||||
"parent": "body",
|
||||
"pivot": [0.5, 25, -10],
|
||||
"cubes": [
|
||||
{"origin": [-3.5, 22, -25], "size": [7, 8, 19], "uv": [60, 24]},
|
||||
{"origin": [-3.5, 30, -25], "size": [7, 14, 7], "uv": [21, 0]},
|
||||
{"origin": [-2.5, 39, -31], "size": [5, 5, 6], "uv": [50, 0]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "bridle",
|
||||
"parent": "head",
|
||||
"pivot": [0.5, 25, -10],
|
||||
"cubes": [
|
||||
{"origin": [-3.5, 22, -25], "size": [7, 8, 19], "inflate": 0.1, "uv": [60, 87], "layer": true, "visibility": false},
|
||||
{"origin": [-3.5, 30, -25], "size": [7, 14, 7], "inflate": 0.1, "uv": [21, 64], "layer": true, "visibility": false},
|
||||
{"origin": [-2.5, 39, -31.1], "size": [5, 5, 6], "inflate": 0.1, "uv": [50, 64], "layer": true, "visibility": false},
|
||||
{"origin": [2.5, 40, -28], "size": [1, 2, 2], "uv": [74, 70]},
|
||||
{"origin": [-3.5, 40, -28], "size": [1, 2, 2], "uv": [74, 70], "mirror": true}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "left_ear",
|
||||
"parent": "head",
|
||||
"pivot": [3, 43, -19.5],
|
||||
"cubes": [
|
||||
{"origin": [3, 42.5, -20.5], "size": [3, 1, 2], "uv": [45, 0]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "right_ear",
|
||||
"parent": "head",
|
||||
"pivot": [-3, 43, -19.5],
|
||||
"cubes": [
|
||||
{"origin": [-6, 42.5, -20.5], "size": [3, 1, 2], "uv": [67, 0]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "reins",
|
||||
"parent": "head",
|
||||
"pivot": [3.7, 41, -27],
|
||||
"cubes": [
|
||||
{"origin": [3.7, 34, -27], "size": [0, 7, 15], "uv": [98, 42], "layer": true, "visibility": false},
|
||||
{"origin": [-3.7, 34, -12], "size": [7.4, 7, 0], "uv": [84, 57], "layer": true, "visibility": false},
|
||||
{"origin": [-3.7, 34, -27], "size": [0, 7, 15], "uv": [98, 42], "layer": true, "visibility": false}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "hump",
|
||||
"parent": "body",
|
||||
"pivot": [0.5, 32, 0],
|
||||
"cubes": [
|
||||
{"origin": [-4.5, 32, -6], "size": [9, 5, 11], "uv": [74, 0]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "right_front_leg",
|
||||
"parent": "root",
|
||||
"pivot": [-4.9, 23, -10.5],
|
||||
"cubes": [
|
||||
{"origin": [-7.4, 0, -13], "size": [5, 21, 5], "uv": [0, 26]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "left_front_leg",
|
||||
"parent": "root",
|
||||
"pivot": [4.9, 23, -10.5],
|
||||
"cubes": [
|
||||
{"origin": [2.4, 0, -13], "size": [5, 21, 5], "uv": [0, 0]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "left_hind_leg",
|
||||
"parent": "root",
|
||||
"pivot": [4.9, 23, 9.5],
|
||||
"cubes": [
|
||||
{"origin": [2.4, 0, 7], "size": [5, 21, 5], "uv": [58, 16]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "right_hind_leg",
|
||||
"parent": "root",
|
||||
"pivot": [-4.9, 23, 9.5],
|
||||
"cubes": [
|
||||
{"origin": [-7.4, 0, 7], "size": [5, 21, 5], "uv": [94, 16]}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`
|
||||
}
|
||||
skin_presets.cat = {
|
||||
display_name: 'Cat',
|
||||
model: `{
|
||||
@ -5095,6 +5264,123 @@ skin_presets.slime = {
|
||||
]
|
||||
}`
|
||||
};
|
||||
skin_presets.sniffer = {
|
||||
display_name: 'Sniffer',
|
||||
model: `{
|
||||
"name": "sniffer",
|
||||
"texturewidth": 192,
|
||||
"textureheight": 192,
|
||||
"eyes": [
|
||||
[13, 31, 4, 1],
|
||||
[34, 31, 4, 1]
|
||||
],
|
||||
"bones": [
|
||||
{
|
||||
"name": "bone",
|
||||
"pivot": [0, 19, 0]
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"parent": "bone",
|
||||
"pivot": [0, 0, 0],
|
||||
"cubes": [
|
||||
{"origin": [-12.5, 9, -20], "size": [25, 24, 40], "inflate": 0.5, "uv": [62, 0]},
|
||||
{"origin": [-12.5, 4, -20], "size": [25, 29, 40], "uv": [62, 68]},
|
||||
{"origin": [-12.5, 8, -20], "size": [25, 0, 40], "uv": [87, 68]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "head",
|
||||
"parent": "body",
|
||||
"pivot": [0, 13.5, -19.4],
|
||||
"cubes": [
|
||||
{"origin": [-6.5, 3, -30.9], "size": [13, 18, 11], "uv": [8, 15]},
|
||||
{"origin": [-6.5, 6, -30.9], "size": [13, 0, 11], "uv": [8, 4]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "left_ear",
|
||||
"parent": "head",
|
||||
"pivot": [6.4, 21, -23.9],
|
||||
"cubes": [
|
||||
{"origin": [6.4, 2, -26.9], "size": [1, 19, 7], "uv": [2, 0]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "right_ear",
|
||||
"parent": "head",
|
||||
"pivot": [-6.4, 21, -23.9],
|
||||
"cubes": [
|
||||
{"origin": [-7.4, 2, -26.9], "size": [1, 19, 7], "uv": [48, 0]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "nose",
|
||||
"parent": "head",
|
||||
"pivot": [0, 18, -30.9],
|
||||
"cubes": [
|
||||
{"origin": [-6.5, 18, -39.9], "size": [13, 2, 9], "uv": [10, 45]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "lower_beak",
|
||||
"parent": "head",
|
||||
"pivot": [0, 11, -31.9],
|
||||
"cubes": [
|
||||
{"origin": [-6.5, 6, -39.9], "size": [13, 12, 9], "uv": [10, 57]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "right_front_leg",
|
||||
"parent": "bone",
|
||||
"pivot": [-7.5, 9, -15],
|
||||
"cubes": [
|
||||
{"origin": [-11, 0, -19], "size": [7, 10, 8], "uv": [32, 87]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "right_mid_leg",
|
||||
"parent": "bone",
|
||||
"pivot": [-7.5, 9, 0],
|
||||
"cubes": [
|
||||
{"origin": [-11, 0, -4], "size": [7, 10, 8], "uv": [32, 105]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "right_hind_leg",
|
||||
"parent": "bone",
|
||||
"pivot": [-7.5, 9, 15],
|
||||
"cubes": [
|
||||
{"origin": [-11, 0, 11], "size": [7, 10, 8], "uv": [32, 123]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "left_front_leg",
|
||||
"parent": "bone",
|
||||
"pivot": [7.5, 9, -15],
|
||||
"cubes": [
|
||||
{"origin": [4, 0, -19], "size": [7, 10, 8], "uv": [0, 87]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "left_mid_leg",
|
||||
"parent": "bone",
|
||||
"pivot": [7.5, 9, 0],
|
||||
"cubes": [
|
||||
{"origin": [4, 0, -4], "size": [7, 10, 8], "uv": [0, 105]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "left_hind_leg",
|
||||
"parent": "bone",
|
||||
"pivot": [7.5, 9, 15],
|
||||
"cubes": [
|
||||
{"origin": [4, 0, 11], "size": [7, 10, 8], "uv": [0, 123]}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`
|
||||
}
|
||||
skin_presets.snowgolem = {
|
||||
display_name: 'Snowgolem',
|
||||
model: `{
|
||||
|
@ -1086,9 +1086,11 @@ BARS.defineActions(function() {
|
||||
title: 'dialog.convert_project.title',
|
||||
width: 540,
|
||||
form: {
|
||||
text: {type: 'info', text: 'dialog.convert_project.text'},
|
||||
current: {type: 'info', label: 'dialog.convert_project.current_format', text: Format.name || '-'},
|
||||
format: {
|
||||
text1: {type: 'info', text: 'dialog.convert_project.text1'},
|
||||
text2: {type: 'info', text: 'dialog.convert_project.text2'},
|
||||
text3: {type: 'info', text: 'dialog.convert_project.text3'},
|
||||
current: {type: 'info', label: 'dialog.convert_project.current_format', text: Format.name || '-'},
|
||||
format: {
|
||||
label: 'data.format',
|
||||
type: 'select',
|
||||
options,
|
||||
|
@ -197,6 +197,7 @@ BARS.defineActions(function() {
|
||||
new Dialog({
|
||||
id: 'share_model_link',
|
||||
title: 'dialog.share_model.title',
|
||||
singleButton: true,
|
||||
form: {
|
||||
link: {type: 'text', value: link, readonly: true, share_text: true}
|
||||
}
|
||||
|
@ -438,7 +438,7 @@ BARS.defineActions(function() {
|
||||
}
|
||||
})
|
||||
}
|
||||
if (value == 'face' && ['edge', 'vertex'].includes(previous_selection_mode)) {
|
||||
if ((value == 'face' || value == 'cluster') && ['edge', 'vertex'].includes(previous_selection_mode)) {
|
||||
Mesh.selected.forEach(mesh => {
|
||||
let vertices = mesh.getSelectedVertices();
|
||||
let faces = mesh.getSelectedFaces(true);
|
||||
@ -1205,9 +1205,9 @@ BARS.defineActions(function() {
|
||||
}
|
||||
let length = getLength();
|
||||
|
||||
function runEdit(amended, offset, direction = 0) {
|
||||
function runEdit(amended, offset, direction = 0, cuts = 1) {
|
||||
Undo.initEdit({elements: Mesh.selected, selection: true}, amended);
|
||||
if (offset == undefined) offset = length / 2;
|
||||
if (offset == undefined) offset = length / (cuts+1);
|
||||
Mesh.selected.forEach(mesh => {
|
||||
let selected_vertices = mesh.getSelectedVertices();
|
||||
let start_face;
|
||||
@ -1225,19 +1225,18 @@ BARS.defineActions(function() {
|
||||
let processed_faces = [start_face];
|
||||
let center_vertices_map = {};
|
||||
|
||||
function getCenterVertex(vertices) {
|
||||
function getCenterVertex(vertices, ratio) {
|
||||
let edge_key = vertices.slice().sort().join('.');
|
||||
let existing_key = center_vertices_map[edge_key];
|
||||
if (existing_key) return existing_key;
|
||||
|
||||
let ratio = offset/length;
|
||||
let vector = mesh.vertices[vertices[0]].map((v, i) => Math.lerp(v, mesh.vertices[vertices[1]][i], ratio))
|
||||
let [vkey] = mesh.addVertices(vector);
|
||||
center_vertices_map[edge_key] = vkey;
|
||||
return vkey;
|
||||
}
|
||||
|
||||
function splitFace(face, side_vertices, double_side) {
|
||||
function splitFace(face, side_vertices, double_side, cut_no) {
|
||||
processed_faces.push(face);
|
||||
let sorted_vertices = face.getSortedVertices();
|
||||
|
||||
@ -1250,18 +1249,22 @@ BARS.defineActions(function() {
|
||||
let opposite_index_diff = sorted_vertices.indexOf(opposite_vertices[0]) - sorted_vertices.indexOf(opposite_vertices[1]);
|
||||
if (opposite_index_diff == 1 || opposite_index_diff < -2) opposite_vertices.reverse();
|
||||
|
||||
let ratio = offset/length;
|
||||
if (cuts > 1) {
|
||||
ratio = 1 - (1 / (cuts + 1 - cut_no) * ratio * 2);
|
||||
}
|
||||
let center_vertices = [
|
||||
getCenterVertex(side_vertices),
|
||||
getCenterVertex(opposite_vertices)
|
||||
getCenterVertex(side_vertices, ratio),
|
||||
getCenterVertex(opposite_vertices, ratio)
|
||||
]
|
||||
|
||||
let c1_uv_coords = [
|
||||
Math.lerp(face.uv[side_vertices[0]][0], face.uv[side_vertices[1]][0], offset/length),
|
||||
Math.lerp(face.uv[side_vertices[0]][1], face.uv[side_vertices[1]][1], offset/length),
|
||||
Math.lerp(face.uv[side_vertices[0]][0], face.uv[side_vertices[1]][0], ratio),
|
||||
Math.lerp(face.uv[side_vertices[0]][1], face.uv[side_vertices[1]][1], ratio),
|
||||
];
|
||||
let c2_uv_coords = [
|
||||
Math.lerp(face.uv[opposite_vertices[0]][0], face.uv[opposite_vertices[1]][0], offset/length),
|
||||
Math.lerp(face.uv[opposite_vertices[0]][1], face.uv[opposite_vertices[1]][1], offset/length),
|
||||
Math.lerp(face.uv[opposite_vertices[0]][0], face.uv[opposite_vertices[1]][0], ratio),
|
||||
Math.lerp(face.uv[opposite_vertices[0]][1], face.uv[opposite_vertices[1]][1], ratio),
|
||||
];
|
||||
|
||||
let new_face = new MeshFace(mesh, face).extend({
|
||||
@ -1284,13 +1287,19 @@ BARS.defineActions(function() {
|
||||
})
|
||||
mesh.addFaces(new_face);
|
||||
|
||||
// Multiple loop cuts
|
||||
if (cut_no+1 < cuts) {
|
||||
splitFace(face, [center_vertices[0], side_vertices[0]], double_side, cut_no+1);
|
||||
}
|
||||
|
||||
if (cut_no != 0) return;
|
||||
// Find next (and previous) face
|
||||
for (let fkey in mesh.faces) {
|
||||
let ref_face = mesh.faces[fkey];
|
||||
if (ref_face.vertices.length < 3 || processed_faces.includes(ref_face)) continue;
|
||||
let vertices = ref_face.vertices.filter(vkey => opposite_vertices.includes(vkey))
|
||||
if (vertices.length >= 2) {
|
||||
splitFace(ref_face, opposite_vertices, ref_face.vertices.length == 4);
|
||||
splitFace(ref_face, opposite_vertices, ref_face.vertices.length == 4, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -1306,7 +1315,7 @@ BARS.defineActions(function() {
|
||||
|
||||
if(ref_opposite_vertices.length == 2)
|
||||
{
|
||||
splitFace(ref_face, ref_opposite_vertices, ref_face.vertices.length == 4);
|
||||
splitFace(ref_face, ref_opposite_vertices, ref_face.vertices.length == 4, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -1323,18 +1332,22 @@ BARS.defineActions(function() {
|
||||
let opposite_index_diff = sorted_vertices.indexOf(opposite_vertices[0]) - sorted_vertices.indexOf(opposite_vertices[1]);
|
||||
if (opposite_index_diff == 1 || opposite_index_diff < -2) opposite_vertices.reverse();
|
||||
|
||||
let ratio = offset/length;
|
||||
if (cuts > 1) {
|
||||
ratio = 1 - (1 / (cuts + 1 - cut_no) * ratio * 2);
|
||||
}
|
||||
let center_vertices = [
|
||||
getCenterVertex(side_vertices),
|
||||
getCenterVertex(opposite_vertices)
|
||||
getCenterVertex(side_vertices, ratio),
|
||||
getCenterVertex(opposite_vertices, ratio)
|
||||
]
|
||||
|
||||
let c1_uv_coords = [
|
||||
Math.lerp(face.uv[side_vertices[0]][0], face.uv[side_vertices[1]][0], offset/length),
|
||||
Math.lerp(face.uv[side_vertices[0]][1], face.uv[side_vertices[1]][1], offset/length),
|
||||
Math.lerp(face.uv[side_vertices[0]][0], face.uv[side_vertices[1]][0], ratio),
|
||||
Math.lerp(face.uv[side_vertices[0]][1], face.uv[side_vertices[1]][1], ratio),
|
||||
];
|
||||
let c2_uv_coords = [
|
||||
Math.lerp(face.uv[opposite_vertices[0]][0], face.uv[opposite_vertices[1]][0], offset/length),
|
||||
Math.lerp(face.uv[opposite_vertices[0]][1], face.uv[opposite_vertices[1]][1], offset/length),
|
||||
Math.lerp(face.uv[opposite_vertices[0]][0], face.uv[opposite_vertices[1]][0], ratio),
|
||||
Math.lerp(face.uv[opposite_vertices[0]][1], face.uv[opposite_vertices[1]][1], ratio),
|
||||
];
|
||||
|
||||
let other_quad_vertex = side_vertices.find(vkey => !opposite_vertices.includes(vkey));
|
||||
@ -1364,13 +1377,19 @@ BARS.defineActions(function() {
|
||||
}
|
||||
mesh.addFaces(new_face);
|
||||
|
||||
// Multiple loop cuts
|
||||
if (cut_no+1 < cuts) {
|
||||
splitFace(face, [center_vertices[0], other_quad_vertex], double_side, cut_no+1);
|
||||
}
|
||||
|
||||
if (cut_no != 0) return;
|
||||
// Find next (and previous) face
|
||||
for (let fkey in mesh.faces) {
|
||||
let ref_face = mesh.faces[fkey];
|
||||
if (ref_face.vertices.length < 3 || processed_faces.includes(ref_face)) continue;
|
||||
let vertices = ref_face.vertices.filter(vkey => opposite_vertices.includes(vkey))
|
||||
if (vertices.length >= 2) {
|
||||
splitFace(ref_face, opposite_vertices, ref_face.vertices.length == 4);
|
||||
splitFace(ref_face, opposite_vertices, ref_face.vertices.length == 4, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -1385,7 +1404,7 @@ BARS.defineActions(function() {
|
||||
let ref_opposite_vertices = ref_sorted_vertices.filter(vkey => !side_vertices.includes(vkey));
|
||||
|
||||
if (ref_opposite_vertices.length == 2) {
|
||||
splitFace(ref_face, ref_opposite_vertices, ref_face.vertices.length == 4);
|
||||
splitFace(ref_face, ref_opposite_vertices, ref_face.vertices.length == 4, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -1394,11 +1413,15 @@ BARS.defineActions(function() {
|
||||
} else {
|
||||
let opposite_vertex = sorted_vertices.find(vkey => !side_vertices.includes(vkey));
|
||||
|
||||
let center_vertex = getCenterVertex(side_vertices);
|
||||
let ratio = offset/length;
|
||||
if (cuts > 1) {
|
||||
ratio = 1 - (1 / (cuts + 1 - cut_no) * ratio * 2);
|
||||
}
|
||||
let center_vertex = getCenterVertex(side_vertices, ratio);
|
||||
|
||||
let c1_uv_coords = [
|
||||
Math.lerp(face.uv[side_vertices[0]][0], face.uv[side_vertices[1]][0], offset/length),
|
||||
Math.lerp(face.uv[side_vertices[0]][1], face.uv[side_vertices[1]][1], offset/length),
|
||||
Math.lerp(face.uv[side_vertices[0]][0], face.uv[side_vertices[1]][0], ratio),
|
||||
Math.lerp(face.uv[side_vertices[0]][1], face.uv[side_vertices[1]][1], ratio),
|
||||
];
|
||||
|
||||
let new_face = new MeshFace(mesh, face).extend({
|
||||
@ -1425,11 +1448,15 @@ BARS.defineActions(function() {
|
||||
}
|
||||
} else if (face.vertices.length == 2) {
|
||||
|
||||
let center_vertex = getCenterVertex(side_vertices);
|
||||
let ratio = offset/length;
|
||||
if (cuts > 1) {
|
||||
ratio = 1 - (1 / (cuts + 1 - cut_no) * ratio * 2);
|
||||
}
|
||||
let center_vertex = getCenterVertex(side_vertices, ratio);
|
||||
|
||||
let c1_uv_coords = [
|
||||
Math.lerp(face.uv[side_vertices[0]][0], face.uv[side_vertices[1]][0], offset/length),
|
||||
Math.lerp(face.uv[side_vertices[0]][1], face.uv[side_vertices[1]][1], offset/length),
|
||||
Math.lerp(face.uv[side_vertices[0]][0], face.uv[side_vertices[1]][0], ratio),
|
||||
Math.lerp(face.uv[side_vertices[0]][1], face.uv[side_vertices[1]][1], ratio),
|
||||
];
|
||||
|
||||
let new_face = new MeshFace(mesh, face).extend({
|
||||
@ -1454,7 +1481,7 @@ BARS.defineActions(function() {
|
||||
let start_edge = [start_vertices[direction % start_vertices.length], start_vertices[(direction+1) % start_vertices.length]];
|
||||
if (start_edge.length == 1) start_edge.splice(0, 0, start_vertices[0]);
|
||||
|
||||
splitFace(start_face, start_edge, start_face.vertices.length == 4 || direction > 2);
|
||||
splitFace(start_face, start_edge, start_face.vertices.length == 4 || direction > 2, 0);
|
||||
|
||||
selected_vertices.empty();
|
||||
for (let key in center_vertices_map) {
|
||||
@ -1469,7 +1496,7 @@ BARS.defineActions(function() {
|
||||
|
||||
Undo.amendEdit({
|
||||
direction: {type: 'number', value: 0, label: 'edit.loop_cut.direction', condition: !!selected_face, min: 0},
|
||||
//cuts: {type: 'number', value: 1, label: 'edit.loop_cut.cuts', min: 0, max: 16},
|
||||
cuts: {type: 'number', value: 1, label: 'edit.loop_cut.cuts', min: 0, max: 16},
|
||||
offset: {type: 'number', value: length/2, label: 'edit.loop_cut.offset', min: 0, max: length, interval_type: 'position'},
|
||||
}, (form, form_options) => {
|
||||
let direction = form.direction || 0;
|
||||
@ -1483,7 +1510,7 @@ BARS.defineActions(function() {
|
||||
saved_direction = direction;
|
||||
}
|
||||
|
||||
runEdit(true, form_options.offset.slider.value, form_options.direction ? direction : 0);
|
||||
runEdit(true, form_options.offset.slider.value, form_options.direction ? direction : 0, form.cuts);
|
||||
})
|
||||
}
|
||||
})
|
||||
|
279
js/modeling/mirror_modeling.js
Normal file
279
js/modeling/mirror_modeling.js
Normal file
@ -0,0 +1,279 @@
|
||||
const MirrorModeling = {
|
||||
isCentered(element) {
|
||||
if (element.origin[0] != 0) return false;
|
||||
if (element.rotation[1] || element.rotation[2]) return false;
|
||||
if (element instanceof Cube && !Math.epsilon(element.to[0], -element.from[0], 0.01)) return false;
|
||||
|
||||
let checkParent = (parent) => {
|
||||
if (parent instanceof Group) {
|
||||
if (parent.origin[0] != 0) return true;
|
||||
if (parent.rotation[1] || parent.rotation[2]) return true;
|
||||
return checkParent(parent.parent);
|
||||
}
|
||||
}
|
||||
if (checkParent(element.parent)) return false;
|
||||
return true;
|
||||
},
|
||||
createClone(original, undo_aspects) {
|
||||
// Create or update clone
|
||||
var center = Format.centered_grid ? 0 : 8;
|
||||
let mirror_element = MirrorModeling.cached_elements[original.uuid]?.counterpart;
|
||||
let element_before_snapshot;
|
||||
|
||||
if (mirror_element && mirror_element !== original) {
|
||||
element_before_snapshot = mirror_element.getUndoCopy(undo_aspects);
|
||||
mirror_element.extend(original);
|
||||
|
||||
} else {
|
||||
function getParentMirror(child) {
|
||||
let parent = child.parent;
|
||||
if (parent instanceof Group == false) return 'root';
|
||||
|
||||
if (parent.origin[0] == center) {
|
||||
return parent;
|
||||
} else {
|
||||
let mirror_group_parent = getParentMirror(parent);
|
||||
let mirror_group = new Group(parent);
|
||||
|
||||
flipNameOnAxis(mirror_group, 0, name => true, parent.name);
|
||||
mirror_group.origin[0] *= -1;
|
||||
mirror_group.rotation[1] *= -1;
|
||||
mirror_group.rotation[2] *= -1;
|
||||
mirror_group.isOpen = parent.isOpen;
|
||||
|
||||
let parent_list = mirror_group_parent instanceof Group ? mirror_group_parent.children : Outliner.root;
|
||||
let match = parent_list.find(node => {
|
||||
if (node instanceof Group == false) return false;
|
||||
if (node.name == mirror_group.name && node.rotation.equals(mirror_group.rotation) && node.origin.equals(mirror_group.origin)) {
|
||||
return true;
|
||||
}
|
||||
})
|
||||
if (match) {
|
||||
return match;
|
||||
} else {
|
||||
mirror_group.createUniqueName();
|
||||
mirror_group.addTo(mirror_group_parent).init();
|
||||
return mirror_group;
|
||||
}
|
||||
}
|
||||
}
|
||||
let add_to = getParentMirror(original);
|
||||
mirror_element = new original.constructor(original).addTo(add_to).init();
|
||||
}
|
||||
mirror_element.flip(0, center);
|
||||
|
||||
MirrorModeling.insertElementIntoUndo(mirror_element, undo_aspects, element_before_snapshot);
|
||||
|
||||
let {preview_controller} = mirror_element;
|
||||
preview_controller.updateTransform(mirror_element);
|
||||
preview_controller.updateGeometry(mirror_element);
|
||||
preview_controller.updateFaces(mirror_element);
|
||||
preview_controller.updateUV(mirror_element);
|
||||
},
|
||||
createLocalSymmetry(mesh) {
|
||||
// Create or update clone
|
||||
let edit_side = Math.sign(Transformer.position.x) || 1;
|
||||
// Delete all vertices on the non-edit side
|
||||
let deleted_vertices = {};
|
||||
let selected_vertices = mesh.getSelectedVertices(true);
|
||||
//let selected_vertices = mesh.getSelectedEdges(true);
|
||||
let selected_faces = mesh.getSelectedFaces(true);
|
||||
let deleted_vertices_by_position = {};
|
||||
function positionKey(position) {
|
||||
return position.map(p => Math.roundTo(p, 2)).join(',');
|
||||
}
|
||||
for (let vkey in mesh.vertices) {
|
||||
if (mesh.vertices[vkey][0] && mesh.vertices[vkey][0] * edit_side < 0) {
|
||||
deleted_vertices[vkey] = mesh.vertices[vkey];
|
||||
delete mesh.vertices[vkey];
|
||||
deleted_vertices_by_position[positionKey(deleted_vertices[vkey])] = vkey;
|
||||
}
|
||||
}
|
||||
// Copy existing vertices back to the non-edit side
|
||||
let added_vertices = [];
|
||||
let vertex_counterpart = {};
|
||||
let replaced_vertices = {};
|
||||
for (let vkey in mesh.vertices) {
|
||||
let vertex = mesh.vertices[vkey];
|
||||
if (mesh.vertices[vkey][0] == 0) {
|
||||
// On Edge
|
||||
vertex_counterpart[vkey] = vkey;
|
||||
} else {
|
||||
let position = [-vertex[0], vertex[1], vertex[2]];
|
||||
let vkey_new = deleted_vertices_by_position[positionKey(position)];
|
||||
if (vkey_new) {
|
||||
mesh.vertices[vkey_new] = position;
|
||||
} else {
|
||||
vkey_new = mesh.addVertices(position)[0];
|
||||
}
|
||||
added_vertices.push(vkey_new);
|
||||
vertex_counterpart[vkey] = vkey_new;
|
||||
}
|
||||
}
|
||||
|
||||
let deleted_faces = {};
|
||||
for (let fkey in mesh.faces) {
|
||||
let face = mesh.faces[fkey];
|
||||
let deleted_face_vertices = face.vertices.filter(vkey => deleted_vertices[vkey]);
|
||||
if (deleted_face_vertices.length == face.vertices.length) {
|
||||
deleted_faces[fkey] = mesh.faces[fkey];
|
||||
delete mesh.faces[fkey];
|
||||
}
|
||||
}
|
||||
|
||||
for (let fkey in mesh.faces) {
|
||||
let face = mesh.faces[fkey];
|
||||
let deleted_face_vertices = face.vertices.filter(vkey => deleted_vertices[vkey]);
|
||||
if (deleted_face_vertices.length && face.vertices.length != deleted_face_vertices.length*2) {
|
||||
// cannot flip. restore vertices instead?
|
||||
deleted_face_vertices.forEach(vkey => {
|
||||
mesh.vertices[vkey] = deleted_vertices[vkey];
|
||||
//delete deleted_vertices[vkey];
|
||||
})
|
||||
|
||||
} else if (deleted_face_vertices.length) {
|
||||
// face across zero line
|
||||
//let kept_face_keys = face.vertices.filter(vkey => mesh.vertices[vkey] != 0 && !deleted_face_vertices.includes(vkey));
|
||||
let new_counterparts = face.vertices.filter(vkey => !deleted_vertices[vkey]).map(vkey => vertex_counterpart[vkey]);
|
||||
face.vertices.forEach((vkey, i) => {
|
||||
if (deleted_face_vertices.includes(vkey)) {
|
||||
// Across
|
||||
//let kept_key = kept_face_keys[i%kept_face_keys.length];
|
||||
new_counterparts.sort((a, b) => {
|
||||
let a_distance = Math.pow(mesh.vertices[a][1] - deleted_vertices[vkey][1], 2) + Math.pow(mesh.vertices[a][2] - deleted_vertices[vkey][2], 2);
|
||||
let b_distance = Math.pow(mesh.vertices[b][1] - deleted_vertices[vkey][1], 2) + Math.pow(mesh.vertices[b][2] - deleted_vertices[vkey][2], 2);
|
||||
return b_distance - a_distance;
|
||||
})
|
||||
|
||||
let counterpart = new_counterparts.pop();
|
||||
if (vkey != counterpart && counterpart) {
|
||||
face.vertices.splice(i, 1, counterpart);
|
||||
face.uv[counterpart] = face.uv[vkey];
|
||||
delete face.uv[vkey];
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
} else if (deleted_face_vertices.length == 0) {
|
||||
// Recreate face as mirrored
|
||||
let new_face_key;
|
||||
for (let key in deleted_faces) {
|
||||
let deleted_face = deleted_faces[key];
|
||||
if (face.vertices.allAre(vkey => deleted_face.vertices.includes(vertex_counterpart[vkey]))) {
|
||||
new_face_key = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let new_face = new MeshFace(mesh, face);
|
||||
face.vertices.forEach((vkey, i) => {
|
||||
let new_vkey = vertex_counterpart[vkey];
|
||||
new_face.vertices.splice(i, 1, new_vkey);
|
||||
delete new_face.uv[vkey];
|
||||
new_face.uv[new_vkey] = face.uv[vkey];
|
||||
})
|
||||
new_face.invert();
|
||||
if (new_face_key) {
|
||||
mesh.faces[new_face_key] = new_face;
|
||||
} else {
|
||||
[new_face_key] = mesh.addFaces(new_face);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
let {preview_controller} = mesh;
|
||||
preview_controller.updateGeometry(mesh);
|
||||
preview_controller.updateFaces(mesh);
|
||||
preview_controller.updateUV(mesh);
|
||||
},
|
||||
insertElementIntoUndo(element, undo_aspects, element_before_snapshot) {
|
||||
// pre
|
||||
if (element_before_snapshot) {
|
||||
if (!Undo.current_save.elements[element.uuid]) Undo.current_save.elements[element.uuid] = element_before_snapshot;
|
||||
} else {
|
||||
if (!Undo.current_save.outliner) Undo.current_save.outliner = MirrorModeling.outliner_snapshot;
|
||||
}
|
||||
|
||||
// post
|
||||
if (!element_before_snapshot) undo_aspects.outliner = true;
|
||||
undo_aspects.elements.safePush(element);
|
||||
},
|
||||
cached_elements: {}
|
||||
}
|
||||
|
||||
Blockbench.on('init_edit', ({aspects}) => {
|
||||
if (!BarItems.mirror_modeling.value) return;
|
||||
if (!aspects.elements) return;
|
||||
|
||||
MirrorModeling.cached_elements = {};
|
||||
MirrorModeling.outliner_snapshot = aspects.outliner ? null : compileGroups(true);
|
||||
|
||||
aspects.elements.forEach((element) => {
|
||||
if (element.allow_mirror_modeling) {
|
||||
let is_centered = MirrorModeling.isCentered(element);
|
||||
|
||||
MirrorModeling.cached_elements[element.uuid] = {is_centered};
|
||||
if (!is_centered) {
|
||||
MirrorModeling.cached_elements[element.uuid].counterpart = Painter.getMirrorElement(element, [1, 0, 0]);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
setTimeout(() => {MirrorModeling.cached_elements = {}}, 10_000);
|
||||
})
|
||||
Blockbench.on('finish_edit', ({aspects}) => {
|
||||
if (!BarItems.mirror_modeling.value) return;
|
||||
if (!aspects.elements) return;
|
||||
|
||||
aspects.elements = aspects.elements.slice();
|
||||
let static_elements_copy = aspects.elements.slice();
|
||||
static_elements_copy.forEach((element) => {
|
||||
if (element.allow_mirror_modeling) {
|
||||
let is_centered = MirrorModeling.isCentered(element);
|
||||
|
||||
if (is_centered && element instanceof Mesh) {
|
||||
// Complete other side of mesh
|
||||
MirrorModeling.createLocalSymmetry(element);
|
||||
}
|
||||
if (!is_centered) {
|
||||
// Construct clone at other side of model
|
||||
MirrorModeling.createClone(element, aspects);
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Element property on cube and mesh
|
||||
new Property(Cube, 'boolean', 'allow_mirror_modeling', {default: true});
|
||||
new Property(Mesh, 'boolean', 'allow_mirror_modeling', {default: true});
|
||||
|
||||
BARS.defineActions(() => {
|
||||
|
||||
new Toggle('mirror_modeling', {
|
||||
icon: 'align_horizontal_center',
|
||||
category: 'edit',
|
||||
condition: {modes: ['edit']},
|
||||
onChange() {
|
||||
updateSelection();
|
||||
}
|
||||
})
|
||||
let allow_toggle = new Toggle('allow_element_mirror_modeling', {
|
||||
icon: 'align_horizontal_center',
|
||||
category: 'edit',
|
||||
condition: {modes: ['edit'], selected: {element: true}, method: () => BarItems.mirror_modeling.value},
|
||||
onChange(value) {
|
||||
Outliner.selected.forEach(element => {
|
||||
if (!element.constructor.properties.allow_mirror_modeling) return;
|
||||
element.allow_mirror_modeling = value;
|
||||
})
|
||||
}
|
||||
})
|
||||
Blockbench.on('update_selection', () => {
|
||||
if (!Condition(allow_toggle.condition)) return;
|
||||
let disabled = Outliner.selected.find(el => el.allow_mirror_modeling === false);
|
||||
if (allow_toggle.value != !disabled) {
|
||||
allow_toggle.value = !disabled;
|
||||
allow_toggle.updateEnabledState();
|
||||
}
|
||||
})
|
||||
})
|
@ -145,6 +145,43 @@ function rotateSelected(axis, steps) {
|
||||
Undo.finishEdit('Rotate elements')
|
||||
}
|
||||
//Mirror
|
||||
function flipNameOnAxis(node, axis, check, original_name) {
|
||||
const flip_pairs = {
|
||||
0: {
|
||||
right: 'left',
|
||||
Right: 'Left',
|
||||
RIGHT: 'LEFT',
|
||||
},
|
||||
1: {
|
||||
top: 'bottom',
|
||||
Top: 'Bottom',
|
||||
TOP: 'BOTTOM',
|
||||
},
|
||||
2: {
|
||||
back: 'front',
|
||||
rear: 'front',
|
||||
Back: 'Front',
|
||||
Rear: 'Front',
|
||||
BACK: 'FRONT',
|
||||
REAR: 'FRONT',
|
||||
}
|
||||
};
|
||||
function matchAndReplace(a, b) {
|
||||
if (node.name.includes(a)) {
|
||||
let name = original_name
|
||||
? original_name.replace(a, b)
|
||||
: node.name.replace(a, b).replace(/2/, '');
|
||||
if (check(name)) node.name = name;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
let pairs = flip_pairs[axis];
|
||||
for (let a in pairs) {
|
||||
let b = pairs[a];
|
||||
if (matchAndReplace(a, b)) break;
|
||||
if (matchAndReplace(b, a)) break;
|
||||
}
|
||||
}
|
||||
function mirrorSelected(axis) {
|
||||
if (Modes.animate && Timeline.selected.length) {
|
||||
|
||||
@ -160,26 +197,6 @@ function mirrorSelected(axis) {
|
||||
Undo.initEdit({elements: selected, outliner: Format.bone_rig || Group.selected, selection: true})
|
||||
var center = Format.centered_grid ? 0 : 8;
|
||||
if (Format.bone_rig) {
|
||||
let flip_pairs = {
|
||||
0: {
|
||||
right: 'left',
|
||||
Right: 'Left',
|
||||
RIGHT: 'LEFT',
|
||||
},
|
||||
1: {
|
||||
top: 'bottom',
|
||||
Top: 'Bottom',
|
||||
TOP: 'BOTTOM',
|
||||
},
|
||||
2: {
|
||||
back: 'front',
|
||||
rear: 'front',
|
||||
Back: 'Front',
|
||||
Rear: 'Front',
|
||||
BACK: 'FRONT',
|
||||
REAR: 'FRONT',
|
||||
}
|
||||
}
|
||||
if (Group.selected && Group.selected.matchesSelection()) {
|
||||
function flipGroup(group) {
|
||||
for (var i = 0; i < 3; i++) {
|
||||
@ -189,21 +206,7 @@ function mirrorSelected(axis) {
|
||||
group.rotation[i] *= -1
|
||||
}
|
||||
}
|
||||
function matchAndReplace(a, b) {
|
||||
if (group.name.includes(a)) {
|
||||
let name = group._original_name
|
||||
? group._original_name.replace(a, b)
|
||||
: group.name.replace(a, b).replace(/2/, '');
|
||||
if (!Group.all.find(g => g.name == name)) group.name = name;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
let pairs = flip_pairs[axis];
|
||||
for (let a in pairs) {
|
||||
let b = pairs[a];
|
||||
if (matchAndReplace(a, b)) break;
|
||||
if (matchAndReplace(b, a)) break;
|
||||
}
|
||||
flipNameOnAxis(group, axis, name => (!Group.all.find(g => g.name == name)), group._original_name);
|
||||
Canvas.updateAllBones([group]);
|
||||
}
|
||||
flipGroup(Group.selected)
|
||||
@ -211,10 +214,10 @@ function mirrorSelected(axis) {
|
||||
}
|
||||
}
|
||||
selected.forEach(function(obj) {
|
||||
obj.flip(axis, center, false)
|
||||
if (obj instanceof Cube && obj.box_uv && axis === 0) {
|
||||
obj.mirror_uv = !obj.mirror_uv
|
||||
Canvas.updateUV(obj)
|
||||
if (obj instanceof Mesh) {
|
||||
obj.flipSelection(axis, center, false);
|
||||
} else {
|
||||
obj.flip(axis, center, false);
|
||||
}
|
||||
})
|
||||
updateSelection()
|
||||
|
@ -49,6 +49,45 @@ class CubeFace extends Face {
|
||||
case 'down': return [7, 2, 3, 6];
|
||||
}
|
||||
}
|
||||
UVToLocal(point) {
|
||||
let {from, to} = this.cube;
|
||||
let vector = new THREE.Vector3().fromArray(from);
|
||||
|
||||
let lerp_x = Math.getLerp(this.uv[0], this.uv[2], point[0]);
|
||||
let lerp_y = Math.getLerp(this.uv[1], this.uv[3], point[1]);
|
||||
|
||||
if (this.direction == 'east') {
|
||||
vector.x = to[0];
|
||||
vector.y = Math.lerp(to[1], from[1], lerp_y);
|
||||
vector.z = Math.lerp(to[2], from[2], lerp_x);
|
||||
}
|
||||
if (this.direction == 'west') {
|
||||
vector.y = Math.lerp(to[1], from[1], lerp_y);
|
||||
vector.z = Math.lerp(from[2], to[2], lerp_x);
|
||||
}
|
||||
if (this.direction == 'up') {
|
||||
vector.y = to[1];
|
||||
vector.z = Math.lerp(from[2], to[2], lerp_y);
|
||||
vector.x = Math.lerp(from[0], to[0], lerp_x);
|
||||
}
|
||||
if (this.direction == 'down') {
|
||||
vector.z = Math.lerp(to[2], from[2], lerp_y);
|
||||
vector.x = Math.lerp(from[0], to[0], lerp_x);
|
||||
}
|
||||
if (this.direction == 'south') {
|
||||
vector.z = to[2];
|
||||
vector.y = Math.lerp(to[1], from[1], lerp_y);
|
||||
vector.x = Math.lerp(from[0], to[0], lerp_x);
|
||||
}
|
||||
if (this.direction == 'north') {
|
||||
vector.y = Math.lerp(to[1], from[1], lerp_y);
|
||||
vector.x = Math.lerp(to[0], from[0], lerp_x);
|
||||
}
|
||||
vector.x -= this.cube.origin[0];
|
||||
vector.y -= this.cube.origin[1];
|
||||
vector.z -= this.cube.origin[2];
|
||||
return vector;
|
||||
}
|
||||
}
|
||||
new Property(CubeFace, 'number', 'rotation', {default: 0});
|
||||
new Property(CubeFace, 'number', 'tint', {default: -1});
|
||||
@ -414,6 +453,9 @@ class Cube extends OutlinerElement {
|
||||
|
||||
if (!skipUV) {
|
||||
|
||||
if (this.box_uv && axis === 0) {
|
||||
this.mirror_uv = !this.mirror_uv;
|
||||
}
|
||||
function mirrorUVX(face, skip_rot) {
|
||||
var f = scope.faces[face]
|
||||
if (skip_rot) {}
|
||||
@ -820,11 +862,11 @@ class Cube extends OutlinerElement {
|
||||
Cube.prototype.needsUniqueName = false;
|
||||
Cube.prototype.menu = new Menu([
|
||||
...Outliner.control_menu_group,
|
||||
'_',
|
||||
'rename',
|
||||
new MenuSeparator('settings'),
|
||||
'convert_to_mesh',
|
||||
'update_autouv',
|
||||
'cube_uv_mode',
|
||||
'allow_element_mirror_modeling',
|
||||
{name: 'menu.cube.color', icon: 'color_lens', children() {
|
||||
return markerColors.map((color, i) => {return {
|
||||
icon: 'bubble_chart',
|
||||
@ -837,7 +879,7 @@ class Cube extends OutlinerElement {
|
||||
}
|
||||
}});
|
||||
}},
|
||||
{name: 'menu.cube.texture', icon: 'collections', condition: () => !Project.single_texture, children: function() {
|
||||
{name: 'menu.cube.texture', icon: 'collections', condition: () => !Format.single_texture, children: function() {
|
||||
var arr = [
|
||||
{icon: 'crop_square', name: 'menu.cube.texture.blank', click: function(cube) {
|
||||
cube.forSelected(function(obj) {
|
||||
@ -859,6 +901,8 @@ class Cube extends OutlinerElement {
|
||||
return arr;
|
||||
}},
|
||||
'edit_material_instances',
|
||||
new MenuSeparator('manage'),
|
||||
'rename',
|
||||
'toggle_visibility',
|
||||
'delete'
|
||||
]);
|
||||
|
@ -436,10 +436,7 @@ class Group extends OutlinerNode {
|
||||
}
|
||||
Group.prototype.menu = new Menu([
|
||||
...Outliner.control_menu_group,
|
||||
'_',
|
||||
'add_locator',
|
||||
'_',
|
||||
'rename',
|
||||
new MenuSeparator('settings'),
|
||||
'edit_bedrock_binding',
|
||||
{name: 'menu.cube.color', icon: 'color_lens', children() {
|
||||
return markerColors.map((color, i) => {return {
|
||||
@ -452,7 +449,10 @@ class Group extends OutlinerNode {
|
||||
}})
|
||||
}},
|
||||
{icon: 'sort_by_alpha', name: 'menu.group.sort', condition: {modes: ['edit']}, click: function(group) {group.sortContent()}},
|
||||
'add_locator',
|
||||
new MenuSeparator('manage'),
|
||||
'resolve_group',
|
||||
'rename',
|
||||
'delete'
|
||||
]);
|
||||
Object.defineProperty(Group, 'all', {
|
||||
|
@ -92,6 +92,8 @@ class Locator extends OutlinerElement {
|
||||
];
|
||||
Locator.prototype.needsUniqueName = true;
|
||||
Locator.prototype.menu = new Menu([
|
||||
...Outliner.control_menu_group,
|
||||
new MenuSeparator('settings'),
|
||||
{
|
||||
id: 'ignore_inherited_scale',
|
||||
name: 'menu.locator.ignore_inherited_scale',
|
||||
@ -106,14 +108,9 @@ class Locator extends OutlinerElement {
|
||||
Undo.finishEdit('Change locator ignore inherit scale option');
|
||||
}
|
||||
},
|
||||
'_',
|
||||
...Outliner.control_menu_group,
|
||||
'_',
|
||||
'copy',
|
||||
'paste',
|
||||
'duplicate',
|
||||
'_',
|
||||
new MenuSeparator('manage'),
|
||||
'rename',
|
||||
'toggle_visibility',
|
||||
'delete'
|
||||
])
|
||||
|
||||
@ -171,7 +168,7 @@ OutlinerElement.registerType(Locator, 'locator');
|
||||
this.dispatchEvent('update_selection', {element});
|
||||
},
|
||||
updateWindowSize(element) {
|
||||
let size = 18 / Preview.selected.height;
|
||||
let size = 0.4 * Preview.selected.camera.fov / Preview.selected.height;
|
||||
element.mesh.sprite.scale.set(size, size, size);
|
||||
}
|
||||
})
|
||||
|
@ -602,7 +602,25 @@ class Mesh extends OutlinerElement {
|
||||
return this;
|
||||
}
|
||||
flip(axis, center) {
|
||||
let object_mode = BarItems.selection_mode.value == 'object';
|
||||
for (let vkey in this.vertices) {
|
||||
this.vertices[vkey][axis] *= -1;
|
||||
}
|
||||
for (let key in this.faces) {
|
||||
this.faces[key].invert();
|
||||
}
|
||||
|
||||
this.origin[axis] *= -1;
|
||||
this.rotation.forEach((n, i) => {
|
||||
if (i != axis) this.rotation[i] = -n;
|
||||
})
|
||||
this.preview_controller.updateTransform(this);
|
||||
|
||||
this.preview_controller.updateGeometry(this);
|
||||
this.preview_controller.updateUV(this);
|
||||
return this;
|
||||
}
|
||||
flipSelection(axis, center) {
|
||||
let object_mode = BarItems.selection_mode.value == 'object' || !!Group.selected;
|
||||
let selected_vertices = this.getSelectedVertices();
|
||||
for (let vkey in this.vertices) {
|
||||
if (object_mode || selected_vertices.includes(vkey)) {
|
||||
@ -700,6 +718,7 @@ class Mesh extends OutlinerElement {
|
||||
Mesh.prototype.rotatable = true;
|
||||
Mesh.prototype.needsUniqueName = false;
|
||||
Mesh.prototype.menu = new Menu([
|
||||
new MenuSeparator('mesh_edit'),
|
||||
'extrude_mesh_selection',
|
||||
'inset_mesh_selection',
|
||||
'loop_cut',
|
||||
@ -708,12 +727,12 @@ class Mesh extends OutlinerElement {
|
||||
'switch_face_crease',
|
||||
'merge_vertices',
|
||||
'dissolve_edges',
|
||||
'_',
|
||||
new MenuSeparator('mesh_combination'),
|
||||
'split_mesh',
|
||||
'merge_meshes',
|
||||
...Outliner.control_menu_group,
|
||||
'_',
|
||||
'rename',
|
||||
new MenuSeparator('settings'),
|
||||
'allow_element_mirror_modeling',
|
||||
{name: 'menu.cube.color', icon: 'color_lens', children() {
|
||||
return markerColors.map((color, i) => {return {
|
||||
icon: 'bubble_chart',
|
||||
@ -726,7 +745,7 @@ class Mesh extends OutlinerElement {
|
||||
}
|
||||
}})
|
||||
}},
|
||||
{name: 'menu.cube.texture', icon: 'collections', condition: () => !Project.single_texture, children: function() {
|
||||
{name: 'menu.cube.texture', icon: 'collections', condition: () => !Format.single_texture, children: function() {
|
||||
var arr = [
|
||||
{icon: 'crop_square', name: 'menu.cube.texture.blank', click: function(cube) {
|
||||
cube.forSelected(function(obj) {
|
||||
@ -747,6 +766,8 @@ class Mesh extends OutlinerElement {
|
||||
})
|
||||
return arr;
|
||||
}},
|
||||
new MenuSeparator('manage'),
|
||||
'rename',
|
||||
'toggle_visibility',
|
||||
'delete'
|
||||
]);
|
||||
@ -849,9 +870,12 @@ new NodePreviewController(Mesh, {
|
||||
|
||||
let index_offset = position_array.length / 3;
|
||||
let face_indices = {};
|
||||
face.vertices.forEach((key, i) => {
|
||||
position_array.push(...element.vertices[key])
|
||||
face_indices[key] = index_offset + i;
|
||||
face.vertices.forEach((vkey, i) => {
|
||||
if (!element.vertices[vkey]) {
|
||||
throw new Error(`Face "${key}" in mesh "${element.name}" contains an invalid vertex key "${vkey}"`, face)
|
||||
}
|
||||
position_array.push(...element.vertices[vkey])
|
||||
face_indices[vkey] = index_offset + i;
|
||||
})
|
||||
|
||||
let normal = face.getNormal(true);
|
||||
|
@ -112,6 +112,7 @@ class NullObject extends OutlinerElement {
|
||||
];
|
||||
NullObject.prototype.needsUniqueName = true;
|
||||
NullObject.prototype.menu = new Menu([
|
||||
new MenuSeparator('ik'),
|
||||
'set_ik_target',
|
||||
'set_ik_source',
|
||||
{
|
||||
@ -129,9 +130,8 @@ class NullObject extends OutlinerElement {
|
||||
if (Modes.animate) Animator.preview();
|
||||
}
|
||||
},
|
||||
'_',
|
||||
...Outliner.control_menu_group,
|
||||
'_',
|
||||
new MenuSeparator('manage'),
|
||||
'rename',
|
||||
'delete'
|
||||
])
|
||||
@ -190,7 +190,7 @@ class NullObject extends OutlinerElement {
|
||||
this.dispatchEvent('update_selection', {element});
|
||||
},
|
||||
updateWindowSize(element) {
|
||||
let size = 17 / Preview.selected.height;
|
||||
let size = 0.38 * Preview.selected.camera.fov / Preview.selected.height;
|
||||
element.mesh.scale.set(size, size, size);
|
||||
}
|
||||
})
|
||||
|
@ -647,7 +647,23 @@ class NodePreviewController extends EventSystem {
|
||||
this.dispatchEvent('update_selection', {element});
|
||||
}
|
||||
}
|
||||
/**
|
||||
Standardied outliner node context menu group order
|
||||
|
||||
(mesh editing)
|
||||
(settings)
|
||||
copypaste
|
||||
copy, paste, duplicate
|
||||
outliner_control
|
||||
group, move
|
||||
(add)
|
||||
settings
|
||||
color, options, texture
|
||||
manage
|
||||
visibility, rename, delete
|
||||
*/
|
||||
Outliner.control_menu_group = [
|
||||
new MenuSeparator('outliner_control'),
|
||||
'copy',
|
||||
'paste',
|
||||
'duplicate',
|
||||
@ -1720,17 +1736,18 @@ Interface.definePanels(function() {
|
||||
`
|
||||
},
|
||||
menu: new Menu([
|
||||
new MenuSeparator('add_element'),
|
||||
'add_mesh',
|
||||
'add_cube',
|
||||
'add_texture_mesh',
|
||||
'add_group',
|
||||
'_',
|
||||
'sort_outliner',
|
||||
new MenuSeparator('manage'),
|
||||
'select_all',
|
||||
'sort_outliner',
|
||||
'collapse_groups',
|
||||
'unfold_groups',
|
||||
'_',
|
||||
'search_outliner',
|
||||
new MenuSeparator('options'),
|
||||
'element_colors',
|
||||
'outliner_toggle'
|
||||
])
|
||||
|
@ -77,9 +77,8 @@ class TextureMesh extends OutlinerElement {
|
||||
TextureMesh.prototype.needsUniqueName = false;
|
||||
TextureMesh.prototype.menu = new Menu([
|
||||
...Outliner.control_menu_group,
|
||||
'_',
|
||||
'rename',
|
||||
{name: 'menu.texture_mesh.texture_name', icon: 'collections', condition: () => !Project.single_texture, click(context) {
|
||||
new MenuSeparator('settings'),
|
||||
{name: 'menu.texture_mesh.texture_name', icon: 'collections', condition: () => !Format.single_texture, click(context) {
|
||||
Blockbench.textPrompt('menu.texture_mesh.texture_name', context.texture_name, value => {
|
||||
Undo.initEdit({elements: TextureMesh.all}),
|
||||
TextureMesh.all.forEach(element => {
|
||||
@ -88,6 +87,8 @@ class TextureMesh extends OutlinerElement {
|
||||
Undo.finishEdit('Change texture mesh texture name')
|
||||
})
|
||||
}},
|
||||
new MenuSeparator('manage'),
|
||||
'rename',
|
||||
'toggle_visibility',
|
||||
'delete'
|
||||
]);
|
||||
|
@ -37,19 +37,22 @@ class Plugin {
|
||||
constructor(id, data) {
|
||||
this.id = id||'unknown';
|
||||
this.installed = false;
|
||||
this.expanded = false;
|
||||
this.title = '';
|
||||
this.author = '';
|
||||
this.description = '';
|
||||
this.about = '';
|
||||
this.icon = '';
|
||||
this.tags = [];
|
||||
this.dependencies = [];
|
||||
this.version = '0.0.1';
|
||||
this.variant = 'both';
|
||||
this.min_version = '';
|
||||
this.max_version = '';
|
||||
this.source = 'store'
|
||||
this.source = 'store';
|
||||
this.creation_date = 0;
|
||||
this.await_loading = false;
|
||||
this.about_fetched = false;
|
||||
this.disabled = false;
|
||||
|
||||
this.extend(data)
|
||||
|
||||
@ -58,7 +61,6 @@ class Plugin {
|
||||
extend(data) {
|
||||
if (!(data instanceof Object)) return this;
|
||||
Merge.boolean(this, data, 'installed')
|
||||
Merge.boolean(this, data, 'expanded')
|
||||
Merge.string(this, data, 'title')
|
||||
Merge.string(this, data, 'author')
|
||||
Merge.string(this, data, 'description')
|
||||
@ -68,7 +70,12 @@ class Plugin {
|
||||
Merge.string(this, data, 'variant')
|
||||
Merge.string(this, data, 'min_version')
|
||||
Merge.boolean(this, data, 'await_loading');
|
||||
Merge.boolean(this, data, 'disabled');
|
||||
if (data.creation_date) this.creation_date = Date.parse(data.creation_date);
|
||||
if (data.tags instanceof Array) this.tags.safePush(...data.tags.slice(0, 3));
|
||||
if (data.dependencies instanceof Array) this.dependencies.safePush(...data.dependencies);
|
||||
|
||||
this.new_repo_format = this.min_version != '' && !compareVersions('4.8.0', this.min_version);
|
||||
|
||||
Merge.function(this, data, 'onload')
|
||||
Merge.function(this, data, 'onunload')
|
||||
@ -80,6 +87,40 @@ class Plugin {
|
||||
return this.title;
|
||||
}
|
||||
async install() {
|
||||
let required_dependencies = this.dependencies
|
||||
.map(id => (Plugins.all.find(p => p.id == id) || id))
|
||||
.filter(p => (p instanceof Plugin == false || p.installed == false));
|
||||
if (required_dependencies.length) {
|
||||
let failed_dependency = required_dependencies.find(p => (!p.isInstallable || p.isInstallable() != true));
|
||||
if (failed_dependency) {
|
||||
let error_message = failed_dependency;
|
||||
if (failed_dependency instanceof Plugin) {
|
||||
error_message = `**${failed_dependency.title}**: ${failed_dependency.isInstallable()}`;
|
||||
}
|
||||
Blockbench.showMessageBox({
|
||||
title: 'message.plugin_dependencies.title',
|
||||
message: `${tl('message.plugin_dependencies.invalid')}\n\n${error_message}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let list = required_dependencies.map(p => `**${p.title}** ${tl('dialog.plugins.author', [p.author])}`);
|
||||
let response = await new Promise(resolve => {
|
||||
Blockbench.showMessageBox({
|
||||
title: 'message.plugin_dependencies.title',
|
||||
message: `${tl('message.plugin_dependencies.message1')} \n\n* ${ list.join('\n* ') }\n\n${tl('message.plugin_dependencies.message2')}`,
|
||||
buttons: ['dialog.continue', 'dialog.cancel'],
|
||||
width: 512,
|
||||
}, button => {
|
||||
resolve(button == 0);
|
||||
})
|
||||
})
|
||||
if (!response) return;
|
||||
|
||||
for (let dependency of required_dependencies) {
|
||||
await dependency.install();
|
||||
}
|
||||
}
|
||||
return await this.download(true);
|
||||
}
|
||||
async load(first, cb) {
|
||||
@ -135,18 +176,43 @@ class Plugin {
|
||||
if (first) register();
|
||||
return await scope.load(first)
|
||||
}
|
||||
return await new Promise((resolve, reject) => {
|
||||
var file = originalFs.createWriteStream(Plugins.path+this.id+'.js')
|
||||
https.get('https://cdn.jsdelivr.net/gh/JannisX11/blockbench-plugins/plugins/'+this.id+'.js', function(response) {
|
||||
|
||||
// Download files
|
||||
async function copyFileToDrive(origin_filename, target_filename, callback) {
|
||||
var file = originalFs.createWriteStream(PathModule.join(Plugins.path, target_filename));
|
||||
https.get('https://cdn.jsdelivr.net/gh/JannisX11/blockbench-plugins/plugins/'+origin_filename, function(response) {
|
||||
response.pipe(file);
|
||||
response.on('end', function() {
|
||||
if (callback) response.on('end', callback);
|
||||
});
|
||||
}
|
||||
return await new Promise(async (resolve, reject) => {
|
||||
// New system
|
||||
if (this.new_repo_format) {
|
||||
copyFileToDrive(`${this.id}/${this.id}.js`, `${this.id}.js`, () => {
|
||||
if (first) register();
|
||||
setTimeout(async function() {
|
||||
await scope.load(first);
|
||||
resolve()
|
||||
}, 20)
|
||||
});
|
||||
if (this.hasImageIcon()) {
|
||||
copyFileToDrive(`${this.id}/${this.icon}`, this.id + '.' + this.icon);
|
||||
}
|
||||
await this.fetchAbout();
|
||||
if (this.about) {
|
||||
fs.writeFileSync(PathModule.join(Plugins.path, this.id + '.about.md'), this.about, 'utf-8');
|
||||
}
|
||||
|
||||
} else {
|
||||
// Legacy system
|
||||
copyFileToDrive(`${this.id}.js`, `${this.id}.js`, () => {
|
||||
if (first) register();
|
||||
})
|
||||
});
|
||||
setTimeout(async function() {
|
||||
await scope.load(first);
|
||||
resolve()
|
||||
}, 20)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
async loadFromFile(file, first) {
|
||||
@ -162,7 +228,6 @@ class Plugin {
|
||||
|
||||
this.id = pathToName(file.path);
|
||||
Plugins.registered[this.id] = this;
|
||||
localStorage.setItem('plugin_dev_path', file.path);
|
||||
Plugins.all.safePush(this);
|
||||
this.source = 'file';
|
||||
this.tags.safePush('Local');
|
||||
@ -217,7 +282,6 @@ class Plugin {
|
||||
|
||||
this.id = pathToName(url)
|
||||
Plugins.registered[this.id] = this;
|
||||
localStorage.setItem('plugin_dev_path', url)
|
||||
Plugins.all.safePush(this)
|
||||
this.tags.safePush('Remote');
|
||||
|
||||
@ -263,6 +327,7 @@ class Plugin {
|
||||
entry.version = this.version;
|
||||
entry.path = path;
|
||||
entry.source = this.source;
|
||||
entry.disabled = this.disabled ? true : undefined;
|
||||
|
||||
if (!already_exists) Plugins.installed.push(entry);
|
||||
|
||||
@ -288,14 +353,16 @@ class Plugin {
|
||||
Plugins.all.remove(this)
|
||||
}
|
||||
if (isApp && this.source != 'file') {
|
||||
var filepath = Plugins.path + this.id + '.js'
|
||||
if (fs.existsSync(filepath)) {
|
||||
fs.unlink(filepath, (err) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
}
|
||||
});
|
||||
function removeCachedFile(filepath) {
|
||||
if (fs.existsSync(filepath)) {
|
||||
fs.unlink(filepath, (err) => {
|
||||
if (err) console.log(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
removeCachedFile(Plugins.path + this.id + '.js');
|
||||
removeCachedFile(Plugins.path + this.id + '.' + this.icon);
|
||||
removeCachedFile(Plugins.path + this.id + '.about.md');
|
||||
}
|
||||
StateMemory.save('installed_plugins')
|
||||
return this;
|
||||
@ -311,6 +378,7 @@ class Plugin {
|
||||
|
||||
this.unload()
|
||||
this.tags.empty();
|
||||
this.dependencies.empty();
|
||||
Plugins.all.remove(this)
|
||||
|
||||
if (this.source == 'file') {
|
||||
@ -321,8 +389,20 @@ class Plugin {
|
||||
}
|
||||
return this;
|
||||
}
|
||||
toggleDisabled() {
|
||||
if (!this.disabled) {
|
||||
this.disabled = true;
|
||||
this.unload()
|
||||
} else {
|
||||
if (this.onload) {
|
||||
this.onload()
|
||||
}
|
||||
this.disabled = false;
|
||||
}
|
||||
this.remember();
|
||||
}
|
||||
isReloadable() {
|
||||
return (this.source == 'file' && isApp) || (this.source == 'url')
|
||||
return this.installed && !this.disabled && ((this.source == 'file' && isApp) || (this.source == 'url'));
|
||||
}
|
||||
isInstallable() {
|
||||
var scope = this;
|
||||
@ -343,20 +423,43 @@ class Plugin {
|
||||
}
|
||||
return (result === true) ? true : tl('dialog.plugins.'+result);
|
||||
}
|
||||
toggleInfo(force) {
|
||||
if (!this.about) return;
|
||||
var scope = this;
|
||||
Plugins.all.forEach(function(p) {
|
||||
if (p !== scope && p.expanded) p.expanded = false;
|
||||
})
|
||||
if (force !== undefined) {
|
||||
this.expanded = force === true
|
||||
} else {
|
||||
this.expanded = this.expanded !== true
|
||||
}
|
||||
hasImageIcon() {
|
||||
return this.icon.endsWith('.png') || this.icon.endsWith('.svg');
|
||||
}
|
||||
get expandicon() {
|
||||
return this.expanded ? 'expand_less' : 'expand_more'
|
||||
getIcon() {
|
||||
if (this.hasImageIcon()) {
|
||||
if (isApp) {
|
||||
if (this.installed && this.source == 'store') {
|
||||
return Plugins.path + this.id + '.' + this.icon;
|
||||
}
|
||||
if (this.source != 'store')
|
||||
return this.path.replace(/\w+\.js$/, this.icon);
|
||||
}
|
||||
return `https://cdn.jsdelivr.net/gh/JannisX11/blockbench-plugins/plugins/${this.id}/${this.icon}`;
|
||||
}
|
||||
return this.icon;
|
||||
}
|
||||
async fetchAbout() {
|
||||
if (!this.about_fetched && !this.about && this.new_repo_format) {
|
||||
if (isApp && this.installed) {
|
||||
try {
|
||||
let content = fs.readFileSync(PathModule.join(Plugins.path, this.id + '.about.md'), {encoding: 'utf-8'});
|
||||
this.about = content;
|
||||
this.about_fetched = true;
|
||||
return;
|
||||
} catch (err) {
|
||||
console.error('failed to get about for plugin ' + this.id);
|
||||
}
|
||||
}
|
||||
let url = `https://cdn.jsdelivr.net/gh/JannisX11/blockbench-plugins/plugins/${this.id}/about.md`;
|
||||
let result = await fetch(url).catch(() => {
|
||||
console.error('about.md missing for plugin ' + this.id);
|
||||
});
|
||||
if (result.ok) {
|
||||
this.about = await result.text();
|
||||
}
|
||||
this.about_fetched = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Alias for typescript
|
||||
@ -383,8 +486,7 @@ Plugin.register = function(id, data) {
|
||||
})
|
||||
};
|
||||
plugin.extend(data)
|
||||
if (data.icon) plugin.icon = Blockbench.getIconNode(data.icon)
|
||||
if (plugin.isInstallable() == true) {
|
||||
if (plugin.isInstallable() == true && plugin.disabled == false) {
|
||||
if (plugin.onload instanceof Function) {
|
||||
plugin.onload()
|
||||
}
|
||||
@ -404,13 +506,13 @@ if (isApp) {
|
||||
}
|
||||
|
||||
Plugins.loading_promise = new Promise((resolve, reject) => {
|
||||
|
||||
$.ajax({
|
||||
cache: false,
|
||||
url: 'https://cdn.jsdelivr.net/gh/JannisX11/blockbench-plugins/plugins.json',
|
||||
dataType: 'json',
|
||||
success(data) {
|
||||
Plugins.json = data;
|
||||
|
||||
resolve();
|
||||
Plugins.loading_promise.resolved = true;
|
||||
},
|
||||
@ -444,7 +546,10 @@ async function loadInstalledPlugins() {
|
||||
return p && p.id == id && p.source == 'store'
|
||||
});
|
||||
if (installed_match) {
|
||||
if (isApp && installed_match.version && plugin.version && !compareVersions(plugin.version, installed_match.version)) {
|
||||
if (isApp && (
|
||||
(installed_match.version && plugin.version && !compareVersions(plugin.version, installed_match.version)) ||
|
||||
Blockbench.isOlderThan(plugin.min_version)
|
||||
)) {
|
||||
// Get from file
|
||||
let promise = plugin.load(false);
|
||||
install_promises.push(promise);
|
||||
@ -460,11 +565,11 @@ async function loadInstalledPlugins() {
|
||||
Plugins.sort();
|
||||
} else if (Plugins.installed.length > 0 && isApp) {
|
||||
//Offline
|
||||
Plugins.installed.forEach(function(plugin) {
|
||||
Plugins.installed.forEach(function(plugin_data) {
|
||||
|
||||
if (plugin.source == 'store') {
|
||||
let plugin = new Plugin(plugin.id);
|
||||
let promise = plugin.load(false, function() {
|
||||
if (plugin_data.source == 'store') {
|
||||
let instance = new Plugin(plugin_data.id);
|
||||
let promise = instance.load(false, function() {
|
||||
Plugins.sort();
|
||||
})
|
||||
install_promises.push(promise);
|
||||
@ -478,7 +583,7 @@ async function loadInstalledPlugins() {
|
||||
if (plugin.source == 'file') {
|
||||
//Dev Plugins
|
||||
if (isApp && fs.existsSync(plugin.path)) {
|
||||
var instance = new Plugin(plugin.id);
|
||||
var instance = new Plugin(plugin.id, {disabled: plugin.disabled});
|
||||
install_promises.push(instance.loadFromFile({path: plugin.path}, false));
|
||||
load_counter++;
|
||||
console.log(`🧩📁 Loaded plugin "${plugin.id || plugin.path}" from file`);
|
||||
@ -487,7 +592,7 @@ async function loadInstalledPlugins() {
|
||||
}
|
||||
|
||||
} else if (plugin.source == 'url') {
|
||||
var instance = new Plugin(plugin.id);
|
||||
var instance = new Plugin(plugin.id, {disabled: plugin.disabled});
|
||||
install_promises.push(instance.loadFromURL(plugin.path, false));
|
||||
load_counter++;
|
||||
console.log(`🧩🌐 Loaded plugin "${plugin.id || plugin.path}" from URL`);
|
||||
@ -517,13 +622,17 @@ BARS.defineActions(function() {
|
||||
Plugins.dialog = new Dialog({
|
||||
id: 'plugins',
|
||||
title: 'dialog.plugins.title',
|
||||
singleButton: true,
|
||||
width: 760,
|
||||
buttons: [],
|
||||
width: 1200,
|
||||
component: {
|
||||
data: {
|
||||
tab: 'installed',
|
||||
search_term: '',
|
||||
items: Plugins.all
|
||||
items: Plugins.all,
|
||||
selected_plugin: null,
|
||||
page: 0,
|
||||
per_page: 25,
|
||||
isMobile: Blockbench.isMobile,
|
||||
},
|
||||
computed: {
|
||||
plugin_search() {
|
||||
@ -543,9 +652,72 @@ BARS.defineActions(function() {
|
||||
}
|
||||
return false;
|
||||
})
|
||||
},
|
||||
suggested_rows() {
|
||||
let tags = ["Animation"];
|
||||
this.items.forEach(plugin => {
|
||||
if (!plugin.installed) return;
|
||||
tags.safePush(...plugin.tags)
|
||||
})
|
||||
let rows = tags.map(tag => {
|
||||
let plugins = this.items.filter(plugin => !plugin.installed && plugin.tags.includes(tag) && !plugin.tags.includes('Deprecated')).slice(0, 12);
|
||||
return {
|
||||
title: tag,
|
||||
plugins,
|
||||
}
|
||||
}).filter(row => row.plugins.length > 2);
|
||||
//rows.sort((a, b) => a.plugins.length - b.plugins.length);
|
||||
rows.sort(() => Math.random() - 0.5);
|
||||
|
||||
let cutoff = Date.now() - (3_600_000 * 24 * 28);
|
||||
let new_plugins = this.items.filter(plugin => !plugin.installed && plugin.creation_date > cutoff && !plugin.tags.includes('Deprecated'))
|
||||
new_plugins.sort((a, b) => a.creation_date - b.creation_date);
|
||||
let new_row = {
|
||||
title: 'New',
|
||||
plugins: new_plugins.slice(0, 12)
|
||||
}
|
||||
rows.splice(0, 0, new_row);
|
||||
|
||||
return rows.slice(0, 3);
|
||||
},
|
||||
viewed_plugins() {
|
||||
return this.plugin_search.slice(this.page * this.per_page, (this.page+1) * this.per_page);
|
||||
},
|
||||
pages() {
|
||||
let pages = [];
|
||||
let length = this.plugin_search.length;
|
||||
for (let i = 0; i * this.per_page < length; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
return pages;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setTab(tab) {
|
||||
this.tab = tab;
|
||||
this.setPage(0);
|
||||
},
|
||||
setPage(number) {
|
||||
this.page = number;
|
||||
},
|
||||
selectPlugin(plugin) {
|
||||
plugin.fetchAbout();
|
||||
this.selected_plugin = plugin;
|
||||
},
|
||||
showDependency(dependency) {
|
||||
let plugin = Plugins.all.find(p => p.id == dependency);
|
||||
if (plugin) {
|
||||
this.selectPlugin(plugin);
|
||||
}
|
||||
},
|
||||
getDependencyName(dependency) {
|
||||
let plugin = Plugins.all.find(p => p.id == dependency);
|
||||
return plugin ? (plugin.title + (plugin.installed ? ' ✓' : '')) : (dependency + ' ⚠');
|
||||
},
|
||||
isDependencyInstalled(dependency) {
|
||||
let plugin = Plugins.all.find(p => p.id == dependency);
|
||||
return plugin && plugin.installed;
|
||||
},
|
||||
getTagClass(tag) {
|
||||
if (tag.match(/^(local|remote)$/i)) {
|
||||
return 'plugin_tag_source'
|
||||
@ -555,52 +727,137 @@ BARS.defineActions(function() {
|
||||
return 'plugin_tag_deprecated'
|
||||
}
|
||||
},
|
||||
formatAbout(about) {
|
||||
return pureMarked(about);
|
||||
},
|
||||
getIconNode: Blockbench.getIconNode,
|
||||
pureMarked,
|
||||
tl
|
||||
},
|
||||
mount_directly: true,
|
||||
template: `
|
||||
<div style="margin-top: 10px;">
|
||||
<div class="bar">
|
||||
<div class="tab_bar">
|
||||
<div :class="{open: tab == 'installed'}" @click="tab = 'installed'">${tl('dialog.plugins.installed')}</div>
|
||||
<div :class="{open: tab == 'available'}" @click="tab = 'available'">${tl('dialog.plugins.available')}</div>
|
||||
<content style="display: flex;" class="dialog_content">
|
||||
<div id="plugin_browser_sidebar" v-show="!isMobile || !selected_plugin">
|
||||
<div class="bar flex" id="plugins_list_main_bar">
|
||||
<div class="tool" v-if="!isMobile" @click="selected_plugin = null"><i class="material-icons icon">home</i></div>
|
||||
<search-bar id="plugin_search_bar" v-model="search_term" @input="setPage(0)"></search-bar>
|
||||
</div>
|
||||
<search-bar id="plugin_search_bar" v-model="search_term"></search-bar>
|
||||
<div class="tab_bar">
|
||||
<div :class="{open: tab == 'installed'}" @click="setTab('installed')">${tl('dialog.plugins.installed')}</div>
|
||||
<div :class="{open: tab == 'available'}" @click="setTab('available')">${tl('dialog.plugins.available')}</div>
|
||||
</div>
|
||||
<ul class="list" id="plugin_list">
|
||||
<li v-for="plugin in viewed_plugins" :plugin="plugin.id" :class="{plugin: true, testing: plugin.fromFile, selected: plugin == selected_plugin, disabled_plugin: plugin.disabled, incompatible: plugin.isInstallable() !== true}" @click="selectPlugin(plugin)">
|
||||
<div>
|
||||
<div class="plugin_icon_area">
|
||||
<img v-if="plugin.hasImageIcon()" :src="plugin.getIcon()" width="48" height="48px" />
|
||||
<dynamic-icon v-else :icon="plugin.icon" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="title">{{ plugin.title || plugin.id }}</div>
|
||||
<div class="author">{{ tl('dialog.plugins.author', [plugin.author]) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="description">{{ plugin.description }}</div>
|
||||
<ul class="plugin_tag_list">
|
||||
<li v-for="tag in plugin.tags" :class="getTagClass(tag)" :key="tag" @click="search_term = tag;">{{tag}}</li>
|
||||
</ul>
|
||||
</li>
|
||||
<div class="no_plugin_message tl" v-if="plugin_search.length < 1 && tab === 'installed'">${tl('dialog.plugins.none_installed')}</div>
|
||||
<div class="no_plugin_message tl" v-if="plugin_search.length < 1 && tab === 'available'" id="plugin_available_empty">{{ tl(navigator.onLine ? 'dialog.plugins.none_available' : 'dialog.plugins.offline') }}</div>
|
||||
</ul>
|
||||
<ol class="pagination_numbers" v-if="pages.length > 1">
|
||||
<li v-for="number in pages" :class="{selected: page == number}" @click="setPage(number)">{{ number+1 }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<ul class="list" id="plugin_list">
|
||||
<li v-for="plugin in plugin_search" v-bind:plugin="plugin.id" v-bind:class="{plugin: true, testing: plugin.fromFile, expanded: plugin.expanded, has_about_text: !!plugin.about}">
|
||||
<div class="title" v-on:click="plugin.toggleInfo()">
|
||||
<div class="icon_wrapper plugin_icon normal" v-html="getIconNode(plugin.icon || 'error_outline', plugin.icon ? plugin.color : 'var(--color-close)').outerHTML"></div>
|
||||
|
||||
<i v-if="plugin.expanded" class="material-icons plugin_expand_icon">expand_less</i>
|
||||
<i v-else class="material-icons plugin_expand_icon">expand_more</i>
|
||||
{{ plugin.title || plugin.id }}
|
||||
|
||||
<div id="plugin_browser_page" v-if="selected_plugin">
|
||||
<div v-if="isMobile" @click="selected_plugin = null;">Back to Overview todo</div>
|
||||
<div class="plugin_browser_page_header" :class="{disabled_plugin: selected_plugin.disabled}">
|
||||
<div class="plugin_icon_area">
|
||||
<img v-if="selected_plugin.hasImageIcon()" :src="selected_plugin.getIcon()" width="48" height="48px" />
|
||||
<dynamic-icon v-else :icon="selected_plugin.icon" />
|
||||
</div>
|
||||
<div class="plugin_version">{{ plugin.version }}</div>
|
||||
<div class="button_bar" v-if="plugin.installed || plugin.isInstallable() == true">
|
||||
<button type="button" class="" v-on:click="plugin.uninstall()" v-if="plugin.installed"><i class="material-icons">delete</i><span>${tl('dialog.plugins.uninstall')}</span></button>
|
||||
<button type="button" class="" v-on:click="plugin.install()" v-else><i class="material-icons">add</i><span>${tl('dialog.plugins.install')}</span></button>
|
||||
<button type="button" v-on:click="plugin.reload()" v-if="plugin.installed && plugin.isReloadable()"><i class="material-icons">refresh</i><span>${tl('dialog.plugins.reload')}</span></button>
|
||||
<div>
|
||||
<h1>
|
||||
{{ selected_plugin.title || selected_plugin.id }}
|
||||
<div class="version">v{{ selected_plugin.version }}</div>
|
||||
</h1>
|
||||
<div class="author">
|
||||
{{ tl('dialog.plugins.author', [selected_plugin.author]) }}
|
||||
<div v-if="selected_plugin.disabled" class="plugin_disabled_tag">🌙 ${tl('dialog.plugins.is_disabled')}</div>
|
||||
<div v-else-if="selected_plugin.installed" class="plugin_installed_tag">✓ ${tl('dialog.plugins.is_installed')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button_bar tiny" v-if="plugin.isInstallable() != true">{{ plugin.isInstallable() }}</div>
|
||||
</div>
|
||||
|
||||
<div class="author">{{ tl('dialog.plugins.author', [plugin.author]) }}</div>
|
||||
<div class="description">{{ plugin.description }}</div>
|
||||
<div v-if="plugin.expanded" class="about markdown" v-html="pureMarked(plugin.about.replace(/\\n/g, '\\n\\n'))"><button>a</button></div>
|
||||
<div v-if="plugin.expanded" v-on:click="plugin.toggleInfo()" style="text-decoration: underline;">${tl('dialog.plugins.show_less')}</div>
|
||||
<ul class="plugin_tag_list">
|
||||
<li v-for="tag in plugin.tags" :class="getTagClass(tag)" :key="tag" @click="search_term = tag;">{{tag}}</li>
|
||||
<div class="button_bar" v-if="selected_plugin.installed || selected_plugin.isInstallable() == true">
|
||||
<button type="button" v-if="selected_plugin.installed && selected_plugin.source != 'store'" @click="selected_plugin.toggleDisabled()">
|
||||
<i class="material-icons icon">bedtime</i>
|
||||
<span>{{ selected_plugin.disabled ? '${tl('dialog.plugins.enable')}' : '${tl('dialog.plugins.disable')}' }}</span>
|
||||
</button>
|
||||
<button type="button" @click="selected_plugin.reload()" v-if="selected_plugin.installed && selected_plugin.isReloadable()">
|
||||
<i class="material-icons icon">refresh</i>
|
||||
<span>${tl('dialog.plugins.reload')}</span>
|
||||
</button>
|
||||
<button type="button" class="" @click="selected_plugin.uninstall()" v-if="selected_plugin.installed">
|
||||
<i class="material-icons icon">delete</i>
|
||||
<span>${tl('dialog.plugins.uninstall')}</span>
|
||||
</button>
|
||||
<button type="button" class="" @click="selected_plugin.install()" v-else>
|
||||
<i class="material-icons icon">add</i>
|
||||
<span>${tl('dialog.plugins.install')}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul class="plugin_tag_list">
|
||||
<li v-for="tag in selected_plugin.tags" :class="getTagClass(tag)" :key="tag" @click="search_term = tag;">{{tag}}</li>
|
||||
</ul>
|
||||
|
||||
<div class="description" :class="{disabled_plugin: selected_plugin.disabled}">{{ selected_plugin.description }}</div>
|
||||
|
||||
<div class="plugin_dependencies" v-if="selected_plugin.dependencies.length">
|
||||
${tl('dialog.plugins.dependencies')}
|
||||
<a v-for="dep in selected_plugin.dependencies" @click="showDependency(dep)" :class="{installed: isDependencyInstalled(dep)}">{{ getDependencyName(dep) }}</a>
|
||||
</div>
|
||||
|
||||
<div class="tiny plugin_compatibility_issue" v-if="selected_plugin.isInstallable() != true">
|
||||
<i class="material-icons icon">error</i>
|
||||
{{ selected_plugin.isInstallable() }}
|
||||
</div>
|
||||
|
||||
<h2 v-if="selected_plugin.about" style="margin-top: 36px;">About</h2>
|
||||
<dynamic-icon v-else-if="!selected_plugin.hasImageIcon()" :icon="selected_plugin.icon" id="plugin_page_background_decoration" />
|
||||
<div class="about markdown" v-if="selected_plugin.about" v-html="formatAbout(selected_plugin.about)"></div>
|
||||
</div>
|
||||
|
||||
<div id="plugin_browser_start_page" v-if="!selected_plugin && !isMobile">
|
||||
<h1>Blockbench Plugins</h1>
|
||||
<img src="./assets/plugins.png" />
|
||||
<p>Plugins allow you to configure Blockbench beyond the default capabilities. Select from a list of 100 community created plugins.</p>
|
||||
<p>Want to write your own plugin? Check out the <a href="https://www.blockbench.net/wiki/docs/plugin" target="_blank">Plugin Documentation</a>.</p>
|
||||
|
||||
<div v-for="row in suggested_rows" class="plugins_suggested_row">
|
||||
<h3>{{row.title}}</h3>
|
||||
<ul>
|
||||
<li v-for="plugin in row.plugins" @click="selectPlugin(plugin)">
|
||||
<div class="plugin_icon_area">
|
||||
<img v-if="plugin.hasImageIcon()" :src="plugin.getIcon()" width="48" height="48px" />
|
||||
<dynamic-icon v-else :icon="plugin.icon" />
|
||||
</div>
|
||||
<div class="title"><span>{{ plugin.title || plugin.id }}</span></div>
|
||||
<div class="author">{{ plugin.author }}</div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<div class="no_plugin_message tl" v-if="plugin_search.length < 1 && tab === 'installed'">${tl('dialog.plugins.none_installed')}</div>
|
||||
<div class="no_plugin_message tl" v-if="plugin_search.length < 1 && tab === 'available'" id="plugin_available_empty">{{ tl(navigator.onLine ? 'dialog.plugins.none_available' : 'dialog.plugins.offline') }}</div>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</content>
|
||||
`
|
||||
}
|
||||
})
|
||||
|
||||
let actions_setup = false;
|
||||
new Action('plugins_window', {
|
||||
icon: 'extension',
|
||||
category: 'blockbench',
|
||||
@ -612,13 +869,11 @@ BARS.defineActions(function() {
|
||||
Plugins.dialog.show();
|
||||
let none_installed = !Plugins.all.find(plugin => plugin.installed);
|
||||
if (none_installed) Plugins.dialog.content_vue.tab = 'available';
|
||||
if (!Plugins.dialog.button_bar) {
|
||||
Plugins.dialog.button_bar = Interface.createElement('div', {class: 'bar next_to_title', id: 'plugins_header_bar'});
|
||||
Plugins.dialog.object.firstElementChild.after(Plugins.dialog.button_bar);
|
||||
BarItems.load_plugin.toElement('#plugins_header_bar');
|
||||
BarItems.load_plugin_from_url.toElement('#plugins_header_bar');
|
||||
if (!actions_setup) {
|
||||
BarItems.load_plugin.toElement('#plugins_list_main_bar');
|
||||
BarItems.load_plugin_from_url.toElement('#plugins_list_main_bar');
|
||||
actions_setup = true;
|
||||
}
|
||||
$('#plugin_list').css('max-height', limitNumber(window.innerHeight-226, 80, 800)+'px');
|
||||
$('dialog#plugins #plugin_search_bar input').trigger('focus')
|
||||
}
|
||||
})
|
||||
|
@ -639,6 +639,22 @@ const Canvas = {
|
||||
alphaTest: 0.2
|
||||
})
|
||||
|
||||
let brush_img = new Image();
|
||||
brush_img.src = 'assets/brush_outline.png';
|
||||
brush_img.tex = new THREE.Texture(brush_img);
|
||||
brush_img.tex.magFilter = THREE.NearestFilter;
|
||||
brush_img.tex.minFilter = THREE.NearestFilter;
|
||||
brush_img.onload = function() {
|
||||
this.tex.needsUpdate = true;
|
||||
}
|
||||
let brush_outline_material = new THREE.MeshBasicMaterial({
|
||||
map: brush_img.tex,
|
||||
transparent: true,
|
||||
side: THREE.DoubleSide,
|
||||
alphaTest: 0.2
|
||||
})
|
||||
Canvas.brush_outline = new THREE.Mesh(new THREE.PlaneBufferGeometry(1, 1), brush_outline_material);
|
||||
|
||||
/*
|
||||
// Vertex gizmos
|
||||
var vertex_img = new Image();
|
||||
|
@ -308,7 +308,7 @@ class Preview {
|
||||
}, false)
|
||||
addEventListeners(this.canvas, 'mousemove', event => { this.mousemove(event)}, false)
|
||||
addEventListeners(this.canvas, 'mouseup touchend', event => { this.mouseup(event)}, false)
|
||||
addEventListeners(this.canvas, 'dblclick', event => { Toolbox.toggleTransforms(event); }, false)
|
||||
addEventListeners(this.canvas, 'dblclick', event => { if (settings.double_click_switch_tools.value) Toolbox.toggleTransforms(event); }, false)
|
||||
addEventListeners(this.canvas, 'mouseenter touchstart', event => { this.occupyTransformer(event)}, false)
|
||||
addEventListeners(this.canvas, 'mouseenter', event => { this.controls.hasMoved = true}, false)
|
||||
|
||||
@ -1007,9 +1007,63 @@ class Preview {
|
||||
}
|
||||
}
|
||||
mousemove(event) {
|
||||
if (Settings.get('highlight_cubes')) {
|
||||
if (Settings.get('highlight_cubes') || Toolbox.selected.brush?.size) {
|
||||
var data = this.raycast(event);
|
||||
updateCubeHighlights(data && data.element);
|
||||
|
||||
if (Toolbox.selected.brush?.size) {
|
||||
if (!data) {
|
||||
scene.remove(Canvas.brush_outline);
|
||||
return;
|
||||
}
|
||||
let face = data.element.faces[data.face];
|
||||
let texture = face.getTexture();
|
||||
if (!texture) {
|
||||
scene.remove(Canvas.brush_outline);
|
||||
return;
|
||||
}
|
||||
scene.add(Canvas.brush_outline);
|
||||
|
||||
let intersect = data.intersects[0];
|
||||
let world_quaternion = intersect.object.getWorldQuaternion(Reusable.quat1)
|
||||
let world_normal = Reusable.vec1.copy(intersect.face.normal).applyQuaternion(world_quaternion);
|
||||
|
||||
// UV
|
||||
let offset = 0;
|
||||
let x = intersect.uv.x * texture.width;
|
||||
let y = (1-intersect.uv.y) * texture.height;
|
||||
if (Condition(Toolbox.selected.brush.floor_coordinates)) {
|
||||
offset = BarItems.slider_brush_size.get()%2 == 0 && Toolbox.selected.brush?.offset_even_radius ? 0 : 0.5;
|
||||
x = Math.round(x + offset) - offset;
|
||||
y = Math.round(y + offset) - offset;
|
||||
}
|
||||
// Position
|
||||
let brush_coord = face.UVToLocal([x, y]);
|
||||
let brush_coord_difference = face.UVToLocal([x, y+1]);
|
||||
brush_coord_difference.sub(brush_coord);
|
||||
intersect.object.localToWorld(brush_coord);
|
||||
Canvas.brush_outline.position.copy(brush_coord);
|
||||
|
||||
//size
|
||||
let radius = BarItems.slider_brush_size.get() * 1.03 * brush_coord_difference.length();
|
||||
Canvas.brush_outline.scale.set(radius, radius, radius);
|
||||
|
||||
// z fighting
|
||||
let z_fight_offset = Preview.selected.calculateControlScale(brush_coord) / 8;
|
||||
Canvas.brush_outline.position.addScaledVector(world_normal, z_fight_offset);
|
||||
|
||||
// rotation
|
||||
Canvas.brush_outline.quaternion.setFromUnitVectors(new THREE.Vector3(0, 0, 1), intersect.face.normal);
|
||||
|
||||
Canvas.brush_outline.rotation.z = 0;
|
||||
let inverse = Reusable.quat2.copy(Canvas.brush_outline.quaternion).invert();
|
||||
brush_coord_difference.applyQuaternion(inverse);
|
||||
let rotation = Math.atan2(brush_coord_difference.x, -brush_coord_difference.y);
|
||||
Canvas.brush_outline.rotation.z = rotation;
|
||||
|
||||
|
||||
Canvas.brush_outline.quaternion.premultiply(world_quaternion);
|
||||
}
|
||||
}
|
||||
}
|
||||
mouseup(event) {
|
||||
@ -1374,12 +1428,12 @@ class Preview {
|
||||
}
|
||||
},
|
||||
'preview_checkerboard',
|
||||
'_',
|
||||
new MenuSeparator('reference_images'),
|
||||
'add_reference_image',
|
||||
'reference_image_from_clipboard',
|
||||
'toggle_all_reference_images',
|
||||
'edit_reference_images',
|
||||
'_',
|
||||
new MenuSeparator('controls'),
|
||||
'focus_on_selection',
|
||||
{icon: 'add_a_photo', name: 'menu.preview.save_angle', condition(preview) {return !ReferenceImageMode.active && !Modes.display}, click(preview) {
|
||||
preview.newAnglePreset()
|
||||
@ -1428,7 +1482,7 @@ class Preview {
|
||||
{icon: (preview) => (preview.isOrtho ? 'check_box' : 'check_box_outline_blank'), name: 'menu.preview.orthographic', click: function(preview) {
|
||||
preview.setProjectionMode(!preview.isOrtho, true);
|
||||
}},
|
||||
'_',
|
||||
new MenuSeparator('interface'),
|
||||
'split_screen',
|
||||
{icon: 'fullscreen', name: 'menu.preview.maximize', condition: function(preview) {return Preview.split_screen.enabled && !ReferenceImageMode.active && !Modes.display}, click: function(preview) {
|
||||
preview.fullscreen();
|
||||
|
@ -625,6 +625,7 @@ class ReferenceImage {
|
||||
}
|
||||
}
|
||||
ReferenceImage.prototype.menu = new Menu([
|
||||
new MenuSeparator('settings'),
|
||||
{
|
||||
id: 'visibility',
|
||||
name: 'reference_image.visibility',
|
||||
@ -707,7 +708,7 @@ ReferenceImage.prototype.menu = new Menu([
|
||||
return children;
|
||||
}
|
||||
},
|
||||
'_',
|
||||
new MenuSeparator('manage'),
|
||||
{
|
||||
name: 'menu.texture.refresh',
|
||||
icon: 'refresh',
|
||||
@ -718,7 +719,7 @@ ReferenceImage.prototype.menu = new Menu([
|
||||
}
|
||||
},
|
||||
'delete',
|
||||
'_',
|
||||
new MenuSeparator('properties'),
|
||||
{
|
||||
name: 'menu.texture.properties',
|
||||
icon: 'list',
|
||||
@ -962,7 +963,7 @@ BARS.defineActions(function() {
|
||||
if (reference.selected) BarItems.delete.trigger();
|
||||
}
|
||||
},
|
||||
'_',
|
||||
new MenuSeparator('properties'),
|
||||
/** Todo: add options
|
||||
* Center
|
||||
*/
|
||||
|
@ -322,6 +322,7 @@ Interface.definePanels(() => {
|
||||
}
|
||||
},
|
||||
menu: new Menu([
|
||||
new MenuSeparator('options'),
|
||||
{
|
||||
id: 'lock_palette',
|
||||
name: 'menu.palette.lock_palette',
|
||||
@ -331,7 +332,7 @@ Interface.definePanels(() => {
|
||||
StateMemory.save('color_palette_locked');
|
||||
}
|
||||
},
|
||||
'_',
|
||||
new MenuSeparator('file'),
|
||||
'sort_palette',
|
||||
'save_palette',
|
||||
'load_palette',
|
||||
|
@ -2086,6 +2086,11 @@ BARS.defineActions(function() {
|
||||
onChange() {
|
||||
BARS.updateConditions();
|
||||
UVEditor.vue.brush_type = this.value;
|
||||
let img = Canvas.brush_outline.material.map.image;
|
||||
switch (this.value) {
|
||||
case 'square': img.src = 'assets/brush_outline.png'; break;
|
||||
case 'circle': img.src = 'assets/brush_outline_circle.png'; break;
|
||||
}
|
||||
},
|
||||
icon_mode: true,
|
||||
options: {
|
||||
@ -2222,6 +2227,7 @@ BARS.defineActions(function() {
|
||||
highlightMirrorPaintingAxes();
|
||||
},
|
||||
side_menu: new Menu('mirror_painting', [
|
||||
new MenuSeparator('options'),
|
||||
// Enabled
|
||||
{
|
||||
name: 'menu.mirror_painting.enabled',
|
||||
@ -2239,7 +2245,7 @@ BARS.defineActions(function() {
|
||||
{name: 'Z', icon: () => Painter.mirror_painting_options.axis.z, color: 'z', click() {toggleMirrorPaintingAxis('z')}},
|
||||
]
|
||||
},
|
||||
'_',
|
||||
new MenuSeparator('space'),
|
||||
// Global
|
||||
{
|
||||
name: 'menu.mirror_painting.global',
|
||||
@ -2254,7 +2260,7 @@ BARS.defineActions(function() {
|
||||
icon: () => !!Painter.mirror_painting_options.local,
|
||||
click() {toggleMirrorPaintingSpace('local')}
|
||||
},
|
||||
'_',
|
||||
new MenuSeparator('texture'),
|
||||
// Texture
|
||||
{
|
||||
name: 'menu.mirror_painting.texture',
|
||||
@ -2298,7 +2304,7 @@ BARS.defineActions(function() {
|
||||
}
|
||||
}
|
||||
},
|
||||
'_',
|
||||
new MenuSeparator('animated_texture'),
|
||||
// Animated Texture Frames
|
||||
{
|
||||
name: 'menu.mirror_painting.texture_frames',
|
||||
|
@ -1446,22 +1446,23 @@ class Texture {
|
||||
}
|
||||
}
|
||||
Texture.prototype.menu = new Menu([
|
||||
new MenuSeparator('apply'),
|
||||
{
|
||||
icon: 'crop_original',
|
||||
name: 'menu.texture.face',
|
||||
condition() {return !Project.single_texture && Outliner.selected.length > 0},
|
||||
condition() {return !Format.single_texture && Outliner.selected.length > 0},
|
||||
click(texture) {texture.apply()}
|
||||
},
|
||||
{
|
||||
icon: 'texture',
|
||||
name: 'menu.texture.blank',
|
||||
condition() {return !Project.single_texture && Outliner.selected.length > 0},
|
||||
condition() {return !Format.single_texture && Outliner.selected.length > 0},
|
||||
click(texture) {texture.apply('blank')}
|
||||
},
|
||||
{
|
||||
icon: 'fa-cube',
|
||||
name: 'menu.texture.elements',
|
||||
condition() {return !Project.single_texture && Outliner.selected.length > 0},
|
||||
condition() {return !Format.single_texture && Outliner.selected.length > 0},
|
||||
click(texture) {texture.apply(true)}
|
||||
},
|
||||
{
|
||||
@ -1476,7 +1477,7 @@ class Texture {
|
||||
}
|
||||
}
|
||||
},
|
||||
'_',
|
||||
new MenuSeparator('settings'),
|
||||
{
|
||||
icon: 'list',
|
||||
name: 'menu.texture.render_mode',
|
||||
@ -1525,10 +1526,10 @@ class Texture {
|
||||
Undo.finishEdit('Merged textures')
|
||||
}
|
||||
},
|
||||
'_',
|
||||
new MenuSeparator('copypaste'),
|
||||
'copy',
|
||||
'duplicate',
|
||||
'_',
|
||||
new MenuSeparator('edit'),
|
||||
{
|
||||
icon: 'edit',
|
||||
name: 'menu.texture.edit_externally',
|
||||
@ -1556,21 +1557,23 @@ class Texture {
|
||||
name: 'menu.texture.edit',
|
||||
condition: {modes: ['paint']},
|
||||
children: [
|
||||
new MenuSeparator('adjustment'),
|
||||
'adjust_brightness_contrast',
|
||||
'adjust_saturation_hue',
|
||||
'adjust_opacity',
|
||||
'invert_colors',
|
||||
'adjust_curves',
|
||||
'_',
|
||||
new MenuSeparator('filters'),
|
||||
'limit_to_palette',
|
||||
'clear_unused_texture_space',
|
||||
'_',
|
||||
new MenuSeparator('transform'),
|
||||
'flip_texture_x',
|
||||
'flip_texture_y',
|
||||
'rotate_texture_cw',
|
||||
'rotate_texture_ccw'
|
||||
]
|
||||
},
|
||||
new MenuSeparator('file'),
|
||||
{
|
||||
icon: 'folder',
|
||||
name: 'menu.texture.folder',
|
||||
@ -1594,7 +1597,7 @@ class Texture {
|
||||
condition: tex => tex.render_mode == 'emissive',
|
||||
click(texture) {texture.exportEmissionMap()}
|
||||
},
|
||||
'_',
|
||||
new MenuSeparator('manage'),
|
||||
{
|
||||
icon: 'refresh',
|
||||
name: 'menu.texture.refresh',
|
||||
@ -1607,7 +1610,7 @@ class Texture {
|
||||
click(texture) { texture.reopen()}
|
||||
},
|
||||
'delete',
|
||||
'_',
|
||||
new MenuSeparator('properties'),
|
||||
{
|
||||
icon: 'list',
|
||||
name: 'menu.texture.properties',
|
||||
@ -2228,7 +2231,9 @@ Interface.definePanels(function() {
|
||||
}
|
||||
},
|
||||
menu: new Menu([
|
||||
new MenuSeparator('copypaste'),
|
||||
'paste',
|
||||
new MenuSeparator('file'),
|
||||
'import_texture',
|
||||
'create_texture',
|
||||
'change_textures_folder',
|
||||
|
@ -1304,6 +1304,7 @@ const UVEditor = {
|
||||
|
||||
|
||||
menu: new Menu([
|
||||
new MenuSeparator('interface'),
|
||||
{name: 'menu.view.zoom', id: 'zoom', icon: 'search', children: [
|
||||
'zoom_in',
|
||||
'zoom_out',
|
||||
@ -1328,11 +1329,11 @@ const UVEditor = {
|
||||
'painting_grid',
|
||||
'uv_checkerboard',
|
||||
'paint_mode_uv_overlay',
|
||||
'_',
|
||||
new MenuSeparator('copypaste'),
|
||||
'copy',
|
||||
'paste',
|
||||
'cube_uv_mode',
|
||||
'_',
|
||||
new MenuSeparator('uv'),
|
||||
{
|
||||
name: 'menu.uv.export',
|
||||
icon: () => UVEditor.getReferenceFace()?.enabled !== false ? 'check_box' : 'check_box_outline_blank',
|
||||
@ -1346,6 +1347,7 @@ const UVEditor = {
|
||||
'uv_maximize',
|
||||
'uv_auto',
|
||||
'uv_rel_auto',
|
||||
'uv_project_from_view',
|
||||
'connect_uv_faces',
|
||||
'merge_uv_vertices',
|
||||
'snap_uv_to_pixels',
|
||||
@ -1399,7 +1401,7 @@ const UVEditor = {
|
||||
Undo.finishEdit('Flip UV');
|
||||
}
|
||||
},
|
||||
'_',
|
||||
new MenuSeparator('face_options'),
|
||||
'face_tint',
|
||||
{icon: 'flip_to_back', condition: () => (Format.java_face_properties && Cube.selected.length && UVEditor.getReferenceFace()), name: 'action.cullface' , children: function() {
|
||||
let off = 'radio_button_unchecked';
|
||||
@ -1424,7 +1426,7 @@ const UVEditor = {
|
||||
'auto_cullface'
|
||||
]
|
||||
}},
|
||||
{icon: 'collections', name: 'menu.uv.texture', condition: () => UVEditor.getReferenceFace() && !Project.single_texture, children: function() {
|
||||
{icon: 'collections', name: 'menu.uv.texture', condition: () => UVEditor.getReferenceFace() && !Format.single_texture, children: function() {
|
||||
let arr = [
|
||||
{icon: 'crop_square', name: 'menu.cube.texture.blank', click: function(context, event) {
|
||||
let elements = UVEditor.vue.mappable_elements;
|
||||
@ -1523,6 +1525,69 @@ BARS.defineActions(function() {
|
||||
Undo.finishEdit('Auto UV')
|
||||
}
|
||||
})
|
||||
new Action('uv_project_from_view', {
|
||||
icon: 'view_in_ar',
|
||||
category: 'uv',
|
||||
condition: () => (UVEditor.isFaceUV() && Mesh.selected.length),
|
||||
click(event) {
|
||||
Undo.initEdit({elements: Mesh.selected, uv_only: true})
|
||||
|
||||
let preview = Preview.selected;
|
||||
let vector = new THREE.Vector3();
|
||||
|
||||
function projectPoint(vector) {
|
||||
let widthHalf = 0.5 * preview.canvas.width / window.devicePixelRatio;
|
||||
let heightHalf = 0.5 * preview.canvas.height / window.devicePixelRatio;
|
||||
vector.project(preview.camera);
|
||||
return [
|
||||
( vector.x * widthHalf ) + widthHalf,
|
||||
-( vector.y * heightHalf ) + heightHalf
|
||||
]
|
||||
}
|
||||
Mesh.selected.forEach(mesh => {
|
||||
let scale = preview.calculateControlScale(mesh.getWorldCenter()) / 14;
|
||||
let vertices = {};
|
||||
let min = [Infinity, Infinity];
|
||||
let max = [-Infinity, -Infinity];
|
||||
let previous_origin = [0, 0];
|
||||
let face_count = 0;
|
||||
|
||||
for (let fkey in mesh.faces) {
|
||||
if (!UVEditor.selected_faces.includes(fkey)) continue;
|
||||
mesh.faces[fkey].vertices.forEach(vkey => {
|
||||
if (vertices[vkey]) return;
|
||||
|
||||
vertices[vkey] = projectPoint( mesh.mesh.localToWorld(vector.fromArray(mesh.vertices[vkey])) );
|
||||
for (let i of [0, 1]) {
|
||||
vertices[vkey][i] *= scale;
|
||||
min[i] = Math.min(min[i], vertices[vkey][i]);
|
||||
max[i] = Math.max(max[i], vertices[vkey][i]);
|
||||
previous_origin[i] += mesh.faces[fkey].uv[vkey][i];
|
||||
}
|
||||
face_count++;
|
||||
})
|
||||
}
|
||||
|
||||
previous_origin.V2_divide(face_count);
|
||||
let offset = previous_origin.map((previous, i) => {
|
||||
let difference = previous - Math.lerp(min[i], max[i], 0.5);
|
||||
return Math.clamp(difference, -min[1], max[1]);
|
||||
})
|
||||
|
||||
for (let fkey in mesh.faces) {
|
||||
if (!UVEditor.selected_faces.includes(fkey)) continue;
|
||||
mesh.faces[fkey].vertices.forEach(vkey => {
|
||||
mesh.faces[fkey].uv[vkey][0] = vertices[vkey][0] + offset[0];
|
||||
mesh.faces[fkey].uv[vkey][1] = vertices[vkey][1] + offset[1];
|
||||
})
|
||||
}
|
||||
mesh.preview_controller.updateUV(mesh);
|
||||
})
|
||||
|
||||
UVEditor.loadData();
|
||||
Undo.finishEdit('Auto UV')
|
||||
}
|
||||
})
|
||||
new Action('uv_rel_auto', {
|
||||
icon: 'brightness_auto',
|
||||
category: 'uv',
|
||||
@ -1916,6 +1981,7 @@ Interface.definePanels(function() {
|
||||
'uv_apply_all',
|
||||
'uv_maximize',
|
||||
'uv_auto',
|
||||
'uv_project_from_view',
|
||||
'uv_transparent',
|
||||
'uv_mirror_x',
|
||||
'uv_mirror_y',
|
||||
@ -2040,13 +2106,6 @@ Interface.definePanels(function() {
|
||||
}
|
||||
},
|
||||
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();
|
||||
@ -3252,7 +3311,7 @@ Interface.definePanels(function() {
|
||||
</div>
|
||||
|
||||
|
||||
<div id="uv_face_properties" v-if="mode === 'face_properties' && mappable_elements[0] && mappable_elements[0].type == 'cube'">
|
||||
<div id="uv_face_properties" v-if="mode === 'face_properties'">
|
||||
<div class="bar" id="face_properties_header_bar">
|
||||
<li></li>
|
||||
<li @click="mode = 'uv'" class="tool face_properties_toggle">
|
||||
@ -3287,7 +3346,7 @@ Interface.definePanels(function() {
|
||||
</div>
|
||||
|
||||
|
||||
<ul>
|
||||
<ul v-if="mappable_elements[0] && mappable_elements[0].type == 'cube'">
|
||||
<li v-for="(face, key) in mappable_elements[0].faces" :face="key"
|
||||
class="uv_face_properties_line"
|
||||
:class="{selected: selected_faces.includes(key), disabled: mappable_elements[0].faces[key].texture === null}"
|
||||
|
@ -21,6 +21,7 @@ class UndoSystem {
|
||||
}
|
||||
this.startChange(amended);
|
||||
this.current_save = new UndoSystem.save(aspects)
|
||||
Blockbench.dispatchEvent('init_edit', {aspects, amended, save: this.current_save})
|
||||
return this.current_save;
|
||||
}
|
||||
finishEdit(action, aspects) {
|
||||
|
@ -113,8 +113,14 @@ async function loadInfoFromURL() {
|
||||
}
|
||||
|
||||
if (Blockbench.queries.m) {
|
||||
$.getJSON(`https://blckbn.ch/api/models/${Blockbench.queries.m}`, (model) => {
|
||||
$.getJSON(`https://blckbn.ch/api/models/${Blockbench.queries.m}`, (model, b) => {
|
||||
Codecs.project.load(model, {path: ''});
|
||||
}).fail(() => {
|
||||
Blockbench.showMessageBox({
|
||||
title: 'message.invalid_link',
|
||||
message: tl('message.invalid_link.message', ['`'+Blockbench.queries.m+'`']),
|
||||
icon: 'running_with_errors'
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
30
lang/en.json
30
lang/en.json
@ -244,10 +244,15 @@
|
||||
"message.child_model_only.open": "Open Parent",
|
||||
"message.child_model_only.open_with_textures": "Open Parent & Adopt Textures",
|
||||
"message.invalid_link": "Invalid or Expired Model Link",
|
||||
"message.invalid_link.message": "The shared model \"%0\" that you are trying to load is either invalid or has expired.",
|
||||
"message.delete_reference_image": "Are you sure you want to delete this reference image? This cannot be undone.",
|
||||
"message.add_reference_image.message": "Select where to load the reference image",
|
||||
"message.add_reference_image.project": "Add to this project",
|
||||
"message.add_reference_image.app": "Add to all projects",
|
||||
"message.plugin_dependencies.title": "Plugin Dependencies",
|
||||
"message.plugin_dependencies.message1": "The plugin requires these dependencies:",
|
||||
"message.plugin_dependencies.message2": "Do you want to install them to continue?",
|
||||
"message.plugin_dependencies.invalid": "Unfortunately the plugin cannot be installed, it has an invalid dependency:",
|
||||
|
||||
"message.unsaved_textures.title": "Unsaved Textures",
|
||||
"message.unsaved_textures.message": "Your model has unsaved textures. Make sure to save them and paste them into your resource pack in the correct folder.",
|
||||
@ -387,7 +392,9 @@
|
||||
"dialog.project.shadow_size": "Shadow Size",
|
||||
|
||||
"dialog.convert_project.title": "Convert Project",
|
||||
"dialog.convert_project.text": "Are you sure you want to convert this project? You cannot undo this step.",
|
||||
"dialog.convert_project.text1": "Convert the project into a different format.",
|
||||
"dialog.convert_project.text2": "Use conversion at your own risk. Formats have different features and restrictions, so converting will in some cases break parts of your project.",
|
||||
"dialog.convert_project.text3": "You cannot undo converting, that is why by default a copy will be created.",
|
||||
"dialog.convert_project.current_format": "Current Format",
|
||||
"dialog.convert_project.create_copy": "Create Copy",
|
||||
|
||||
@ -524,7 +531,11 @@
|
||||
"dialog.plugins.web_only": "Only for the web app",
|
||||
"dialog.plugins.app_only": "Only for the desktop app",
|
||||
"dialog.plugins.author": "by %0",
|
||||
"dialog.plugins.show_less": "Show Less",
|
||||
"dialog.plugins.is_installed": "Installed",
|
||||
"dialog.plugins.is_disabled": "Disabled",
|
||||
"dialog.plugins.disable": "Disable",
|
||||
"dialog.plugins.enable": "Enable",
|
||||
"dialog.plugins.dependencies": "Depends on:",
|
||||
|
||||
"dialog.load_plugins_from_query.title": "Load Plugins",
|
||||
"dialog.load_plugins_from_query.text": "You used a link that requires plugins to be installed. Would you like to install them?",
|
||||
@ -861,8 +872,10 @@
|
||||
"settings.undo_limit.desc": "Number of steps you can undo",
|
||||
"settings.local_move": "Move on Relative Axes",
|
||||
"settings.local_move.desc": "Move rotated elements on their own axes if possible",
|
||||
"settings.canvas_unselect": "Canvas Click Unselect",
|
||||
"settings.canvas_unselect.desc": "Unselects all elements when clicking on the canvas background",
|
||||
"settings.canvas_unselect": "Unselect when Clicking Background",
|
||||
"settings.canvas_unselect.desc": "Unselects all elements when clicking on the background behind the model",
|
||||
"settings.double_click_switch_tools": "Switch Tools on Double Click",
|
||||
"settings.double_click_switch_tools.desc": "Double click the viewport to switch between tools",
|
||||
"settings.highlight_cubes": "Highlight Elements",
|
||||
"settings.highlight_cubes.desc": "Highlight elements when you hover over them or select them",
|
||||
"settings.deactivate_size_limit": "Deactivate Size Limit",
|
||||
@ -1143,7 +1156,7 @@
|
||||
"action.export_bedrock": "Export Bedrock Geometry",
|
||||
"action.export_bedrock.desc": "Export the model as a bedrock edition geometry file.",
|
||||
"action.export_entity": "Export Bedrock Entity",
|
||||
"action.export_entity.desc": "Add the current model as an entity to a mobs.json file",
|
||||
"action.export_entity.desc": "Export the model as an entity model for Minecraft Bedrock Edition",
|
||||
"action.export_class_entity": "Export Java Entity",
|
||||
"action.export_class_entity.desc": "Export the entity model as a Java class",
|
||||
"action.import_optifine_part": "Import OptiFine Part",
|
||||
@ -1348,6 +1361,10 @@
|
||||
"action.update_autouv.desc": "Update the auto UV mapping of the selected cubes",
|
||||
"action.edit_material_instances": "Edit Material Instances",
|
||||
"action.edit_material_instances.desc": "Edit material instance names for bedrock block geometries",
|
||||
"action.mirror_modeling": "Mirror Modeling",
|
||||
"action.mirror_modeling.desc": "Enable Mirror Modeling on the X axis. All changes you make in the viewport will be reflected to the other side, unless specifically disabled on the element.",
|
||||
"action.allow_element_mirror_modeling": "Allow Mirror Modeling",
|
||||
"action.allow_element_mirror_modeling.desc": "Choose whether the selected elements can be affected by mirror modeling",
|
||||
"action.selection_mode": "Selection Mode",
|
||||
"action.selection_mode.desc": "Change how elements can be selected in the viewport",
|
||||
"action.selection_mode.object": "Object",
|
||||
@ -1533,6 +1550,8 @@
|
||||
"action.uv_auto.desc": "Sets the UV size of this face to the real size of the face",
|
||||
"action.uv_rel_auto": "Relative Auto UV",
|
||||
"action.uv_rel_auto.desc": "Sets the UV of this face to the position and size of the actual face",
|
||||
"action.uv_project_from_view": "UV Project from View",
|
||||
"action.uv_project_from_view.desc": "Automatically generate UVs for the selected faces from the perspective of the 3D view",
|
||||
"action.uv_mirror_x": "UV Mirror X",
|
||||
"action.uv_mirror_x.desc": "Mirrors the UV of this face on the X axis",
|
||||
"action.uv_mirror_y": "UV Mirror Y",
|
||||
@ -1884,6 +1903,7 @@
|
||||
|
||||
"edit.loop_cut.direction": "Direction",
|
||||
"edit.loop_cut.offset": "Offset",
|
||||
"edit.loop_cut.cuts": "Cuts",
|
||||
"edit.extrude_mesh_selection.extend": "Extend",
|
||||
|
||||
"web.download_app": "Download App",
|
||||
|
26
package-lock.json
generated
26
package-lock.json
generated
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Blockbench",
|
||||
"version": "4.7.4",
|
||||
"version": "4.8.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@ -2410,9 +2410,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"cacheable-request": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz",
|
||||
"integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==",
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
|
||||
"integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"clone-response": "^1.0.2",
|
||||
@ -2823,9 +2823,9 @@
|
||||
}
|
||||
},
|
||||
"electron": {
|
||||
"version": "24.1.1",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-24.1.1.tgz",
|
||||
"integrity": "sha512-ymjUMe6Pvh9ytpM4lOvr+Qxd6NG5AELRtR6tw54bK3FXfKtTTKKAtZw/NbwHwkRAlWu8FNAGOuvCoap6/bm9LQ==",
|
||||
"version": "25.2.0",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-25.2.0.tgz",
|
||||
"integrity": "sha512-I/rhcW2sV2fyiveVSBr2N7v5ZiCtdGY0UiNCDZgk2fpSC+irQjbeh7JT2b4vWmJ2ogOXBjqesrN9XszTIG6DHg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@electron/get": "^2.0.0",
|
||||
@ -2834,9 +2834,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": {
|
||||
"version": "18.15.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
|
||||
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==",
|
||||
"version": "18.16.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.18.tgz",
|
||||
"integrity": "sha512-/aNaQZD0+iSBAGnvvN2Cx92HqE5sZCPZtx2TsK+4nvV23fFe09jVDvpArXr2j9DnYlzuU9WuoykDDc6wqvpNcw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
@ -3542,9 +3542,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz",
|
||||
"integrity": "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==",
|
||||
"version": "7.5.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz",
|
||||
"integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "Blockbench",
|
||||
"description": "Low-poly modeling and animation software",
|
||||
"version": "4.7.4",
|
||||
"version": "4.8.0",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"author": {
|
||||
"name": "JannisX11",
|
||||
@ -106,7 +106,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"blockbench-types": "^4.6.1",
|
||||
"electron": "^24.1.1",
|
||||
"electron": "^25.2.0",
|
||||
"electron-builder": "^23.6.0",
|
||||
"electron-notarize": "^1.0.0",
|
||||
"webpack": "^5.74.0",
|
||||
|
Loading…
Reference in New Issue
Block a user