Merge branch 'master' into next

This commit is contained in:
JannisX11 2024-09-07 21:24:08 +02:00
commit a5c138b302
45 changed files with 1569 additions and 1636 deletions

View File

@ -29,6 +29,7 @@ body:
id: format
attributes:
label: Model format in which the issue occurs
description: Format means which option you choose when creating a new model. Not the file extension.
validations:
required: true
- type: dropdown

View File

@ -628,7 +628,7 @@
}
.settings_list li .setting_icon i {
font-size: 26pt;
width: 34px;
max-width: unset;
margin-top: -6px;
}
.settings_list li:hover .setting_icon i {
@ -2488,6 +2488,9 @@
object-fit: contain;
image-rendering: auto;
}
#tab_overview_grid > li.pixel_art img {
image-rendering: pixelated;
}
#tab_overview_grid > li label {
cursor: inherit;
}

View File

@ -451,6 +451,7 @@
cursor: default;
float: left;
color: var(--color-text);
flex-shrink: 0;
}
.tool i {
display: block;

View File

@ -1367,7 +1367,7 @@
width: 144px;
flex-shrink: 0;
background-color: var(--color-back);
z-index: 4;
z-index: 6;
border-bottom: 1px solid var(--color-border);
height: calc(100% + 1px);
}

View File

@ -299,6 +299,7 @@
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
overflow: hidden;
flex-shrink: 0;
}
i.fa_big {
font-size: 18px;

View File

@ -21,6 +21,7 @@
}
#start_screen button {
margin-right: 4px;
margin-top: 4px;
}
#start_screen .recent_project {
margin: 2px 0;

View File

@ -1049,6 +1049,7 @@
z-index: 9;
pointer-events: initial;
cursor: move;
clip-path: none !important;
}
.reference_image.selected, .reference_image:hover {
outline: 1px solid var(--color-accent);

View File

@ -299,8 +299,7 @@
<div id="right_bar" class="sidebar"></div>
<div id="center">
<ul id="toast_notification_list">
</ul>
<ul id="toast_notification_list"></ul>
<div id="top_slot"></div>
<div id="preview">

View File

@ -829,7 +829,7 @@ class Animation extends AnimationItem {
icon: 'folder',
condition(animation) {return isApp && Format.animation_files && animation.path && fs.existsSync(animation.path)},
click(animation) {
shell.showItemInFolder(animation.path);
showItemInFolder(animation.path);
}
},
'rename',
@ -2139,180 +2139,4 @@ Interface.definePanels(function() {
'save_all_animations',
])
})
new Panel('variable_placeholders', {
icon: 'fas.fa-stream',
condition: {modes: ['animate']},
growable: true,
resizable: true,
default_position: {
slot: 'left_bar',
float_position: [0, 0],
float_size: [300, 400],
height: 400
},
component: {
name: 'panel-placeholders',
components: {VuePrismEditor},
data() { return {
text: '',
buttons: []
}},
methods: {
updateButtons() {
let old_values = {};
this.buttons.forEach(b => old_values[b.id] = b.value);
this.buttons.empty();
let text = this.text//.toLowerCase();
let matches = text.matchAll(/(slider|toggle|impulse)\(.+\)/gi);
for (let match of matches) {
let [type, content] = match[0].substring(0, match[0].length - 1).split(/\(/);
let [id, ...args] = content.split(/\(|, */);
id = id.replace(/['"]/g, '');
if (this.buttons.find(b => b.id == id)) return;
let variable = text.substring(0, match.index).match(/[\w.-]+ *= *$/);
variable = variable ? variable[0].replace(/[ =]+/g, '').replace(/^v\./i, 'variable.').replace(/^q\./i, 'query.').replace(/^t\./i, 'temp.').replace(/^c\./i, 'context.') : undefined;
if (type == 'slider') {
this.buttons.push({
type,
id,
value: old_values[id] || 0,
variable,
step: isNaN(args[0]) ? undefined : parseFloat(args[0]),
min: isNaN(args[1]) ? undefined : parseFloat(args[1]),
max: isNaN(args[2]) ? undefined : parseFloat(args[2])
})
} else if (type == 'toggle') {
this.buttons.push({
type,
id,
value: old_values[id] || 0,
variable,
})
} else if (type == 'impulse') {
this.buttons.push({
type,
id,
value: 0,
variable,
duration: parseFloat(args[0]) || 0.1
})
}
}
},
changeButtonValue(button, event) {
if (button.type == 'toggle') {
button.value = event.target.checked ? 1 : 0;
}
if (button.type == 'impulse') {
button.value = 1;
setTimeout(() => {
button.value = 0;
}, Math.clamp(button.duration, 0, 1) * 1000);
}
if (button.variable) {
delete Animator.MolangParser.variables[button.variable];
}
Animator.preview();
},
slideButton(button, e1) {
convertTouchEvent(e1);
let last_event = e1;
let started = false;
let move_calls = 0;
let last_val = 0;
let total = 0;
let clientX = e1.clientX;
function start() {
started = true;
if (!e1.touches && last_event == e1 && e1.target.requestPointerLock) e1.target.requestPointerLock();
}
function move(e2) {
convertTouchEvent(e2);
if (!started && Math.abs(e2.clientX - e1.clientX) > 5) {
start()
}
if (started) {
if (e1.touches) {
clientX = e2.clientX;
} else {
let limit = move_calls <= 2 ? 1 : 100;
clientX += Math.clamp(e2.movementX, -limit, limit);
}
let val = Math.round((clientX - e1.clientX) / 45);
let difference = (val - last_val);
if (!difference) return;
if (button.step) {
difference *= button.step;
} else {
difference *= canvasGridSize(e2.shiftKey || Pressing.overrides.shift, e2.ctrlOrCmd || Pressing.overrides.ctrl);
}
button.value = Math.clamp(Math.roundTo((parseFloat(button.value) || 0) + difference, 4), button.min, button.max);
last_val = val;
last_event = e2;
total += difference;
move_calls++;
Animator.preview()
Blockbench.setStatusBarText(trimFloatNumber(total));
}
}
function off(e2) {
if (document.exitPointerLock) document.exitPointerLock()
removeEventListeners(document, 'mousemove touchmove', move);
removeEventListeners(document, 'mouseup touchend', off);
}
addEventListeners(document, 'mouseup touchend', off);
addEventListeners(document, 'mousemove touchmove', move);
},
autocomplete(text, position) {
let test = MolangAutocomplete.VariablePlaceholdersContext.autocomplete(text, position);
return test;
}
},
watch: {
text(text) {
if (Project && typeof text == 'string') {
Project.variable_placeholders = text;
this.updateButtons();
Project.variable_placeholder_buttons.replace(this.buttons);
}
}
},
template: `
<div style="flex-grow: 1; display: flex; flex-direction: column; overflow: visible;">
<ul id="placeholder_buttons">
<li v-for="button in buttons" :key="button.id" :class="{placeholder_slider: button.type == 'slider'}" @click="button.type == 'impulse' && changeButtonValue(button, $event)" :buttontype="button.type">
<i v-if="button.type == 'impulse'" class="material-icons">play_arrow</i>
<input v-if="button.type == 'toggle'" type="checkbox" class="tab_target" :value="button.value == 1" @change="changeButtonValue(button, $event)" :id="'placeholder_button_'+button.id">
<numeric-input v-if="button.type == 'slider'" class="dark_bordered tab_target" :step="button.step" :min="button.min" :max="button.max" v-model="button.value" @input="changeButtonValue(button, $event)" />
<label :for="'placeholder_button_'+button.id" @mousedown="slideButton(button, $event)" @touchstart="slideButton(button, $event)">{{ button.id }}</label>
</li>
</ul>
<p>${tl('panel.variable_placeholders.info')}</p>
<vue-prism-editor
id="var_placeholder_area"
class="molang_input tab_target capture_tab_key"
v-model="text"
language="molang"
:autocomplete="autocomplete"
:line-numbers="false"
style="flex-grow: 1;"
onkeyup="Animator.preview()"
/>
</div>
`
}
})
})

View File

@ -859,7 +859,7 @@ class AnimationController extends AnimationItem {
icon: 'folder',
condition(animation) {return isApp && Format.animation_files && animation.path && fs.existsSync(animation.path)},
click(animation) {
shell.showItemInFolder(animation.path);
showItemInFolder(animation.path);
}
},
'rename',

View File

@ -1431,6 +1431,7 @@ Interface.definePanels(function() {
if (document.exitPointerLock) document.exitPointerLock()
removeEventListeners(document, 'mousemove touchmove', move);
removeEventListeners(document, 'mouseup touchend', off);
Blockbench.setStatusBarText();
}
addEventListeners(document, 'mouseup touchend', off);
addEventListeners(document, 'mousemove touchmove', move);

View File

@ -589,6 +589,7 @@ class Keyframe {
function updateKeyframeValue(axis, value, data_point) {
Timeline.selected.forEach(function(kf) {
if (axis == 'uniform' && kf.channel == 'scale') kf.uniform = true;
if (data_point && !kf.data_points[data_point]) return;
kf.set(axis, value, data_point);
})
if (!['effect', 'locator', 'script'].includes(axis)) {
@ -603,8 +604,8 @@ function updateKeyframeSelection() {
}
let has_expressions = false;
if (kf.transform) {
has_expressions = !!kf.data_points.find(point => {
return !isStringNumber(point.x) || !isStringNumber(point.y) ||! isStringNumber(point.z);
has_expressions = !!kf.data_points.find((point, i) => {
return kf.getArray(i).find(v => typeof v == 'string');
})
}
if (has_expressions != kf.has_expressions) {
@ -1208,6 +1209,15 @@ BARS.defineActions(function() {
time = (time + Animation.selected.length/2) % (Animation.selected.length + 0.001);
}
time = Timeline.snapTime(time);
if (Math.epsilon(time, Animation.selected.length, 0.004) && formResult.offset && !occupied_times.includes(0)) {
// Copy keyframe to start
occupied_times.push(0);
let new_kf = opposite_animator.createKeyframe(old_kf, 0, channel, false, false)
if (new_kf) {
new_kf.flip(0);
new_keyframes.push(new_kf);
}
}
if (occupied_times.includes(time)) return;
occupied_times.push(time);
let new_kf = opposite_animator.createKeyframe(old_kf, time, channel, false, false)
@ -1462,6 +1472,17 @@ Interface.definePanels(function() {
}
}
return channel;
},
firstKeyframe() {
let data_point_length = 0;
let keyframe;
for (let kf of this.keyframes) {
if (kf.data_points.length > data_point_length) {
keyframe = kf;
data_point_length = kf.data_points.length;
}
}
return keyframe;
}
},
template: `
@ -1474,7 +1495,7 @@ Interface.definePanels(function() {
<label>{{ tl('panel.keyframe.type', [getKeyframeInfos()]) }}</label>
<div
class="in_list_button"
v-if="keyframes[0].animator.channels[channel] && keyframes[0].data_points.length < keyframes[0].animator.channels[channel].max_data_points && keyframes[0].interpolation !== 'catmullrom'"
v-if="firstKeyframe.animator.channels[channel] && firstKeyframe.data_points.length < firstKeyframe.animator.channels[channel].max_data_points && firstKeyframe.interpolation !== 'catmullrom'"
v-on:click.stop="addDataPoint()"
title="${ tl('panel.keyframe.change_effect_file') }"
>
@ -1482,19 +1503,19 @@ Interface.definePanels(function() {
</div>
</div>
<ul class="list" :style="{overflow: keyframes[0].data_points.length > 1 ? 'auto' : 'visible'}">
<ul class="list" :style="{overflow: firstKeyframe.data_points.length > 1 ? 'auto' : 'visible'}">
<div v-for="(data_point, data_point_i) of keyframes[0].data_points" class="keyframe_data_point">
<div v-for="(data_point, data_point_i) of firstKeyframe.data_points" class="keyframe_data_point">
<div class="keyframe_data_point_header" v-if="keyframes[0].data_points.length > 1">
<label>{{ keyframes[0].transform ? tl('panel.keyframe.' + (data_point_i ? 'post' : 'pre')) : (data_point_i + 1) }}</label>
<div class="keyframe_data_point_header" v-if="firstKeyframe.data_points.length > 1">
<label>{{ firstKeyframe.transform ? tl('panel.keyframe.' + (data_point_i ? 'post' : 'pre')) : (data_point_i + 1) }}</label>
<div class="flex_fill_line"></div>
<div class="in_list_button" v-on:click.stop="removeDataPoint(data_point_i)" title="${ tl('panel.keyframe.remove_data_point') }">
<i class="material-icons">clear</i>
</div>
</div>
<template v-if="channel == 'scale' && keyframes[0].uniform && data_point.x_string == data_point.y_string && data_point.y_string == data_point.z_string">
<template v-if="channel == 'scale' && firstKeyframe.uniform && data_point.x_string == data_point.y_string && data_point.y_string == data_point.z_string">
<div
class="bar flex"
id="keyframe_bar_uniform_scale"
@ -1541,7 +1562,7 @@ Interface.definePanels(function() {
@focus="key == 'locator' && updateLocatorSuggestionList()"
@input="updateInput(key, $event.target.value, data_point_i)"
/>
<div class="tool" v-if="key == 'effect'" :title="tl(channel == 'sound' ? 'timeline.select_sound_file' : 'timeline.select_particle_file')" @click="changeKeyframeFile(data_point, keyframes[0])">
<div class="tool" v-if="key == 'effect'" :title="tl(channel == 'sound' ? 'timeline.select_sound_file' : 'timeline.select_particle_file')" @click="changeKeyframeFile(data_point, firstKeyframe)">
<i class="material-icons">upload_file</i>
</div>
</div>

View File

@ -305,6 +305,10 @@ function loadDataFromModelMemory() {
Blockbench.dispatchEvent('load_from_recent_project_data', {data: project});
}
function showItemInFolder(path) {
ipcRenderer.send('show-item-in-folder', path);
}
//Window Controls
function updateWindowState(e, type) {
let maximized = currentwindow.isMaximized();
@ -733,11 +737,11 @@ ipcRenderer.on('update-available', (event, arg) => {
})
} else {
addStartScreenSection({
addStartScreenSection('update_notification', {
color: 'var(--color-back)',
graphic: {type: 'icon', icon: 'update'},
text: [
{type: 'h2', text: tl('message.update_notification.title')},
{type: 'h3', text: tl('message.update_notification.title')},
{text: tl('message.update_notification.message')},
{type: 'button', text: tl('generic.enable'), click: (e) => {
settings.automatic_updates.set(true);

View File

@ -1941,7 +1941,7 @@ const BARS = {
category: 'file',
condition: () => {return isApp && (Project.save_path || Project.export_path)},
click: function () {
shell.showItemInFolder(Project.export_path || Project.save_path);
showItemInFolder(Project.export_path || Project.save_path);
}
})
new Action('reload', {

View File

@ -450,7 +450,8 @@ Interface.definePanels = function(callback) {
//Misc
function unselectInterface(event) {
if (
open_menu && $('.contextMenu').find(event.target).length === 0 &&
open_menu &&
!event.target.classList.contains('contextMenu') && $('.contextMenu').find(event.target).length === 0 &&
$('.menu_bar_point.opened:hover').length === 0 &&
!document.getElementById('mobile_menu_bar')?.contains(event.target)
) {

View File

@ -470,10 +470,6 @@ const Settings = {
}});
new Setting('stretch_linked', {category: 'edit', value: true});
new Setting('auto_keyframe', {category: 'edit', value: true});
new Setting('bedrock_uv_rotations', {category: 'edit', value: false, name: 'Bedrock UV Rotations (Experimental)', description: 'Enable the experimental bedrock UV rotations feature.', onChange(value) {
Formats.bedrock.uv_rotation = value;
Formats.bedrock_block.uv_rotation = value;
}});
//Grid
new Setting('grids', {category: 'grid', value: true, onChange() {Canvas.buildGrid()}});

View File

@ -36,7 +36,7 @@ function addStartScreenSection(id, data) {
data = id;
id = '';
}
var obj = $(Interface.createElement('section', {id}))
var obj = $(Interface.createElement('section', {class: 'start_screen_section', section_id: id}))
if (typeof data.graphic === 'object') {
var left = $('<div class="start_screen_left graphic"></div>')
obj.append(left)
@ -140,9 +140,9 @@ function addStartScreenSection(id, data) {
if (data.last) {
$('#start_screen > content').append(obj);
} else if (data.insert_after) {
$('#start_screen > content').find(`#${data.insert_after}`).after(obj);
$('#start_screen > content').find(`.start_screen_section[section_id="${data.insert_after}"]`).after(obj);
} else if (data.insert_before) {
$('#start_screen > content').find(`#${data.insert_before}`).before(obj);
$('#start_screen > content').find(`.start_screen_section[section_id="${data.insert_before}"]`).before(obj);
} else {
$('#start_screen > content').prepend(obj);
}
@ -261,7 +261,7 @@ onVueSetup(async function() {
name: 'menu.texture.folder',
icon: 'folder',
click() {
shell.showItemInFolder(recent_project.path)
showItemInFolder(recent_project.path)
}
},
{
@ -375,7 +375,7 @@ onVueSetup(async function() {
template: `
<div id="start_screen">
<content>
<section id="splash_screen" v-if="show_splash_screen">
<section id="splash_screen" v-if="show_splash_screen" class="start_screen_section" section_id="splash_screen">
<div class="splash_art_slideshow_image" :style="{backgroundImage: getBackground(slideshow[slideshow_selected].source)}">
<p v-if="slideshow[slideshow_selected].description" class="start_screen_graphic_description" v-html="pureMarked(slideshow[slideshow_selected].description)"></p>
</div>
@ -387,7 +387,7 @@ onVueSetup(async function() {
<i class="material-icons start_screen_close_button" @click="show_splash_screen = false">clear</i>
</section>
<section id="start_files">
<section id="start_files" class="start_screen_section" section_id="start_files">
<div class="start_screen_left" v-if="!(selected_format_id && mobile_layout)">
<h2>${tl('mode.start.new')}</h2>
@ -581,7 +581,7 @@ ModelLoader.loaders = {};
let twitter_ad;
if (Blockbench.startup_count < 20 && Blockbench.startup_count % 5 === 4) {
twitter_ad = true;
addStartScreenSection({
addStartScreenSection('twitter_link', {
color: '#1da1f2',
text_color: '#ffffff',
graphic: {type: 'icon', icon: 'fab.fa-twitter'},
@ -594,7 +594,7 @@ ModelLoader.loaders = {};
}
//Discord
if (Blockbench.startup_count < 6 && !twitter_ad) {
addStartScreenSection({
addStartScreenSection('discord_link', {
color: '#5865F2',
text_color: '#ffffff',
graphic: {type: 'icon', icon: 'fab.fa-discord'},
@ -666,7 +666,7 @@ ModelLoader.loaders = {};
}
},
template: `
<section id="quick_setup">
<section id="quick_setup" section_id="quick_setup" class="start_screen_section">
<i class="material-icons start_screen_close_button" @click="close()">clear</i>
<h2>${tl('mode.start.quick_setup')}</h2>

View File

@ -193,6 +193,9 @@ const CustomTheme = {
},
getThemeThumbnailStyle(theme) {
let style = {};
for (let key in CustomTheme.defaultColors) {
style[`--color-${key}`] = CustomTheme.defaultColors[key];
}
for (let key in theme.colors) {
style[`--color-${key}`] = theme.colors[key];
}
@ -325,7 +328,8 @@ const CustomTheme = {
<div v-if="open_category == 'css'">
<h2 class="i_b">${tl('layout.css')}</h2>
<div id="css_editor">
<vue-prism-editor v-model="data.css" @change="customizeTheme(1, $event)" language="css" :line-numbers="true" />
<p v-if="data.css && data.css.length > 65000">Hidden due to performance limitations of the built-in CSS editor</p>
<vue-prism-editor v-else v-model="data.css" @change="customizeTheme(1, $event)" language="css" :line-numbers="true" />
</div>
</div>

View File

@ -1343,7 +1343,7 @@ var entity_format = new ModelFormat({
rotate_cubes: true,
box_uv: true,
optional_box_uv: true,
uv_rotation: settings.bedrock_uv_rotations.value,
uv_rotation: true,
single_texture: true,
bone_rig: true,
centered_grid: true,
@ -1382,7 +1382,7 @@ var block_format = new ModelFormat({
rotate_cubes: true,
box_uv: false,
optional_box_uv: true,
uv_rotation: settings.bedrock_uv_rotations.value,
uv_rotation: true,
single_texture_default: true,
bone_rig: true,
centered_grid: true,

View File

@ -229,8 +229,8 @@ function buildSkinnedMesh(root_group, scale) {
let bone = new THREE.Bone();
bone.name = group.name;
bone.uuid = group.mesh.uuid;
let parent_offset = THREE.fastWorldPosition(group.mesh.parent, Reusable.vec3);
THREE.fastWorldPosition(group.mesh, bone.position).sub(parent_offset);
bone.position.copy(group.mesh.position);
bone.rotation.copy(group.mesh.rotation)
if (group == root_group) {
bone.position.set(0, 0, 0);
}
@ -334,7 +334,6 @@ var codec = new Codec('gltf', {
Outliner.root.forEach(node => {
if (node instanceof Group) {
let armature = buildSkinnedMesh(node, options.scale);
console.log(armature)
gl_scene.add(armature);
} else {
gl_scene.add(node.mesh);

View File

@ -1838,6 +1838,252 @@ skin_presets.boat = {
]
}`
};
skin_presets.bogged = {
display_name: 'Bogged',
model: `{
"name": "bogged",
"texturewidth": 64,
"textureheight": 32,
"eyes": [
[9, 12, 2, 1],
[13, 12, 2, 1]
],
"bones": [
{
"name": "body",
"pivot": [0, 24, 0],
"cubes": [
{"origin": [-4, 12, -2], "size": [8, 12, 4], "uv": [16, 16]}
]
},
{
"name": "waist",
"pivot": [0, 12, 0]
},
{
"name": "head",
"pivot": [0, 24, 0],
"cubes": [
{"origin": [-4, 24, -4], "size": [8, 8, 8], "uv": [0, 0]}
]
},
{
"name": "mushrooms",
"parent": "head",
"pivot": [3, 31.5, 3],
"cubes": [
{"origin": [-6, 31, -3], "size": [6, 4, 0], "pivot": [-3, 32.5, -3], "rotation": [0, -45, 0], "uv": [50, 22]},
{"origin": [-6, 31, -3], "size": [6, 4, 0], "pivot": [-3, 32.5, -3], "rotation": [0, 45, 0], "uv": [50, 22]},
{"origin": [0, 31, 3], "size": [6, 4, 0], "pivot": [3, 31.5, 3], "rotation": [0, 45, 0], "uv": [50, 16]},
{"origin": [0, 31, 3], "size": [6, 4, 0], "pivot": [3, 31.5, 3], "rotation": [0, -45, 0], "uv": [50, 16]},
{"origin": [-5, 25, 3], "size": [6, 5, 0], "pivot": [-2, 25, 3], "rotation": [-90, 0, 45], "uv": [50, 27]},
{"origin": [-5, 25, 3], "size": [6, 5, 0], "pivot": [-2, 25, 3], "rotation": [-90, 0, 135], "uv": [50, 27]}
]
},
{
"name": "hat",
"pivot": [0, 24, 0],
"cubes": [
{"origin": [-4, 24, -4], "size": [8, 8, 8], "inflate": 0.2, "uv": [32, 0], "layer": true}
]
},
{
"name": "rightArm",
"pivot": [-5, 22, 0],
"cubes": [
{"origin": [-6, 12, -1], "size": [2, 12, 2], "uv": [40, 16]}
]
},
{
"name": "rightItem",
"parent": "rightArm",
"pivot": [-6, 15, 1]
},
{
"name": "leftArm",
"pivot": [5, 22, 0],
"mirror": true,
"cubes": [
{"origin": [4, 12, -1], "size": [2, 12, 2], "uv": [40, 16], "mirror": true}
]
},
{
"name": "leftItem",
"parent": "leftArm",
"pivot": [6, 15, 1]
},
{
"name": "rightLeg",
"pivot": [-2, 12, 0],
"cubes": [
{"origin": [-3, 0, -1], "size": [2, 12, 2], "uv": [0, 16]}
]
},
{
"name": "leftLeg",
"pivot": [2, 12, 0],
"mirror": true,
"cubes": [
{"origin": [1, 0, -1], "size": [2, 12, 2], "uv": [0, 16], "mirror": true}
]
}
]
}`
};
skin_presets.bogged_layer = {
display_name: 'Bogged/Stray Layer',
model: `{
"name": "bogged_layer",
"texturewidth": 64,
"textureheight": 32,
"eyes": [
[9, 12, 2, 1],
[13, 12, 2, 1]
],
"bones": [
{
"name": "body",
"pivot": [0, 24, 0],
"cubes": [
{"origin": [-4, 12, -2], "size": [8, 12, 4], "uv": [16, 16]}
]
},
{
"name": "leftArm",
"parent": "body",
"pivot": [5, 22, 0],
"mirror": true,
"cubes": [
{"origin": [4, 12, -2], "size": [4, 12, 4], "uv": [40, 16], "mirror": true}
]
},
{
"name": "head",
"pivot": [0, 24, 0],
"cubes": [
{"origin": [-4, 24, -4], "size": [8, 8, 8], "uv": [0, 0]}
]
},
{
"name": "hat",
"pivot": [0, 24, 0],
"cubes": [
{"origin": [-4, 24, -4], "size": [8, 8, 8], "inflate": 0.5, "uv": [32, 0], "layer": true, , "visibility": false}
]
},
{
"name": "rightArm",
"pivot": [-5, 22, 0],
"cubes": [
{"origin": [-8, 12, -2], "size": [4, 12, 4], "uv": [40, 16]}
]
},
{
"name": "rightLeg",
"pivot": [-1.9, 12, 0],
"cubes": [
{"origin": [-3.9, 0, -2], "size": [4, 12, 4], "uv": [0, 16]}
]
},
{
"name": "leftLeg",
"pivot": [1.9, 12, 0],
"mirror": true,
"cubes": [
{"origin": [-0.1, 0, -2], "size": [4, 12, 4], "uv": [0, 16], "mirror": true}
]
}
]
}`
};
skin_presets.breeze = {
display_name: 'Breeze',
model: `{
"name": "breeze",
"texturewidth": 32,
"textureheight": 32,
"eyes": [
[7, 14, 3, 1],
[14, 14, 3, 1],
[6, 29, 5, 1],
[15, 29, 5, 1]
],
"bones": [
{
"name": "body",
"pivot": [0, 0, 0]
},
{
"name": "rods",
"parent": "body",
"pivot": [0, 16, 0],
"cubes": [
{"origin": [-1, 11, -6], "size": [2, 8, 2], "pivot": [0, 19, -3], "rotation": [22.5, 0, 0], "uv": [0, 17]},
{"origin": [-3.59808, 11, -1.5], "size": [2, 8, 2], "pivot": [-2.59808, 19, 1.5], "rotation": [-157.5, 60, 180], "uv": [0, 17]},
{"origin": [1.59808, 11, -1.5], "size": [2, 8, 2], "pivot": [2.59808, 19, 1.5], "rotation": [-157.5, -60, 180], "uv": [0, 17]}
]
},
{
"name": "head",
"parent": "body",
"pivot": [0, 20, 0],
"cubes": [
{"origin": [-4, 20, -4], "size": [8, 8, 8], "uv": [0, 0]}
]
},
{
"name": "eyes",
"parent": "head",
"pivot": [0, 20, 0],
"cubes": [
{"origin": [-5, 22, -4.2], "size": [10, 3, 4], "uv": [4, 24], "layer": true}
]
}
]
}`
}
skin_presets.breeze_tornado = {
display_name: 'Breeze Tornado',
model: `{
"name": "breeze_wind",
"texturewidth": 128,
"textureheight": 128,
"bones": [
{
"name": "tornado_body",
"pivot": [0, 0, 0]
},
{
"name": "tornado_bottom",
"parent": "tornado_body",
"pivot": [0, 0, 0],
"cubes": [
{"origin": [-2.5, 0, -2.5], "size": [5, 7, 5], "uv": [1, 83]}
]
},
{
"name": "tornado_mid",
"parent": "tornado_bottom",
"pivot": [0, 7, 0],
"cubes": [
{"origin": [-2.5, 7, -2.5], "size": [5, 6, 5], "uv": [49, 71]},
{"origin": [-4, 7, -4], "size": [8, 6, 8], "uv": [78, 32]},
{"origin": [-6, 7, -6], "size": [12, 6, 12], "uv": [74, 28]}
]
},
{
"name": "tornado_top",
"parent": "tornado_mid",
"pivot": [0, 13, 0],
"cubes": [
{"origin": [-2.5, 13, -2.5], "size": [5, 8, 5], "uv": [105, 57]},
{"origin": [-6, 13, -6], "size": [12, 8, 12], "uv": [6, 6]},
{"origin": [-9, 13, -9], "size": [18, 8, 18], "uv": [0, 0]}
]
}
]
}`
}
skin_presets.camel = {
display_name: 'Camel',
model: `{
@ -5439,7 +5685,7 @@ skin_presets.silverfish = {
}`
};
skin_presets.skeleton = {
display_name: 'Skeleton',
display_name: 'Skeleton/Stray',
model: `{
"name": "skeleton",
"texturewidth": 64,

View File

@ -147,7 +147,7 @@ async function loadImages(files, event) {
let new_textures = [];
Undo.initEdit({textures: new_textures});
files.forEach(function(f, i) {
let tex = new Texture().fromFile(f).add().fillParticle();
let tex = new Texture().fromFile(f).add(false, true).fillParticle();
new_textures.push(tex);
if (Format.image_editor && i == 0) {
tex.select();

View File

@ -1232,6 +1232,9 @@ BARS.defineActions(function() {
select(project) {
Dialog.open.confirm();
project.select();
},
isPixelArt(project) {
return project.format.image_editor && project.textures[0]?.height < 190;
}
},
computed: {
@ -1249,7 +1252,7 @@ BARS.defineActions(function() {
<search-bar id="tab_overview_search_bar" v-model="search_term"></search-bar>
</div>
<ul id="tab_overview_grid">
<li v-for="project in filtered_projects" @mousedown="select(project)">
<li v-for="project in filtered_projects" @mousedown="select(project)" :class="{pixel_art: isPixelArt(project)}">
<img :src="project.thumbnail" :style="{visibility: project.thumbnail ? 'unset' : 'hidden'}">
{{ project.name }}
</li>

View File

@ -266,12 +266,12 @@ const AutoBackup = {
let has_backups = await AutoBackup.hasBackups();
if (has_backups && (!isApp || !currentwindow.webContents.second_instance)) {
let section = addStartScreenSection({
let section = addStartScreenSection('recover_backup', {
color: 'var(--color-back)',
graphic: {type: 'icon', icon: 'fa-archive'},
insert_before: 'start_files',
text: [
{type: 'h2', text: tl('message.recover_backup.title')},
{type: 'h3', text: tl('message.recover_backup.title')},
{text: tl('message.recover_backup.message')},
{type: 'button', text: tl('message.recover_backup.recover'), click: (e) => {
AutoBackup.recoverAllBackups().then(() => {
@ -438,6 +438,15 @@ function factoryResetAndReload() {
}
}
function benchmarkCode(id, iterations, code) {
if (!iterations) iterations = 1000;
console.time(id);
for (let i = 0; i < iterations; i++) {
code();
}
console.timeEnd(id);
}
const documentReady = new Promise((resolve, reject) => {
$(document).ready(function() {
resolve()

View File

@ -1013,7 +1013,11 @@ async function autoFixMeshEdit() {
return true;
})
let off_corners = edges.find(edge => !edge.includes(concave_vkey))
if (!off_corners) return;
if (!off_corners) {
// not sure if this always works, but its only required in special cases (if the quad edge that should be split is already connected to another face).
let concave_index = sorted_vertices.indexOf(concave_vkey);
off_corners = (concave_index%2) ? [sorted_vertices[1], sorted_vertices[3]] : [sorted_vertices[0], sorted_vertices[2]];
}
let new_face = new MeshFace(mesh, face);
new_face.vertices.remove(off_corners[0]);

View File

@ -252,6 +252,18 @@ class Cube extends OutlinerElement {
}
return this;
}
selectLow(...args) {
let was_selected = this.selected;
super.selectLow(...args);
if (!was_selected && Cube.selected[0]) {
let other_selected_faces = UVEditor.selected_faces.slice();
let own_selected_faces = UVEditor.getSelectedFaces(this, true);
if (other_selected_faces?.length && !own_selected_faces?.length) {
own_selected_faces.replace(other_selected_faces);
}
}
return this;
}
size(axis, floored) {
var scope = this;
let epsilon = 0.0000001;
@ -684,55 +696,56 @@ class Cube extends OutlinerElement {
if (scope.autouv === 2) {
//Relative UV
var all_faces = ['north', 'south', 'west', 'east', 'up', 'down']
let offset = Format.centered_grid ? 8 : 0;
all_faces.forEach(function(side) {
var uv = scope.faces[side].uv.slice()
switch (side) {
case 'north':
uv = [
pw - scope.to[0],
pw - (scope.to[0]+offset),
ph - scope.to[1],
pw - scope.from[0],
pw - (scope.from[0]+offset),
ph - scope.from[1],
];
break;
case 'south':
uv = [
scope.from[0],
(scope.from[0]+offset),
ph - scope.to[1],
scope.to[0],
(scope.to[0]+offset),
ph - scope.from[1],
];
break;
case 'west':
uv = [
scope.from[2],
(scope.from[2]+offset),
ph - scope.to[1],
scope.to[2],
(scope.to[2]+offset),
ph - scope.from[1],
];
break;
case 'east':
uv = [
pw - scope.to[2],
pw - (scope.to[2]+offset),
ph - scope.to[1],
pw - scope.from[2],
pw - (scope.from[2]+offset),
ph - scope.from[1],
];
break;
case 'up':
uv = [
scope.from[0],
scope.from[2],
scope.to[0],
scope.to[2],
(scope.from[0]+offset),
(scope.from[2]+offset),
(scope.to[0]+offset),
(scope.to[2]+offset),
];
break;
case 'down':
uv = [
scope.from[0],
ph - scope.to[2],
scope.to[0],
ph - scope.from[2],
(scope.from[0]+offset),
ph - (scope.to[2]+offset),
(scope.to[0]+offset),
ph - (scope.from[2]+offset),
];
break;
}

View File

@ -64,15 +64,15 @@ class Group extends OutlinerNode {
Canvas.updateAllBones([this]);
return this;
}
select(event, isOutlinerClick) {
select(event, is_outliner_click) {
var scope = this;
if (Blockbench.hasFlag('renaming') || this.locked) return this;
if (!event) event = true
if (isOutlinerClick && event.pointerType == 'touch') return;
var allSelected = Group.selected === this && selected.length && this.matchesSelection()
var allSelected = Group.selected === this && selected.length && this.matchesSelection();
let previous_first_selected = Project.selected_elements[0];
//Clear Old Group
if (Group.selected) Group.selected.unselect()
if (Group.selected) Group.selected.unselect();
if ((event.shiftKey || Pressing.overrides.shift) !== true && (event.ctrlOrCmd || Pressing.overrides.ctrl) !== true) {
selected.length = 0
}
@ -88,6 +88,10 @@ class Group extends OutlinerNode {
//Select Only Group, unselect Children
selected.length = 0
} else {
// Fix for #2401
if (previous_first_selected && previous_first_selected.isChildOf(this)) {
selected.push(previous_first_selected);
}
scope.children.forEach(function(s) {
s.selectLow()
})

View File

@ -434,7 +434,7 @@ class OutlinerElement extends OutlinerNode {
TickUpdates.selection = true;
return copy;
}
select(event, isOutlinerClick) {
select(event, is_outliner_click) {
if (Modes.animate && !this.constructor.animator) {
Blockbench.showQuickMessage('message.group_required_to_animate');
return false;
@ -442,7 +442,7 @@ class OutlinerElement extends OutlinerNode {
//Shift
var just_selected = [];
let allow_multi_select = (!Modes.paint || (Toolbox.selected.id == 'fill_tool' && BarItems.fill_mode.value == 'selected_elements'));
if (event && allow_multi_select && (event.shiftKey === true || Pressing.overrides.shift) && this.getParentArray().includes(selected[selected.length-1]) && isOutlinerClick) {
if (event && allow_multi_select && (event.shiftKey === true || Pressing.overrides.shift) && this.getParentArray().includes(selected[selected.length-1]) && is_outliner_click) {
var starting_point;
var last_selected = selected[selected.length-1]
this.getParentArray().forEach((s, i) => {
@ -485,7 +485,9 @@ class OutlinerElement extends OutlinerNode {
//Normal
} else {
selected.forEachReverse(obj => obj.unselect())
selected.forEachReverse(obj => {
if (obj != this) obj.unselect();
})
if (Group.selected) Group.selected.unselect()
this.selectLow()
just_selected.push(this)
@ -510,6 +512,9 @@ class OutlinerElement extends OutlinerNode {
unselect() {
Project.selected_elements.remove(this);
this.selected = false;
if (UVEditor.selected_element_faces[this.uuid]) {
delete UVEditor.selected_element_faces[this.uuid];
}
TickUpdates.selection = true;
return this;
}
@ -1404,7 +1409,7 @@ Interface.definePanels(function() {
v-bind:style="{'--indentation': indentation}"
@contextmenu.prevent.stop="node.showContextMenu($event)"
@click="node.select($event, true)"
@touchstart="node.select($event)" :title="node.title"
:title="node.title"
@dblclick.stop.self="!node.locked && renameOutliner()"
>` +
//Opener
@ -1587,8 +1592,10 @@ Interface.definePanels(function() {
let key = e1.target.getAttribute('toggle');
let previous_values = {};
let value = original[key];
let toggle_config = Outliner.buttons[key];
value = (typeof value == 'number') ? (value+1) % 3 : !value;
if (!toggle_config) return;
if (!(key == 'locked' || key == 'visibility' || Modes.edit)) return;
function move(e2) {
@ -1609,11 +1616,15 @@ Interface.definePanels(function() {
} else if (!affected.includes(node) && (!node.locked || key == 'locked' || key == 'visibility')) {
let new_affected = [node];
if (node instanceof Group) {
node.forEachChild(node => new_affected.push(node))
if (toggle_config.change_children != false) {
node.forEachChild(node => {
if (node.buttons.find(b => b.id == key)) new_affected.push(node)
});
}
affected_groups.push(node);
} else if (node.selected && selected.length > 1) {
selected.forEach(el => {
if (node[key] != undefined) new_affected.safePush(el);
} else if (node.selected && Outliner.selected.length > 1) {
Outliner.selected.forEach(el => {
if (el.buttons.find(b => b.id == key)) new_affected.safePush(el);
})
}
new_affected.forEach(node => {
@ -1895,7 +1906,7 @@ class Face {
if (Format.per_group_texture && this.element.parent instanceof Group && this.element.parent.texture) {
return Texture.all.findInArray('uuid', this.element.parent.texture);
}
if (this.texture !== null && (Format.single_texture || (Format.single_texture_default && !this.texture))) {
if (this.texture !== null && (Format.single_texture || (Format.single_texture_default && (Format.per_group_texture || !this.texture)))) {
return Texture.getDefault();
}
if (typeof this.texture === 'string') {
@ -1920,8 +1931,6 @@ class Face {
let tex = this.getTexture()
if (tex === null) {
copy.texture = null;
} else if (!this.texture) {
copy.texture = false;
} else if (tex instanceof Texture && project) {
copy.texture = Texture.all.indexOf(tex)
} else if (tex instanceof Texture) {

View File

@ -723,7 +723,7 @@ Plugin.prototype.menu = new Menu([
icon: 'folder',
condition: plugin => (isApp && plugin.source == 'file'),
click(plugin) {
shell.showItemInFolder(plugin.path);
showItemInFolder(plugin.path);
}
},
]);
@ -1301,8 +1301,8 @@ BARS.defineActions(function() {
<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" ref="plugin_list">
<li v-for="plugin in viewed_plugins" :plugin="plugin.id" :class="{plugin: true, testing: plugin.fromFile, selected: plugin == selected_plugin, installed: plugin.installed, disabled_plugin: plugin.disabled, incompatible: plugin.isInstallable() !== true}" @click="selectPlugin(plugin)" @contextmenu="selectPlugin(plugin); plugin.showContextMenu($event)">
<ul class="list" :class="{paginated_list: pages.length > 1}" id="plugin_list" ref="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, installed_plugin: plugin.installed, disabled_plugin: plugin.disabled, incompatible: plugin.isInstallable() !== true}" @click="selectPlugin(plugin)" @contextmenu="selectPlugin(plugin); plugin.showContextMenu($event)">
<div>
<div class="plugin_icon_area">
<img v-if="plugin.hasImageIcon()" :src="plugin.getIcon()" width="48" height="48px" />
@ -1327,7 +1327,7 @@ BARS.defineActions(function() {
</ol>
</div>
<div id="plugin_browser_page" v-if="selected_plugin">
<div id="plugin_browser_page" v-if="selected_plugin" :class="{plugin_disabled: selected_plugin.disabled, plugin_installed: selected_plugin.installed}">
<div v-if="isMobile" @click="selectPlugin(null);" class="plugin_browser_back_button">
<i class="material-icons icon">arrow_back_ios</i>
${tl('generic.navigate_back')}</div>

View File

@ -785,7 +785,7 @@ class Preview {
if (Toolbox.selected.selectElements && Modes.selected.selectElements && (data.type === 'element' || Toolbox.selected.id == 'knife_tool')) {
if (Toolbox.selected.selectFace && data.face && data.element.type != 'mesh') {
let face_selection = UVEditor.getSelectedFaces(data.element, true);
if (multi_select || group_select) {
if (data.element.selected && (multi_select || group_select)) {
face_selection.safePush(data.face);
} else {
face_selection.replace([data.face]);
@ -955,7 +955,7 @@ class Preview {
let vertices = data.element.getSelectedVertices(true);
let edges = data.element.getSelectedEdges(true);
let faces = data.element.getSelectedEdges(true);
let faces = data.element.getSelectedFaces(true);
if (multi_select || group_select) {
let index = edges.findIndex(edge => sameMeshEdge(edge, data.vertices))
@ -1085,6 +1085,9 @@ class Preview {
x = Math.round(x + offset) - offset;
y = Math.round(y + offset) - offset;
}
if (texture.currentFrame) {
y -= texture.display_height * texture.currentFrame;
}
// Position
let brush_coord = face.UVToLocal([x * uv_factor_x, y * uv_factor_y]);
let brush_coord_difference_x = face.UVToLocal([(x+1) * uv_factor_x, y * uv_factor_y]);

View File

@ -303,11 +303,18 @@ class ReferenceImage {
this.node.style.left = pos_x + 'px';
this.node.style.top = pos_y + 'px';
let offset_top = preview.node.offsetTop - this.node.offsetTop;
let offset_right = preview.node.clientWidth + preview.node.offsetLeft - this.node.offsetLeft;
let offset_bottom = preview.node.clientHeight + preview.node.offsetTop - this.node.offsetTop;
let offset_left = preview.node.offsetLeft - this.node.offsetLeft;
this.node.style.clipPath = `rect(${offset_top}px ${offset_right}px ${offset_bottom}px ${offset_left}px) view-box`;
} else {
this.node.style.width = this.size[0] + 'px';
this.node.style.height = this.size[1] + 'px';
this.node.style.left = (Math.clamp(this.position[0], 0, this.node.parentNode.clientWidth) - this.size[0]/2) + 'px';
this.node.style.top = (Math.clamp(this.position[1], 0, this.node.parentNode.clientHeight) - this.size[1]/2) + 'px';
this.node.style.clipPath = '';
}
return this;
}

View File

@ -273,7 +273,7 @@ const Painter = {
let texture = Painter.current.texture;
if (Toolbox.selected.brush && Toolbox.selected.brush.onStrokeEnd) {
let result = Toolbox.selected.brush.onStrokeEnd({texture, x, y, uv, event, raycast_data: data});
let result = Toolbox.selected.brush.onStrokeEnd({texture});
if (result == false) return;
}
if (Painter.brushChanges) {
@ -294,6 +294,7 @@ const Painter = {
delete Painter.current.texture;
delete Painter.current.textures;
delete Painter.current.uv_rects;
delete Painter.current.uv_islands;
Painter.painting = false;
Painter.currentPixel = [-1, -1];
},
@ -340,6 +341,7 @@ const Painter = {
let island = Painter.getMeshUVIsland(Painter.current.face, current_face);
island.forEach(fkey => {
let face = Mesh.selected[0].faces[fkey];
if (!face) return;
for (let vkey in face.uv) {
min_x = Math.min(min_x, face.uv[vkey][0]); max_x = Math.max(max_x, face.uv[vkey][0]);
min_y = Math.min(min_y, face.uv[vkey][1]); max_y = Math.max(max_y, face.uv[vkey][1]);
@ -1734,7 +1736,11 @@ const Painter = {
} else {
preset.color = null;
}
if (form.pixel_perfect) {
preset.pixel_perfect = true;
} else {
preset.pixel_perfect = false;
}
if (form.shape !== 'unset') {
preset.shape = form.shape;
} else {
@ -2106,8 +2112,8 @@ SharedActions.add('paste', {
layer.setSize(frame.width, frame.height);
layer.ctx.putImageData(image_data, 0, 0);
if (!offset) layer.center();
texture.layers.push(layer);
layer.select();
layer.addForEditing();
layer.setLimbo();
texture.updateChangesAfterEdit();

View File

@ -211,7 +211,7 @@ BARS.defineActions(function() {
} else if (Format.id == 'java_block') {
docs = 'https://minecraft.wiki/w/Resource_pack#Animation';
file_name = texture.name + '.mcmeta';
file_name = (texture.name.match(/\.png$/i) ? texture.name : texture.name + '.png') + '.mcmeta';
content = texture.getMCMetaContent();
text = compileJSON(content);
}

View File

@ -888,15 +888,17 @@ class Texture {
if (event instanceof Event) {
Prop.active_panel = 'textures';
}
if (event && (event.shiftKey || event.ctrlKey)) {
this.multi_selected = true;
if (event.shiftKey) {
if (event && (event.shiftKey || event.ctrlOrCmd || Pressing.overrides.ctrl || Pressing.overrides.shift)) {
if (event.shiftKey || Pressing.overrides.shift) {
this.multi_selected = true;
let start_i = Texture.last_selected;
let end_i = Texture.all.indexOf(this);
if (start_i > end_i) [start_i, end_i] = [end_i, start_i];
for (let i = start_i+1; i < end_i; i++) {
Texture.all[i].multi_selected = true;
}
} else {
this.multi_selected = !this.multi_selected;
}
Texture.last_selected = Texture.all.indexOf(this);
return;
@ -931,7 +933,7 @@ class Texture {
Blockbench.dispatchEvent('update_texture_selection');
return this;
}
add(undo) {
add(undo, uv_size_from_resolution) {
if (isApp && this.path && Project.textures.length) {
for (var tex of Project.textures) {
if (tex.path === this.path) return tex;
@ -940,7 +942,7 @@ class Texture {
if (Texture.all.find(t => t.render_mode == 'layered')) {
this.render_mode = 'layered';
}
if (Format.per_texture_uv_size && undo) {
if (Format.per_texture_uv_size && uv_size_from_resolution) {
this.flags.add('update_uv_size_from_resolution');
}
if (undo) {
@ -1081,7 +1083,7 @@ class Texture {
Blockbench.showQuickMessage('texture.error.file')
return this;
}
shell.showItemInFolder(this.path)
showItemInFolder(this.path)
return this;
}
openEditor() {
@ -1111,10 +1113,9 @@ class Texture {
return this;
}
showContextMenu(event) {
var scope = this;
scope.select()
if (this != Texture.selected) this.select()
Prop.active_panel = 'textures'
this.menu.open(event, scope)
this.menu.open(event, this)
}
openMenu() {
this.select();
@ -2236,7 +2237,7 @@ BARS.defineActions(function() {
let texture_group = context instanceof TextureGroup ? context : Texture.selected?.getGroup();
Undo.initEdit({textures: new_textures});
results.forEach(function(f) {
let t = new Texture({name: f.name}).fromFile(f).add(false).fillParticle();
let t = new Texture({name: f.name}).fromFile(f).add(false, true).fillParticle();
new_textures.push(t);
if (texture_group) {
t.group = texture_group.uuid;

View File

@ -2070,10 +2070,10 @@
"message.auto_fix_mesh_edit.merge_vertices": "Eckpunkte vereinen",
"message.auto_fix_mesh_edit.split_quads": "Vierecke teilen",
"message.load_images.add_image": "Bild hinzufügen",
"dialog.project.modded_entity_entity_class": "Entity Class",
"dialog.project.modded_entity_entity_class": "Entity Klasse",
"dialog.resize_texture.offset": "Versatz",
"dialog.animated_texture_editor.code_reference": "Code Reference",
"dialog.animated_texture_editor.code_reference.about": "This code to implement the texture animation was generated as a reference and does not necessarily work without modification. Check out the documentation to learn more.",
"dialog.animated_texture_editor.code_reference": "Code Beispiel",
"dialog.animated_texture_editor.code_reference.about": "Dieser Code zur Implementierung der animierten Textur wurde als Beispiel generiert, und funktioniert nicht unbedingt ohne weitere Veränderungen. Lies die Dokumentation, um mehr zu erfahren.",
"dialog.animated_texture_editor.code_reference.docs": "Dokumentation",
"dialog.animated_texture_editor.reframe": "Neu unterteilen",
"dialog.animated_texture_editor.stride": "Bildgröße",
@ -2081,7 +2081,6 @@
"dialog.animated_texture_editor.add_frame": "Einzelbild hinzufügen",
"dialog.animated_texture_editor.resize_frames": "Bildgröße ändern...",
"dialog.animated_texture_editor.apply": "Anwenden",
"dialog.add_primitive.shape.beveled_cuboid": "Beveled Cuboid",
"dialog.add_primitive.edge_size": "Kantenbreite",
"dialog.change_animation_speed.speed": "Geschwindigkeit",
"dialog.change_animation_speed.adjust_snapping": "Einrastintervall anpassen",
@ -2098,32 +2097,32 @@
"settings.pixel_grid.desc": "Im Bearbeitungsmodus ein Gitter auf den Texturen anzeigen",
"settings.image_editor_grid_size": "Gittergröße im Bildbearbeitungsmodus",
"settings.image_editor_grid_size.desc": "Größe des großen Gitters im Bildbearbeitungsmodus",
"settings.ground_plane_double_side": "Ground Plane Double-sided",
"settings.ground_plane_double_side.desc": "Make the ground plane visible from below",
"settings.optifine_save_default_texture": "OptiFine JEM: Export selected texture",
"settings.optifine_save_default_texture.desc": "Export the selected texture as the default texture of the model",
"action.slider_color_select_threshold": "Color Select Threshold",
"action.slider_color_select_threshold.desc": "How close the color of a neighboring pixel has to be to get selected by color or wand select",
"action.stretch_tool": "Stretch",
"action.stretch_tool.desc": "Tool to select and stretch elements",
"settings.ground_plane_double_side": "Doppelseitige Bodenebene",
"settings.ground_plane_double_side.desc": "Macht die Bodenebene von unten sichtbar",
"settings.optifine_save_default_texture": "OptiFine JEM: Ausgewählte Texture Exportieren",
"settings.optifine_save_default_texture.desc": "Exportiere die ausgewählte Textur als Standardtextur für das Modell",
"action.slider_color_select_threshold": "Farbauswahl Schwellenwert",
"action.slider_color_select_threshold.desc": "Wie ähnlich die Farbe benachbarter Pixel sein muss, um vom Zauberstab oder Farb-Auswahlwerkzeug ausgewählt zu werden",
"action.stretch_tool": "Strecken",
"action.stretch_tool.desc": "Werkzeug zum Strecken von Elementen",
"action.knife_tool": "Messerwerkzeug",
"action.knife_tool.desc": "Werkzeug zum Schneiden von Maschenflächen in kleinere Segmente",
"action.duplicate_project": "Projekt duplizieren",
"action.duplicate_project.desc": "Erstelle eine Kopie des geöffneten Projekts",
"action.transform_space.parent": "Parent",
"action.transform_pivot_space": "Pivot Transform Space",
"action.solidify_mesh_selection": "Solidify Faces",
"action.solidify_mesh_selection.desc": "Solidify the selected faces of the mesh",
"action.transform_space.parent": "Übergeordnet",
"action.transform_pivot_space": "Angelpunkt Transformationsraum",
"action.solidify_mesh_selection": "Flächen solide machen",
"action.solidify_mesh_selection.desc": "Verwandle die ausgewählten Flächen einer Masche in ein solides Objekt mit Dicke",
"action.animated_texture_editor": "Animierte Textur...",
"action.animated_texture_editor.desc": "Erstelle oder bearbeite die Daumenkino-Animation der ausgewählten Textur",
"action.center_individual_pivots": "Center Individual Pivots",
"action.center_individual_pivots.desc": "Set the pivot point of each selected element to its center",
"action.uv_cycle": "Cycle UV",
"action.uv_cycle.desc": "Cycle through the order of UV vertices without changing the UV positions",
"action.uv_cycle_invert": "Cycle Invert UV",
"action.uv_cycle_invert.desc": "Reverse the order of UV vertices without changing the UV positions",
"action.slider_animation_controller_speed": "Controller Playback Speed",
"action.slider_animation_controller_speed.desc": "The playback speed of animation controllers in percent",
"action.center_individual_pivots": "Individuelle Angelpunkte zentrieren",
"action.center_individual_pivots.desc": "Verschiebt den Angelpunkt jedes einzelnen ausgewählten Elements in dessen Mitte",
"action.uv_cycle": "UV Punkte durchrotieren",
"action.uv_cycle.desc": "Rotiere die Anordnung der UV Eckpunkte durch, ohne die Positionen zu verändern",
"action.uv_cycle_invert": "UV Punkte durchtauschen",
"action.uv_cycle_invert.desc": "Kehre die Anordnung der UV Eckpunkte um, ohne die Positionen zu verändern",
"action.slider_animation_controller_speed": "Regler Wiedergabegeschwindigkeit",
"action.slider_animation_controller_speed.desc": "Die Wiedergabegeschwindigkeit des Animationsreglers in Prozent",
"action.optimize_animation": "Animation optimieren",
"action.optimize_animation.desc": "Optimiere die ausgewählte Animation und reduziere die Anzahl der Keyframes",
"action.change_animation_speed": "Animationsgeschwindigkeit verändern...",
@ -2137,5 +2136,6 @@
"menu.toolbar.overflow": "Überlauf der Werkzeugleiste",
"edit.solidify_mesh_selection.thickness": "Dicke",
"reference_image.sync_to_timeline": "Mit der Zeitleiste synchronisieren",
"reference_image.toggle_playback": "Abspielen/Pausieren"
"reference_image.toggle_playback": "Abspielen/Pausieren",
"dialog.add_primitive.shape.beveled_cuboid": "Abgeschrägter Quader"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -401,7 +401,7 @@
"menu.texture.refresh": "刷新",
"menu.texture.change": "更改文件",
"menu.texture.folder": "在文件夹中打开",
"menu.texture.edit": "外部编辑",
"menu.texture.edit": "编辑",
"menu.texture.export": "另存为",
"menu.texture.save": "保存",
"menu.texture.properties": "属性...",
@ -1618,7 +1618,7 @@
"generic.delete_all": "删除所有",
"message.child_model_only.open": "打开父项",
"message.child_model_only.open_with_textures": "打开父项并应用纹理",
"dialog.project.credit": "Credit",
"dialog.project.credit": "归因",
"dialog.convert_project.create_copy": "创建副本",
"dialog.texture.frame_time": "帧时间",
"dialog.texture.frame_time.desc": "设置每帧可见的时长,以刻度为单位。 每个刻度为 1/20 秒。",
@ -1692,7 +1692,7 @@
"dialog.load_plugins_from_query.text": "此链接的插件需要被安装。您确定要安装此插件吗?",
"dialog.select_model.title": "选择模型",
"dialog.select_model.bones": "骨骼",
"dialog.select_model.cubes": "Cubes",
"dialog.select_model.cubes": "多立方",
"dialog.settings.create_profile": "创建配置...",
"settings_profile.confirm_delete": "确定删除此配置?",
"settings_profile.condition.type.selectable": "Manually Selectable",
@ -1714,11 +1714,11 @@
"action.animation_controller_preview_mode.paused": "已暂停",
"action.animation_controller_preview_mode.manual": "手动预览",
"action.animation_controller_preview_mode.play": "自动播放",
"action.keyframe_interpolation.bezier": "Bézier",
"action.keyframe_bezier_linked": "Link Bézier Handles",
"action.keyframe_interpolation.bezier": "贝塞尔",
"action.keyframe_bezier_linked": "链接贝塞尔手柄",
"action.keyframe_bezier_linked.desc": "Connect the right and left keyframe bézier handle",
"action.reset_keyframe_handles": "Reset Keyframe Handles",
"action.reset_keyframe_handles.desc": "Reset the bézier handles of the selected keyframes",
"action.reset_keyframe_handles.desc": "重置已选中贝塞尔手柄",
"action.looped_animation_playback": "循环播放",
"action.looped_animation_playback.desc": "无限循环预览动画",
"timeline.bind_to_actor": "Bind to Actor",
@ -1739,7 +1739,7 @@
"menu.color_picker.slider_mode": "Slider Mode",
"menu.color_picker.slider_mode.hsv": "色相、饱和度、明度",
"menu.color_picker.slider_mode.rgb": "RGB",
"menu.texture.render_mode.additive": "Additive",
"menu.texture.render_mode.additive": "添加",
"panel.color.picker_options": "取色器设置",
"panel.animation_controllers": "动画控制器",
"display.preset.armor_stand": "地面(盔甲架)",
@ -1768,7 +1768,7 @@
"message.add_reference_image.project": "添加至当前项目",
"message.add_reference_image.app": "添加至所有项目",
"message.import_particle_texture.import": "导入粒子材质",
"message.import_particle_texture.message": "Would you like to import a texture file to be used for your particle?",
"message.import_particle_texture.message": "你要导入纹理文件用在粒子吗?",
"message.save_codec_selector.title": "保存模型格式",
"message.save_codec_selector.message": "选择保存模型的格式。",
"message.save_codec_selector.project_file": "Blockbench 项目 (.bbmodel)",
@ -1787,8 +1787,8 @@
"dialog.proportional_editing.selection": "Selection",
"dialog.proportional_editing.selection.linear": "Linear Distance",
"dialog.proportional_editing.selection.connections": "Connections",
"dialog.mirror_painting_texture_center.middle": "Middle",
"dialog.mirror_painting_texture_center.custom": "Custom",
"dialog.mirror_painting_texture_center.middle": "中间",
"dialog.mirror_painting_texture_center.custom": "风俗中间",
"dialog.settings.reset_to_default": "重置至原值",
"keybindings.item.num_slider.increase": "Increase",
"keybindings.item.num_slider.decrease": "Decrease",
@ -1821,7 +1821,7 @@
"menu.uv.flip_x": "反转 X 轴",
"menu.uv.flip_y": "反转 Y 轴",
"menu.mirror_painting.enabled": "启用",
"menu.mirror_painting.configure_texture_center": "Configure Texture Center...",
"menu.mirror_painting.configure_texture_center": "配置纹理中间...",
"reference_image.position": "位置",
"reference_image.size": "尺寸",
"reference_image.rotation": "旋转",
@ -1840,7 +1840,7 @@
"codec.common.encoding": "编码",
"codec.common.armature": "Export Groups as Armature",
"codec.common.export_animations": "导出动作",
"codec.common.embed_textures": "Embed Textures",
"codec.common.embed_textures": "内嵌纹理",
"preview.center_camera": "Center Camera",
"display.reference.frame_top": "Item Frame Top",
"display.reference.frame_top_invisible": "Item Frame Top (Invisible)",
@ -1872,9 +1872,9 @@
"dialog.unsaved_work.text": "The following projects contain unsaved changes. Do you want to save your changes?",
"dialog.unsaved_work.discard_all": "Discard All",
"dialog.unsaved_work.save_all": "Save All",
"dialog.resize_texture.mode": "Mode",
"dialog.resize_texture.mode.crop": "Crop/Expand",
"dialog.resize_texture.mode.scale": "Scale",
"dialog.resize_texture.mode": "模式",
"dialog.resize_texture.mode.crop": "裁切/扩展",
"dialog.resize_texture.mode.scale": "缩放",
"dialog.plugins.is_installed": "Installed",
"dialog.plugins.is_disabled": "Disabled",
"dialog.plugins.disable": "Disable",
@ -1965,7 +1965,7 @@
"message.texture_refresh_conflict.keep_ours": "Keep Blockbench's version",
"message.texture_refresh_conflict.keep_theirs": "Load file and discard all changes and layers in Blockbench",
"message.group_required_to_animate": "You cannot animate this type of element. Create a group to animate it.",
"dialog.texture.uv_size": "UV Size",
"dialog.texture.uv_size": "UV尺寸",
"dialog.advanced_screenshot.angle_preset": "Angle Preset",
"dialog.advanced_screenshot.resolution": "Resolution",
"dialog.advanced_screenshot.zoom_to_fit": "Zoom To Fit",
@ -2004,8 +2004,8 @@
"action.swap_colors.desc": "Swap the main color with the secondary color",
"action.apply_mesh_rotation": "Apply Rotation",
"action.apply_mesh_rotation.desc": "Reset the element rotation of the mesh, and apply it to the geometry instead",
"action.flip_texture_x.desc": "Flip the texture or layer horizontally",
"action.flip_texture_y.desc": "Flip the texture or layer vertically",
"action.flip_texture_x.desc": "水平翻转纹理或层",
"action.flip_texture_y.desc": "翻转纹理或层垂直",
"action.rotate_texture_cw.desc": "Rotate the texture or layer clockwise",
"action.rotate_texture_ccw.desc": "Rotate the texture or layer counter-clockwise",
"action.crop_texture_to_selection": "Crop Texture to Selection",
@ -2081,7 +2081,6 @@
"dialog.animated_texture_editor.add_frame": "Add Frame",
"dialog.animated_texture_editor.resize_frames": "Resize Frames...",
"dialog.animated_texture_editor.apply": "Apply",
"dialog.add_primitive.shape.beveled_cuboid": "Beveled Cuboid",
"dialog.add_primitive.edge_size": "Edge Size",
"dialog.change_animation_speed.speed": "Speed",
"dialog.change_animation_speed.adjust_snapping": "Adjust Snapping",
@ -2137,5 +2136,6 @@
"menu.toolbar.overflow": "Toolbar Overflow",
"edit.solidify_mesh_selection.thickness": "Thickness",
"reference_image.sync_to_timeline": "Sync To Timeline",
"reference_image.toggle_playback": "Play/Pause"
"reference_image.toggle_playback": "Play/Pause",
"dialog.add_primitive.shape.beveled_cuboid": "Beveled Cuboid"
}

View File

@ -401,7 +401,7 @@
"menu.texture.refresh": "重新載入",
"menu.texture.change": "更改檔案",
"menu.texture.folder": "在資料夾中開啟",
"menu.texture.edit": "Edit",
"menu.texture.edit": "編輯",
"menu.texture.export": "儲存為",
"menu.texture.save": "儲存",
"menu.texture.properties": "屬性...",
@ -1440,14 +1440,14 @@
"edit.loop_cut.direction": "方向",
"message.meshes_and_box_uv": "Meshes are not compatible with Box UV. Go to File > Project... and switch to Per-face UV.",
"data.texture_mesh": "Texture Mesh",
"generic.left": "Left",
"generic.right": "Right",
"mode.start.quick_setup": "Quick Setup",
"generic.left": "",
"generic.right": "",
"mode.start.quick_setup": "快設置",
"mode.start.keymap": "Keymap",
"mode.start.quick_setup.more_themes": "More...",
"mode.start.quick_setup.more_themes": "...",
"dialog.resize_texture.animation_frames": "Animation Frames",
"dialog.export_emission_map.title": "Export Emission Map",
"dialog.export_emission_map.format": "Format",
"dialog.export_emission_map.format": "格式",
"dialog.export_emission_map.format.luminance": "Luminance",
"dialog.export_emission_map.format.luminance_inverted": "Luminance Inverted",
"dialog.export_emission_map.format.colors": "Colors",
@ -1518,7 +1518,7 @@
"format.java_block.info.size": "The model is limited to a size of 3 by 3 by 3 blocks. Display settings can make item models larger though",
"format.java_block.info.animation": "This format does not support animations in vanilla Minecraft. If you are creating a mod, you can use GeckoLib to animate models. If not, the only way to animate is to switch out the model using commands or animated textures.",
"format.bedrock.info.textures": "Each model can only have one texture",
"format.bedrock_block": "Bedrock Block",
"format.bedrock_block": "Bedrock方塊",
"format.bedrock_block.desc": "Block model for Minecraft Bedrock Edition",
"format.modded_entity.info.integer_size": "The size of individual cubes is limited to integers.",
"format.modded_entity.info.format": "Models are written in Java code instead of dedicated data structures like all other Blockbench export formats.",
@ -2081,7 +2081,6 @@
"dialog.animated_texture_editor.add_frame": "Add Frame",
"dialog.animated_texture_editor.resize_frames": "Resize Frames...",
"dialog.animated_texture_editor.apply": "Apply",
"dialog.add_primitive.shape.beveled_cuboid": "Beveled Cuboid",
"dialog.add_primitive.edge_size": "Edge Size",
"dialog.change_animation_speed.speed": "Speed",
"dialog.change_animation_speed.adjust_snapping": "Adjust Snapping",
@ -2137,5 +2136,6 @@
"menu.toolbar.overflow": "Toolbar Overflow",
"edit.solidify_mesh_selection.thickness": "Thickness",
"reference_image.sync_to_timeline": "Sync To Timeline",
"reference_image.toggle_playback": "Play/Pause"
"reference_image.toggle_playback": "Play/Pause",
"dialog.add_primitive.shape.beveled_cuboid": "Beveled Cuboid"
}

View File

@ -101,8 +101,8 @@ class CanvasFrame {
}
}
var trimHeight = bound.bottom - bound.top,
trimWidth = bound.right - bound.left,
var trimHeight = bound.bottom - bound.top + 1,
trimWidth = bound.right - bound.left + 1,
trimmed = this.ctx.getImageData(bound.left, bound.top, trimWidth, trimHeight);
copy.canvas.width = trimWidth;

View File

@ -1,4 +1,4 @@
const {app, BrowserWindow, Menu, ipcMain} = require('electron')
const {app, BrowserWindow, Menu, ipcMain, shell} = require('electron')
const path = require('path')
const url = require('url')
const { autoUpdater } = require('electron-updater');
@ -247,6 +247,9 @@ ipcMain.on('request-color-picker', async (event, arg) => {
})
}
})
ipcMain.on('show-item-in-folder', async (event, path) => {
shell.showItemInFolder(path);
})
app.on('ready', () => {
@ -266,7 +269,7 @@ app.on('ready', () => {
}
app_was_loaded = true;
if (process.execPath && process.execPath.match(/electron\.\w+$/)) {
if (process.execPath && process.execPath.match(/node_modules[\\\/]electron/)) {
console.log('[Blockbench] App launched in development mode')

876
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -122,7 +122,7 @@
"publish-windows": "npm run bundle && electron-builder -w --publish=onTagOrDraft && node ./scripts/rename_portable.js && electron-builder --windows portable --publish=onTagOrDraft",
"pwa": "node ./scripts/generate_pwa.js",
"prepublish": "npm run bundle && npm run pwa",
"webapp": "git checkout gh-pages && git merge master && git push && git checkout master"
"webapp": "git checkout gh-pages && git pull && git merge master && git push && git checkout master"
},
"devDependencies": {
"@electron/notarize": "^2.3.0",

View File

@ -20,6 +20,7 @@
"light": "#f4f3ff",
"accent_text": "#000006",
"subtle_text": "#848891",
"bright_ui_text": "#000006",
"grid": "#495061",
"wireframe": "#576f82",
"checkerboard": "#1c2026"

View File

@ -20,6 +20,7 @@
"light": "#111111",
"accent_text": "#000006",
"subtle_text": "#6b6d72",
"bright_ui_text": "#000006",
"grid": "#b0b2b8",
"wireframe": "#8797a3",
"checkerboard": "#eeeeee"