Merge branch 'next' into preview-scenes-2

This commit is contained in:
JannisX11 2023-07-04 20:12:19 +02:00
commit 5020dd3a56
49 changed files with 1894 additions and 461 deletions

BIN
assets/brush_outline.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 B

BIN
assets/plugins.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: () => {

View File

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

View File

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

View File

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

View File

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

View File

@ -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: `{

View File

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

View File

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

View File

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

View 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();
}
})
})

View File

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

View File

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

View File

@ -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', {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@ -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": {

View File

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