Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
de4fca7
Use encompassing collider for `minecraft:selection_box`
IllagerCaptain Apr 4, 2026
47d5a56
Fix #3467 mesh deformation inconsistent when switching away from anim…
JannisX11 Apr 4, 2026
68e2172
Fix #3465 Dragging groups in outliner does not display number of drag…
JannisX11 Apr 4, 2026
2e1a7e8
Improve painting performance on simple meshes
JannisX11 Apr 5, 2026
2b7c9e1
Switch Focus On View target to UV on hover over UV editor
JannisX11 Apr 5, 2026
b2974cf
Changes
JannisX11 Apr 5, 2026
aa1e364
Fix #3460 action side menu button click detection area does not cover…
JannisX11 Apr 5, 2026
cac8bc6
Fix recover model overflow
JannisX11 Apr 5, 2026
02f73fc
v5.1.3 [ci-build]
JannisX11 Apr 5, 2026
ed0a960
Fix #3483 Toggling cube face only works if the face is selected
JannisX11 Apr 7, 2026
0434fa2
Merge pull request #3469 from IllagerCaptain/patch
JannisX11 Apr 7, 2026
5257298
Fix texture content not updating when duplicating layer
JannisX11 Apr 10, 2026
f977f7d
Fix labels missing on num sliders
JannisX11 Apr 10, 2026
bebc738
Fix copy brush not working when brush resizing is assigned to ctrl + …
JannisX11 Apr 10, 2026
83b2bed
Fix long layer name overflow
JannisX11 Apr 10, 2026
4e0ce9c
Merge branch 'patch' of https://github.com/JannisX11/blockbench into …
JannisX11 Apr 10, 2026
50ed4c5
Add molang validator check for complex expression with no return value
JannisX11 Apr 11, 2026
9d2572f
Fix java version of zombie skin preset texture not matching UV (#3490)
JannisX11 Apr 11, 2026
01ca8da
Change plugin store text
JannisX11 Apr 11, 2026
39ad4b4
Update contributing guide
JannisX11 Apr 14, 2026
43f37f8
🩹 Fix type of `Plugins.download_stats`
SnaveSutit Apr 14, 2026
68125e8
🩹 Ignore delete error
SnaveSutit Apr 14, 2026
d6cbe8d
✨ Make node's `stream` a safe API
SnaveSutit Apr 14, 2026
87c7222
✨ Make node's `tty` a safe API
SnaveSutit Apr 14, 2026
eb9e4c7
✨ Make node's `perf_hooks` a safe API
SnaveSutit Apr 14, 2026
8c3d3ef
✂️ Remove `tty` library as it's not what I thought.
SnaveSutit Apr 17, 2026
61a07ad
✂️ Undo commit "Ignore delete error"
SnaveSutit Apr 17, 2026
8b4d370
Fix issue with Preserve World Transform on meshes
JannisX11 Apr 18, 2026
638377b
Add scoped FS watch APIS
JannisX11 Apr 18, 2026
ef85dc2
Fix #3505 Advanced screenshot with ortho preset has wrong size on fir…
JannisX11 Apr 18, 2026
f9a9237
Fix #3503 wrong UV on beak on java edition parrot skin preset
JannisX11 Apr 19, 2026
464f389
Timeline css fix
JannisX11 Apr 20, 2026
18d9f09
Improve toggle visibility performance
JannisX11 Apr 20, 2026
89042c2
Fix #3519 Open With Blockbench not working with special character at …
JannisX11 Apr 21, 2026
a424b12
Improve canvas raycast performance
JannisX11 Apr 23, 2026
ab4611f
Fix mirror modeling issues
JannisX11 Apr 23, 2026
7f6ef95
FIx #3476 Resolve Group does not undo track child groups correctly
JannisX11 Apr 23, 2026
0483e63
Fix #3500 Opening a second blockbench instance shows file unsupported…
JannisX11 Apr 23, 2026
789a57d
Fix mirror modeling issue with imprecise values
JannisX11 Apr 24, 2026
10dd09b
Fix duplicate group undo issues
JannisX11 Apr 24, 2026
d104e8f
Add node module types
JannisX11 Apr 24, 2026
388c86b
Merge pull request #3509 from SnaveSutit/fix/node-stream
JannisX11 Apr 24, 2026
00e72c8
Add missing type
JannisX11 Apr 24, 2026
6cc43fe
Merge branch 'master' into patch
JannisX11 Apr 24, 2026
18158c3
Expose uv size utilities
JannisX11 Apr 25, 2026
616452d
✨ Improved `generate-types` script.
SnaveSutit Mar 4, 2026
429e47f
🩹 Simplify excluded path filtering
SnaveSutit Mar 4, 2026
9b9b202
✨ Add cleanup step
SnaveSutit Mar 4, 2026
445bb46
🐛 Fixed empty processed file filtering
SnaveSutit Mar 4, 2026
4116b73
🎨 Cleanup imports
SnaveSutit Mar 4, 2026
f9ceea3
🩹 Add `dontTransform` list to `type_config.json`
SnaveSutit Mar 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ If you want to contribute Blockbench translations, you can visit [blockbench.net

## Source code contributions

Blockbench, the program itself, is mostly just coded by me. But sometimes, code contributions can be helpful!
Blockbench, the program itself, is mostly just coded by me. But third party contributions are welcome!

If you want to contribute to the Blockbench code, please contact me (@Jannis) on [Discord](https://discord.gg/sGkj78W) (e.g. in #bb-feedback) *before* to discuss whether a change is necessary in the base program and how it can be approached.
Some changes make sense in Blockbench itself, while others could work better as a plugin.
Bug fixes can be submitted via pull request.

If you want to add a feature or change functionality, I recommend opening a discussion on Discord before to talk about if and where it fits in. You can do so in the channel [#bb-feedback](https://discord.gg/kZBmCwvBFw). Some features may be out of scope and work better as plugins, or I might have additional tips on how to approach it. The timing of features being integrated also depends on the release cycle, new features should go through beta testing before being shipped.

Minor fixes should be targeting the `patch` branch, new features and fixes that require additional testing should generally target the `next` branch.

Expand Down
6 changes: 5 additions & 1 deletion css/general.css
Original file line number Diff line number Diff line change
Expand Up @@ -687,7 +687,10 @@
}
.tool.side_menu_tool i.icon.action_more_options {
display: inline-block;
width: 19px;
width: 20px;
height: 100%;
margin-top: 0;
padding-top: 4px;
color: var(--color-text);
margin-left: 0;
}
Expand All @@ -702,6 +705,7 @@
text-align: center;
width: 100%;
margin-top: 0px;
padding-top: 0px;
}

/*(Context-)Menu*/
Expand Down
3 changes: 2 additions & 1 deletion css/panels.css
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,7 @@
}
.texture_layer > label {
flex-grow: 1;
overflow: hidden;
}
.texture_layer i.toggle_disabled {
color: var(--color-subtle_text);
Expand Down Expand Up @@ -1773,7 +1774,7 @@
}
#timeline_vue.graph_editor .channel_head {
flex-wrap: wrap;
justify-content: flex-end;
justify-content: flex-start;
}
.drag_hover[order]::before {
z-index: 7;
Expand Down
8 changes: 4 additions & 4 deletions js/animations/animation_mode.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ export const Animator = {
Timeline.pause()
Animator.open = false;

scene.remove(WinterskyScene.space);
scene.remove(Animator.motion_trail);
scene.remove(Animator.onion_skin_object);
Canvas.scene.remove(WinterskyScene.space);
Canvas.scene.remove(Animator.motion_trail);
Canvas.scene.remove(Animator.onion_skin_object);
Animator.resetParticles(true);

three_grid.position.z = three_grid.position.x;
Expand Down Expand Up @@ -113,7 +113,7 @@ export const Animator = {
}
}
Blockbench.dispatchEvent('display_default_pose', {reduced_updates});
if (!reduced_updates) scene.updateMatrixWorld()
if (!reduced_updates) Canvas.scene.updateMatrixWorld()
},
resetParticles(optimized) {
for (var path in Animator.particle_effects) {
Expand Down
3 changes: 3 additions & 0 deletions js/animations/molang.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,9 @@ new ValidatorCheck('molang_syntax', {
if (clear_string.match(/^[+*/.,?=&<>|]/)) {
issues.push('Expression starts with an invalid character')
}
if (clear_string.endsWith(';') && !clear_string.includes('return ')) {
issues.push('Complex expression with no return value. Remove the semicolon or add a return statement')
}
if (
(clear_string.match(/[\w.]\s+[\w.]/) &&
!keywords.find((k) => clear_string.includes(k))) ||
Expand Down
13 changes: 11 additions & 2 deletions js/auto_backup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,12 @@ export const AutoBackup = {

// Confirm selection
if (confirm_selection && projects.length > 1) {
let form: Record<string, FormElementOptions | '_'> = {};
let form: Record<string, FormElementOptions | '_'> = {
info: {
text: '⚠ Recovering models is only a backup. Please remember to always save your work to your device.',
type: 'info'
}
};
let keys: UUID[] = [];

projects.sort((a, b) => (b.date??0) - (a.date??0));
Expand Down Expand Up @@ -155,7 +160,7 @@ export const AutoBackup = {
}

projects = await new Promise<ProjectSave[]>((resolve, reject) => {
new Dialog({
let dialog = new Dialog({
id: 'recover_backup',
title: 'message.recover_backup.title',
form,
Expand All @@ -171,6 +176,10 @@ export const AutoBackup = {
resolve(to_open);
}
}).show();
let content = dialog.object.querySelector('.dialog_content') as HTMLElement;
if (content) {
content.style.maxHeight = (window.innerHeight - 150) + 'px';
}
})
}

Expand Down
2 changes: 1 addition & 1 deletion js/desktop.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export function initializeDesktopApp() {
//Load Model
export function loadOpenWithBlockbenchFile() {
function load(path) {
if (!path || path.length < 7 || !path.match(/\w\.\w+$/)) return;
if (!path || path.length < 7 || path.startsWith('--') || !path.match(/.\.\w+$/)) return;
var extension = pathToExtension(path);
if (extension == 'png') {
Blockbench.read([path], {readtype: 'image'}, (files) => {
Expand Down
27 changes: 26 additions & 1 deletion js/formats/bedrock/bedrock_voxel_shape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,32 @@ BARS.defineActions(function() {
size: bb.size()
}
});
if (type == 'selection_box') box_data.length = 1;
if (type == 'selection_box') {
let { origin, size } = box_data[0];
let minX = origin[0];
let minY = origin[1];
let minZ = origin[2];
let maxX = origin[0] + size[0];
let maxY = origin[1] + size[1];
let maxZ = origin[2] + size[2];
for (let i = 1; i < box_data.length; i++) {
const { origin, size } = box_data[i];
minX = Math.min(minX, origin[0]);
minY = Math.min(minY, origin[1]);
minZ = Math.min(minZ, origin[2]);
maxX = Math.max(maxX, origin[0] + size[0]);
maxY = Math.max(maxY, origin[1] + size[1]);
maxZ = Math.max(maxZ, origin[2] + size[2]);
}
box_data = [{
origin: [minX, minY, minZ],
size: [
maxX - minX,
maxY - minY,
maxZ - minZ
]
}]
};
let data = box_data as any;
if (box_data.length == 1 && box_data[0].origin.equals([-8, 0, -8]) && box_data[0].size.equals([16, 16, 16])) {
data = true;
Expand Down
72 changes: 70 additions & 2 deletions js/formats/minecraft/skin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,8 @@ Interface.definePanels(function() {
// Source: https://github.com/Mojang/bedrock-samples/, licensed under the Minecraft EULA
// With modifications for usability

// Clipbench.setText(compileJSON(Codecs.skin_model.compile({raw: false})).replace(/\n/g, '\n\t'))

skin_presets.steve = {
display_name: 'Player - Wide',
pose: true,
Expand Down Expand Up @@ -6873,7 +6875,7 @@ skin_presets.panda_baby = {
};
skin_presets.parrot = {
display_name: 'Parrot',
model: `{
model_bedrock: `{
"name": "parrot",
"external_textures": ["entity/parrot/parrot_red_blue.png"],
"texturewidth": 32,
Expand Down Expand Up @@ -6939,6 +6941,73 @@ skin_presets.parrot = {
]
}
]
}`,
model_java: `{
"name": "parrot",
"external_textures": ["entity/parrot/parrot_red_blue.png"],
"texturewidth": 32,
"textureheight": 32,
"bones": [
{
"name": "body",
"pivot": [0, 7.5, -3],
"rotation": [25, 0, 0],
"cubes": [
{"origin": [-1.5, 1.5, -4.5], "size": [3, 6, 3], "uv": [2, 8]}
]
},
{
"name": "wing0",
"parent": "body",
"pivot": [1.5, 7.1, -2.8],
"rotation": [10, 0, 0],
"cubes": [
{"origin": [1, 2.1, -4.3], "size": [1, 5, 3], "uv": [19, 8]}
]
},
{
"name": "wing1",
"parent": "body",
"pivot": [-1.5, 7.1, -2.8],
"rotation": [10, 0, 0],
"cubes": [
{"origin": [-2, 2.1, -4.3], "size": [1, 5, 3], "uv": [19, 8]}
]
},
{
"name": "head",
"pivot": [0, 8.3, -2.8],
"cubes": [
{"origin": [-1, 6.8, -3.8], "size": [2, 3, 2], "uv": [2, 2]},
{"origin": [-1, 9.8, -5.8], "size": [2, 1, 4], "uv": [10, 0]},
{"origin": [-0.5, 7.8, -4.7], "size": [1, 2, 1], "uv": [11, 7]},
{"origin": [-0.5, 8.035, -5.64], "size": [1, 2.025, 1], "uv": [16, 7]},
{"origin": [0, 9.1, -4.9], "size": [0, 5, 4], "uv": [2, 18]}
]
},
{
"name": "tail",
"pivot": [0, 2.9, 1.2],
"rotation": [50, 0, 0],
"cubes": [
{"origin": [-1.5, -0.1, 0.2], "size": [3, 4, 1], "uv": [22, 1]}
]
},
{
"name": "leg0",
"pivot": [1.5, 1, -0.5],
"cubes": [
{"origin": [0.5, -0.5, -1.5], "size": [1, 2, 1], "uv": [14, 18]}
]
},
{
"name": "leg1",
"pivot": [-0.5, 1, -0.5],
"cubes": [
{"origin": [-1.5, -0.5, -1.5], "size": [1, 2, 1], "uv": [14, 18]}
]
}
]
}`
};
skin_presets.phantom = {
Expand Down Expand Up @@ -10500,7 +10569,6 @@ skin_presets.zombie = {
pose: true,
model_java: `{
"name": "zombie",
"external_textures": ["entity/zombie/zombie.png"],
"texturewidth": 64,
"textureheight": 64,
"eyes": [
Expand Down
5 changes: 4 additions & 1 deletion js/interface/about.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ BARS.defineActions(() => {
icon: 'info',
category: 'blockbench',
click: function () {
let date = new Date();
const data = {
isApp,
version_label: Blockbench.version
version_label: Blockbench.version,
ann: date.dayOfYear() == 95 && date.getFullYear() == 2026, c: 0, link: Blockbench.openLink
};
jQuery.ajax({
url: 'https://api.github.com/repos/JannisX11/blockbench/releases/latest',
Expand Down Expand Up @@ -140,6 +142,7 @@ BARS.defineActions(() => {

<p style="margin-top: 20px">Released under the <a class="open-in-browser" href="https://github.com/JannisX11/blockbench/blob/master/LICENSE.MD">GPL 3.0 license</a></p>
<p><a class="open-in-browser" href="https://www.blockbench.net/privacy-policy">Privacy Policy</a></p>
<i class="material-icons icon" v-if="ann" style="position: absolute;font-size: 40px;color: rgb(215 79 79);max-width: unset;top: 338px;right: 51px;transform: rotate(8deg);" @click="c++; if (c==5) link('https://www.youtube.com/watch?v=ajZDIH61ww8')" title="Easter Egg">egg</i>

</div>`
}
Expand Down
2 changes: 1 addition & 1 deletion js/interface/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1150,7 +1150,7 @@ export class NumSlider extends Widget {
this.node.classList.add('is_colored');
}

//this.addLabel(data.label);
this.addLabel();

this.jq_inner
.on('mousedown touchstart', async (event) => {
Expand Down
33 changes: 22 additions & 11 deletions js/modeling/mirror_modeling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export const MirrorModeling = {
let parent = child.parent;
if (parent instanceof OutlinerNode == false) return 'root';

if ('origin' in parent && parent.origin[0] == center && MirrorModeling.isParentTreeSymmetrical(child, {center})) {
if ('origin' in parent && Math.epsilon(parent.origin[0], center) && MirrorModeling.isParentTreeSymmetrical(child, {center})) {
return parent;
} else {
let mirror_group_parent = getParentMirror(parent) as OutlinerNode & OutlinerNodeParentTraits;
Expand Down Expand Up @@ -185,8 +185,8 @@ export const MirrorModeling = {
parents.push(subject)
}
return parents.allAre(parent => {
if (parent.rotation && off_axes.some(axis => parent.rotation[axis])) return false;
if (parent.origin && !symmetry_axes.allAre(axis => parent.origin[axis] == center)) return false;
if (parent.rotation && off_axes.some(axis => !Math.epsilon(parent.rotation[axis], 0))) return false;
if (parent.origin && !symmetry_axes.allAre(axis => Math.epsilon(parent.origin[axis], center))) return false;
return true;
})
},
Expand Down Expand Up @@ -245,7 +245,10 @@ export const MirrorModeling = {
// post
if (!before_snapshop) undo_aspects.outliner = true;
if (!undo_aspects.groups) undo_aspects.groups = [];
undo_aspects.groups.safePush(group);
if (!undo_aspects.groups.includes(group)) {
// Must be new array so we don't accidentally change the undo input array
undo_aspects.groups = [...undo_aspects.groups, group];
}
},
element_types: {} as Record<string, MirrorModelingElementTypeOptions>,
registerElementType(type_class: any, options: MirrorModelingElementTypeOptions) {
Expand Down Expand Up @@ -310,7 +313,7 @@ Blockbench.on('init_edit', (args) => {
aspects.outliner = true;

selected_groups.forEach(group => {
if (group.origin[0] == (Format.centered_grid ? 0 : 8)) return;
if (Math.epsilon(group.origin[0], (Format.centered_grid ? 0 : 8))) return;

let mirror_group = Group.all.find(g => {
if (
Expand Down Expand Up @@ -366,13 +369,17 @@ Blockbench.on('finish_edit', ({aspects}) => {
if (aspects.group || aspects.groups || aspects.outliner) {
Canvas.updateAllBones();
}
} else if (aspects.group || aspects.groups) {
}
if (aspects.group || aspects.groups) {
let selected_groups = aspects.groups ?? [aspects.group];

selected_groups.forEach(group => {
let mirror_group = MirrorModeling.cached_elements[group.uuid]?.counterpart;
if (mirror_group && Group.all.includes(mirror_group)) {
let before_snapshot = mirror_group.getChildlessCopy();
before_snapshot.uuid = mirror_group.uuid;
MirrorModeling.updateParentNodeCounterpart(mirror_group, group);
MirrorModeling.insertGroupIntoUndo(mirror_group, aspects, before_snapshot);
}
})

Expand Down Expand Up @@ -421,7 +428,8 @@ MirrorModeling.registerElementType(Cube, {
!off_axes.some(axis => !Math.epsilon(element.to[axis], element2.to[axis], e)) &&
!symmetry_axes.some(axis => !Math.epsilon(element.size(axis), element2.size(axis), e)) &&
!symmetry_axes.some(axis => !Math.epsilon(element.to[axis]-center, center-element2.from[axis], e)) &&
isOppositeEuler(element.rotation, element2.rotation)
isOppositeEuler(element.rotation, element2.rotation) &&
element2.getDepth() == element.getDepth()
) {
return element2;
}
Expand Down Expand Up @@ -469,7 +477,8 @@ MirrorModeling.registerElementType(Mesh, {
symmetry_axes.find(axis => !Math.epsilon(this_center[axis]-center, center-other_center[axis], ep)) == undefined &&
off_axes.find(axis => !Math.epsilon(element.origin[axis], element2.origin[axis], e)) == undefined &&
off_axes.find(axis => !Math.epsilon(this_center[axis], other_center[axis], ep)) == undefined &&
isOppositeEuler(element.rotation, element2.rotation)
isOppositeEuler(element.rotation, element2.rotation) &&
element2.getDepth() == element.getDepth()
) {
return element2;
}
Expand Down Expand Up @@ -698,7 +707,8 @@ MirrorModeling.registerElementType(ArmatureBone, {
if (
isOppositeVector(element.position, element2.position, center) &&
isOppositeEuler(element.rotation, element2.rotation) &&
MirrorModeling.isParentTreeOpposite(element, element2)
MirrorModeling.isParentTreeOpposite(element, element2) &&
element2.getDepth() == element.getDepth()
) {
return element2;
}
Expand Down Expand Up @@ -737,7 +747,8 @@ MirrorModeling.registerElementType(Billboard, {
for (let element2 of (Billboard.all as Billboard[])) {
if (element == element2) continue;
if (
isOppositeVector(element.position, element2.position, center)
isOppositeVector(element.position, element2.position, center) &&
element2.getDepth() == element.getDepth()
) {
return element2;
}
Expand Down Expand Up @@ -766,7 +777,7 @@ export function symmetrizeArmature(armature: Armature, mesh: Mesh, affected_vkey
// For each vkey, copy its value on each bone to the other side
for (let vkey in mesh.vertices) {
let position = mesh.vertices[vkey];
if (position[0] == 0) continue;
if (!Math.epsilon(position[0], 0)) continue;
if (affected_vkeys.has(vkey) == false) continue;
let opposite = getOppositeMeshVertex(mesh, vkey);
if (!opposite) continue;
Expand Down
Loading