Merge branch 'master' into next

This commit is contained in:
JannisX11 2024-02-29 18:47:56 +01:00
commit c62729362f
26 changed files with 606 additions and 88 deletions

View File

@ -147,6 +147,9 @@
.dialog_bar > label {
width: var(--max_label_width);
}
.dialog_bar > .molang_input {
width: calc(100% - var(--max_label_width));
}
/*.dialog_bar::after {
content: "";
clear: both;
@ -1493,6 +1496,7 @@ dialog#edit_bedrock_binding > .dialog_wrapper > .dialog_content {
color: var(--color-subtle_text);
margin-left: auto;
cursor: inherit;
overflow-wrap: anywhere;
}

View File

@ -2672,6 +2672,7 @@ span.controller_state_section_info {
padding: 2px;
min-height: 160px;
max-height: 232px;
line-height: 0;
}
#palette_list .color {
display: inline-block;

View File

@ -178,7 +178,8 @@ class AnimationControllerState {
if (this.transitions.length) {
object.transitions = this.transitions.map(transition => {
let state = this.controller.states.find(s => s.uuid == transition.target);
return new oneLiner({[state ? state.name : 'missing_state']: transition.condition})
let condition = transition.condition.replace(/\n/g, '');
return new oneLiner({[state ? state.name : 'missing_state']: condition})
})
}
if (this.blend_transition) object.blend_transition = this.blend_transition;
@ -1723,7 +1724,7 @@ Interface.definePanels(() => {
</ul>
<div class="controller_state_input_bar">
<label>${tl('animation_controllers.state.blend_transition')}</label>
<numeric-input style="width: 70px;" v-model.number="state.blend_transition" min="0" step="0.05" />
<numeric-input style="width: 70px;" v-model.number="state.blend_transition" :min="0" :step="0.05" />
</div>
<div class="controller_state_input_bar">
<label :for="state.uuid + '_shortest_path'">${tl('animation_controllers.state.shortest_path')}</label>

View File

@ -2016,7 +2016,7 @@ BARS.defineActions(function() {
"position": [4, 12, -2],
"size": [4, 12, 4],
"origin": [5, 22, 0],
"rotation": [-1, 0, 3],
"rotation": [15, 0, 0],
"faces": {
"north": {"uv": [44, 20, 48, 32]},
"east": {"uv": [40, 20, 44, 32]},
@ -2031,7 +2031,7 @@ BARS.defineActions(function() {
"position": [3.75, 11.75, -2.25],
"size": [4.5, 12.5, 4.5],
"origin": [5, 22, 0],
"rotation": [-1, 0, 3],
"rotation": [15, 0, 0],
"faces": {
"north": {"uv": [44, 36, 48, 48]},
"east": {"uv": [40, 36, 44, 48]},
@ -2079,7 +2079,7 @@ BARS.defineActions(function() {
"position": [4, 11.5, -2],
"size": [3, 12, 4],
"origin": [5, 21.5, 0],
"rotation": [-1, 0, 3],
"rotation": [15, 0, 0],
"faces": {
"north": {"uv": [44,20,47,32]},
"east": {"uv": [40,20,44,32]},
@ -2094,7 +2094,7 @@ BARS.defineActions(function() {
"position": [3.75, 11.25, -2.25],
"size": [3.5, 12.5, 4.5],
"origin": [5, 21.5, 0],
"rotation": [-1, 0, 3],
"rotation": [15, 0, 0],
"faces": {
"north": {"uv": [44,36,47,48]},
"east": {"uv": [40,36,44,48]},
@ -2193,6 +2193,7 @@ BARS.defineActions(function() {
window.player_attachable_reference_model = player_attachable_reference_model;
player_attachable_reference_model.updateArmVariant = player_preview_model.updateArmVariant;
player_attachable_reference_model.updateArmVariant();
let camera_preset_1st = {
name: tl('action.bedrock_animation_mode.attachable_first'),

View File

@ -1688,7 +1688,12 @@ class Toolbar {
if (arr.equals(this.default_children)) {
delete BARS.stored[this.id];
}
localStorage.setItem('toolbars', JSON.stringify(BARS.stored))
// Temporary fix
try {
localStorage.setItem('toolbars', JSON.stringify(BARS.stored))
} catch (err) {
localStorage.removeItem('backup_model');
}
return this;
}
reset() {

View File

@ -744,7 +744,7 @@ window.Dialog = class Dialog {
handle.append(title);
let jq_dialog = $(this.object);
this.max_label_width = 0;
this.max_label_width = 140;
this.uses_wide_inputs = false;
let wrapper = document.createElement('div');

View File

@ -478,7 +478,7 @@ const MenuBar = {
'open_dev_tools',
{name: 'Error Log', condition: () => window.ErrorLog.length, icon: 'error', color: 'red', keybind: {toString: () => window.ErrorLog.length.toString()}, click() {
let lines = window.ErrorLog.slice(0, 64).map((error) => {
return Interface.createElement('p', {}, `${error.message}\n - In .${error.file.split(location.origin).join('')} : ${error.line}`);
return Interface.createElement('p', {style: 'word-break: break-word;'}, `${error.message}\n - In .${error.file.split(location.origin).join('')} : ${error.line}`);
})
new Dialog({
id: 'error_log',

View File

@ -134,7 +134,12 @@ Vue.component('numeric-input', {
<input class="dark_bordered focusable_input" :value="string_value" @input="change($event.target.value)" inputmode="decimal" lang="en" @focusout="resolve($event)" @dblclick="resolve($event)">
<div class="tool numeric_input_slider" @mousedown="slide($event)" @touchstart="slide($event)"><i class="material-icons">code</i></div>
</div>
`
`,
mounted() {
if (typeof this.min == 'string') console.warn('Argument "min" should be set as a numeric property via "v-bind:"')
if (typeof this.max == 'string') console.warn('Argument "max" should be set as a numeric property via "v-bind:"')
if (typeof this.step == 'string') console.warn('Argument "step" should be set as a numeric property via "v-bind:"')
}
})
Vue.component('dynamic-icon', {
props: {

View File

@ -106,7 +106,8 @@ class Codec extends EventSystem {
}
async export() {
if (Object.keys(this.export_options).length) {
await this.promptExportOptions();
let result = await this.promptExportOptions();
if (result === null) return;
}
Blockbench.export({
resource_id: 'model',

View File

@ -1051,7 +1051,8 @@ var codec = new Codec('fbx', {
},
async export() {
if (Object.keys(this.export_options).length) {
await this.promptExportOptions();
let result = await this.promptExportOptions();
if (result === null) return;
}
var scope = this;
if (isApp) {

View File

@ -345,7 +345,8 @@ var codec = new Codec('gltf', {
}
},
async export() {
await this.promptExportOptions();
let options = await this.promptExportOptions();
if (options === null) return;
let content = await this.compile();
await new Promise(r => setTimeout(r, 20));
Blockbench.export({

View File

@ -127,8 +127,12 @@ async function loadImages(files, event) {
if (img.naturalHeight == img.naturalWidth && [64, 128].includes(img.naturalWidth)) {
options.minecraft_skin = 'format.skin';
}
if (Project && !Format.image_editor && Condition(Panels.textures.condition)) {
options.texture = 'action.import_texture';
if (Project && Condition(Panels.textures.condition)) {
if (Format.image_editor) {
options.texture = 'message.load_images.add_image';
} else {
options.texture = 'action.import_texture';
}
}
if (Project && (!Project.box_uv || Format.optional_box_uv)) {
options.extrude_with_cubes = 'dialog.extrude.title';
@ -138,9 +142,12 @@ async function loadImages(files, event) {
if (method == 'texture') {
let new_textures = [];
Undo.initEdit({textures: new_textures});
files.forEach(function(f) {
files.forEach(function(f, i) {
let tex = new Texture().fromFile(f).add().fillParticle();
new_textures.push(tex);
if (Format.image_editor && i == 0) {
tex.select();
}
});
Undo.finishEdit('Add texture');

View File

@ -748,9 +748,11 @@ class Cube extends OutlinerElement {
} else if (scope.autouv === 1) {
function calcAutoUV(face, size) {
var sx = scope.faces[face].uv[0]
var sy = scope.faces[face].uv[1]
var rot = scope.faces[face].rotation
size[0] = Math.abs(size[0]);
size[1] = Math.abs(size[1]);
var sx = scope.faces[face].uv[0];
var sy = scope.faces[face].uv[1];
var rot = scope.faces[face].rotation;
//Match To Rotation
if (rot === 90 || rot === 270) {

View File

@ -262,7 +262,6 @@ class Group extends OutlinerNode {
return array;
}
showContextMenu(event) {
Prop.active_panel = 'outliner'
if (this.locked) return this;
if (Group.selected != this) this.select(event);
this.menu.open(event, this)

View File

@ -369,7 +369,6 @@ class OutlinerElement extends OutlinerNode {
return this;
}
showContextMenu(event) {
Prop.active_panel = 'outliner'
if (this.locked) return this;
if (!this.selected) {
this.select()

View File

@ -436,7 +436,8 @@ class Plugin {
this.unload()
this.tags.empty();
this.dependencies.empty();
Plugins.all.remove(this)
Plugins.all.remove(this);
this.details = null;
if (this.source == 'file') {
this.loadFromFile({path: this.path}, false)
@ -808,10 +809,14 @@ async function loadInstalledPlugins() {
}
} else if (plugin.source == 'url') {
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`);
if (plugin.path) {
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`);
} else {
Plugins.installed.remove(plugin);
}
} else {
if (Plugins.all.find(p => p.id == plugin.id)) {
@ -1317,7 +1322,7 @@ BARS.defineActions(function() {
</tr>
<tr v-if="selected_plugin.details.website">
<td>Website</td>
<td>{{ selected_plugin.details.website }}</td>
<td><a :href="selected_plugin.details.website" :title="selected_plugin.details.website">{{ reduceLink(selected_plugin.details.website) }}</a></td>
</tr>
<tr v-if="selected_plugin.details.repository">
<td>Plugin source</td>

View File

@ -311,11 +311,11 @@ const PredicateOverrideEditor = {
<select-input v-model="generator.type" :options="available_predicate_options" @input="updateGeneratorType()" />
<label>${tl('dialog.predicate_overrides.variants')}</label>
<numeric-input v-model.number="generator.variants" min="1" step="1" style="width: 70px;" />
<numeric-input v-model.number="generator.variants" :min="1" :step="1" style="width: 70px;" />
<template v-if="generator.type == 'custom_model_data'">
<label>${tl('dialog.predicate_overrides.start_value')}</label>
<numeric-input v-model.number="generator.start_value" min="0" step="1" style="width: 45px;" />
<numeric-input v-model.number="generator.start_value" :min="0" :step="1" style="width: 45px;" />
</template>
<label>${tl('dialog.predicate_overrides.model')}</label>

View File

@ -655,7 +655,7 @@ class Preview {
position: {label: 'dialog.save_angle.position', type: 'vector', dimensions: 3, value: position},
target: {label: 'dialog.save_angle.target', type: 'vector', dimensions: 3, value: target, condition: ({rotation_mode}) => rotation_mode == 'target'},
rotation: {label: 'dialog.save_angle.rotation', type: 'vector', dimensions: 2, condition: ({rotation_mode}) => rotation_mode == 'rotation'},
zoom: {label: 'dialog.save_angle.zoom', type: 'number', value: 1, condition: result => (result.projection == 'orthographic')},
zoom: {label: 'dialog.save_angle.zoom', type: 'number', value: Math.roundTo(scope.camOrtho.zoom || 1, 4), condition: result => scope.isOrtho},
},
onFormChange(form) {
if (form.rotation_mode !== rotation_mode) {
@ -677,7 +677,7 @@ class Preview {
position: formResult.position,
target: formResult.target,
}
if (this.isOrtho) preset.zoom = this.camOrtho.zoom;
if (scope.isOrtho) preset.zoom = scope.camOrtho.zoom;
let presets = localStorage.getItem('camera_presets');
try {
@ -1894,6 +1894,444 @@ window.addEventListener("gamepadconnected", function(event) {
}
});
if (new Date().getDate() == 1 && new Date().getMonth() == 3) {
class RainbowRace {
constructor() {
this.velocity = 0.03;
this.y_velocity = 0;
this.steering_angle = 0;
this.steer_direction = 0;
this.turn_axis = new THREE.Vector3(0, 1, 0);
this.step = 4;
this.path = [];
this.waypoints = [];
this.material = new THREE.MeshPhongMaterial({
color: 0xffffff,
flatShading: true,
vertexColors: true,
shininess: 0,
side: THREE.DoubleSide
});
this.geometry = new THREE.BufferGeometry();
this.track_width = 6;
this.track_length = 250;
this.track_length_back = 12;
this.collision_length = 64;
this.ticks = 0;
this.playing = false;
this.on_track = true;
this.score = 0;
for (let i = 0; i < this.track_length; i++) {
this.addPathPoint(i < this.track_length_back);
}
let colors = [
[128/255, 75/255, 227/255],
[ 21/255, 118/255, 240/255],
[ 68/255, 189/255, 96/255],
[253/255, 203/255, 42/255],
[252/255, 137/255, 46/255],
[240/255, 40/255, 67/255],
]
let bg_color = new THREE.Color(CustomTheme.data.colors.dark);
let vertex_count = 4 * this.track_width * this.track_length;
this.geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertex_count * 3), 3));
this.geometry.setAttribute('color', new THREE.BufferAttribute( new Float32Array(vertex_count * 3), 3));
for (let i = 0; i < this.track_length * 20; i++) {
for (let j = 0; j < 4; j++) {
let k = 0;
for (let color of colors) {
let color_factor = 1;
if ( (k==0 && j%2==0) || (k==5 && j%2==1) ) {
color_factor = 1.5;
}
let fade = Math.min(Math.pow(i / (this.track_length * 0.97), 2), 1);
this.geometry.attributes.color.setXYZ(
(i*6 + k) * 4 + j,
Math.lerp(color[0] * color_factor, bg_color.r, fade),
Math.lerp(color[1] * color_factor, bg_color.g, fade),
Math.lerp(color[2] * color_factor, bg_color.b, fade)
);
k++;
}
}
}
this.updateGeometry();
this.coin_geometry = new THREE.OctahedronGeometry(5, 0);
this.coin_geometry2 = new THREE.OctahedronGeometry(6.2, 0);
this.coin_material = new THREE.MeshPhongMaterial({
color: new THREE.Color(3.3, 3.2, 0.8),
flatShading: false,
shininess: 1
});
this.coin_material2 = new THREE.MeshPhongMaterial({
color: 0xff9c2b,
flatShading: true,
shininess: 0,
opacity: 0.5,
transparent: true
});
this.coins = [];
this.scene = new THREE.Object3D();
this.world = new THREE.Object3D();
this.track = new THREE.Mesh(this.geometry, this.material);
this.scene.add(this.world);
this.world.add(this.track);
this.position = new THREE.Vector3();
this.keys = {
w: false,
s: false,
a: false,
d: false,
};
document.addEventListener('keydown', event => {
if (event.key == 'w') this.keys.w = true;
if (event.key == 's') this.keys.s = true;
if (event.key == 'a') this.keys.a = true;
if (event.key == 'd') this.keys.d = true;
})
document.addEventListener('keyup', event => {
if (event.key == 'w') this.keys.w = false;
if (event.key == 's') this.keys.s = false;
if (event.key == 'a') this.keys.a = false;
if (event.key == 'd') this.keys.d = false;
})
this.interval = setInterval(() => {
this.tick();
}, 1000/30);
}
async start() {
if (this.playing) this.stop();
Canvas.scene.add(this.scene);
three_grid.visible = false;
let model_size = 3 / calculateVisibleBox()[0];
Project.model_3d.scale.set(model_size, model_size, model_size);
let path_scale = 3;
this.track.scale.set(path_scale, path_scale, path_scale);
this.velocity = 0;
this.y_velocity = 0;
this.on_track = true;
this.playing = true;
this.score = 0;
this.steer_direction = 0;
this.scene.rotation.set(0, 0, 0);
this.world.position.set(0, 0, -this.track_length_back * path_scale);
this.track.position.set(0, 0, 0);
this.track.rotation.set(0, 0, 0);
let camera_preset = {
projection: 'perspective',
position: [0, 32, -40],
target: [0, 20, 0]
};
Preview.selected.loadAnglePreset(camera_preset);
this.front_right_wheel = Group.all.find(g => {
let name = g.name.toLowerCase();
return name.includes('wheel') && name.includes('front') && name.includes('right')
})?.mesh;
this.front_left_wheel = Group.all.find(g => {
let name = g.name.toLowerCase();
return name.includes('wheel') && name.includes('front') && name.includes('left')
})?.mesh;
let rear_wheel = Group.all.find(g => {
let name = g.name.toLowerCase();
return (name.includes('wheel') || name.includes('axle') || name.includes('axis')) && (name.includes('rear') || name.includes('back'))
})
this.rotation_adjustment = Math.PI;
if (rear_wheel) {
if (rear_wheel.origin[2] < 0) {
this.rotation_adjustment = 0;
Project.model_3d.position.z -= rear_wheel.origin[2] * model_size;
} else {
Project.model_3d.position.z += rear_wheel.origin[2] * model_size;
}
}
return this;
}
stop() {
Canvas.scene.remove(this.scene);
three_grid.visible = true;
this.playing = false;
Project.model_3d.rotation.y = 0;
Project.model_3d.scale.set(1, 1, 1);
Project.model_3d.position.set(0, 0, 0);
Blockbench.setStatusBarText();
if (this.timeout) {
clearTimeout(this.timeout);
delete this.timeout;
}
}
tick() {
if (!this.playing) {
return;
}
let steering_amount = 0.2 * (1-Math.exp(0.2 * (this.velocity - 18)));
if (this.keys.a) {
this.steering_angle -= steering_amount * (1+Math.pow( this.steering_angle / 2, 3));
} else if (this.keys.d) {
this.steering_angle += steering_amount * (1+Math.pow(-this.steering_angle / 2, 3));
} else if (this.steering_angle) {
//this.steering_angle -= (this.steering_angle > 0 ? 1 : -1) * 0.14;
//if (Math.abs(this.steering_angle) < 0.1) this.steering_angle = 0;
this.steering_angle *= 0.8;
}
this.steering_angle = Math.clamp(this.steering_angle, -1.5, 1.4);
if (this.keys.w) {
this.velocity += 0.1 * (1-Math.exp(0.3 * (this.velocity - 7)));
} else if (this.keys.s) {
this.velocity -= this.velocity > 0 ? 0.15 : 0.1;
} else {
this.velocity -= this.velocity > 0 ? 0.1 : -0.1;
if (Math.abs(this.velocity) < 0.1) this.velocity = 0;
}
this.velocity = Math.clamp(this.velocity, -4, 10);
if (!this.on_track) {
this.y_velocity = Math.clamp(this.y_velocity - 0.2, -4, 4);
}
let movement = new THREE.Vector3(0, -this.y_velocity, -this.velocity);
this.steer_direction = this.steer_direction + this.steering_angle * Math.min(this.velocity, 1.4) * 0.02;
movement.applyAxisAngle(this.turn_axis, -this.steer_direction + 0);
this.scene.rotation.y = Math.lerp(this.scene.rotation.y, this.steer_direction, 0.1);
Project.model_3d.rotation.y = this.rotation_adjustment + this.scene.rotation.y - this.steer_direction;
this.world.position.add(movement);
if (this.front_right_wheel) this.front_right_wheel.rotation.y = this.steering_angle * -0.5;
if (this.front_left_wheel) this.front_left_wheel.rotation.y = this.steering_angle * -0.5;
let closest_waypoint = this.getClosestWaypoint();
if (closest_waypoint) {
let segments_regenerated = false;
while (closest_waypoint.index > this.track_length_back) {
this.generatePathStep();
segments_regenerated = true;
closest_waypoint.index--;
}
if (segments_regenerated) this.updateGeometry();
} else if (this.on_track) {
// Fall off
this.fall();
}
this.coins.forEach(coin => {
coin.rotation.y += 0.01;
coin.position.y = 10 + Math.sin(this.ticks * 0.1) * 0.8;
let offset = Reusable.vec2.set(0, 0, 0);
coin.localToWorld(offset);
let distance = offset.length();
if (distance && distance < 34) {
this.collectCoin(coin);
}
});
Blockbench.setStatusBarText(`Score: ${separateThousands(this.score)}`);
this.ticks++;
}
fall() {
this.on_track = false;
let previous_highscore = localStorage.getItem('rainbow_game.highscore') || 0;
if (this.score > previous_highscore) {
Blockbench.showQuickMessage(`New Highscore: ${separateThousands(this.score)}`);
localStorage.setItem('rainbow_game.highscore', this.score);
} else {
Blockbench.showQuickMessage(`Score: ${separateThousands(this.score)}`);
}
this.timeout = setTimeout(() => {
this.stop();
}, 1000)
}
createCoin(position) {
let coin = new THREE.Mesh(this.coin_geometry, this.coin_material);
let coin_glow = new THREE.Mesh(this.coin_geometry2, this.coin_material2);
coin.add(coin_glow);
coin.position.copy(position);
coin.position.y = 10;
coin.scale.y = 1.4;
this.world.add(coin);
this.coins.push(coin);
if (this.coins.length > 40) {
this.world.remove(this.coins.shift());
}
}
collectCoin(coin) {
this.score += 5;
setTimeout(() => {
this.coins.remove(coin);
}, 40);
let interval = setInterval(() => {
coin.position.y *= 1.1;
coin.scale.multiplyScalar(0.95);
}, 16)
setTimeout(() => {
clearInterval(interval);
this.coins.remove(coin);
this.world.remove(coin);
}, 150);
}
generatePathStep() {
this.track.rotation.y += this.path[0];
let offset = new THREE.Vector3(0, 0, -this.step * this.track.scale.x);
offset.applyAxisAngle(this.turn_axis, this.track.rotation.y);
//offset.x *= this.track.scale.x;
//offset.z *= this.track.scale.x;
this.track.position.sub(offset);
if (Math.random() < 1/24) {
// Coin
let waypoint = this.waypoints[this.collision_length - 2];
let position = Reusable.vec1.copy(waypoint.position);
position.applyMatrix4(this.track.matrix);
this.createCoin(position);
}
this.path.shift();
this.addPathPoint();
this.score++;
}
getClosestWaypoint() {
let position = new THREE.Vector3();
this.track.worldToLocal(position);
let distance = new THREE.Vector3();
let waypoints_in_reach = this.waypoints.filter((waypoint, index) => {
waypoint.index = index;
waypoint.distance = distance.copy(position).sub(waypoint.position).length();
return waypoint.distance <= (this.track_width + 2);
});
waypoints_in_reach.sort((a, b) => a.distance - b.distance);
return waypoints_in_reach[0];
}
updateGeometry() {
let segment_width = 2;
let angle = 0;
let trailhead = new THREE.Vector3(0, 0, 0);
let offset1 = new THREE.Vector3(0, 0, 0);
let offset2 = new THREE.Vector3(0, 0, 0);
let offset3 = new THREE.Vector3(0, 0, 0);
let offset4 = new THREE.Vector3(0, 0, 0);
let quad_index = 0;
let indices = [];
let positions = [];
this.waypoints.empty();
let i = 0;
for (let curvature of this.path) {
let old_angle = angle;
angle += curvature;
if (i < this.collision_length) {
this.waypoints.push({position: new THREE.Vector3().copy(trailhead)});
}
for (let x = -3; x < 3; x++) {
offset1.set(x * segment_width, 0, 0)
offset1.applyAxisAngle(this.turn_axis, old_angle);
offset1.add(trailhead);
positions.push(offset1.x, offset1.y, offset1.z);
offset2.set((x+1) * segment_width, 0, 0)
offset2.applyAxisAngle(this.turn_axis, old_angle);
offset2.add(trailhead);
positions.push(offset2.x, offset2.y, offset2.z);
offset3.set(x * segment_width, 0, this.step);
offset3.applyAxisAngle(this.turn_axis, angle);
offset3.add(trailhead);
positions.push(offset3.x, offset3.y, offset3.z);
offset4.set((x+1) * segment_width, 0, this.step);
offset4.applyAxisAngle(this.turn_axis, angle);
offset4.add(trailhead);
positions.push(offset4.x, offset4.y, offset4.z);
indices.push(quad_index*4 + 0, quad_index*4 + 3, quad_index*4 + 1, quad_index*4 + 0, quad_index*4 + 2, quad_index*4 + 3);
quad_index++;
}
offset1.set(0, -0.005, this.step);
offset1.applyAxisAngle(this.turn_axis, angle);
trailhead.add(offset1);
i++;
}
this.geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) );
this.geometry.setIndex(indices);
}
addPathPoint(straight) {
let last_point = this.path.last() || 0;
let influence = straight ? 0 : Math.pow(Math.randomab(-1, 1), 3) * 0.5;
this.path.push(Math.lerp(last_point, influence, 0.075));
}
}
RainbowRace.start = () => {
if (!RainbowRace.current_race) {
RainbowRace.current_race = new RainbowRace();
}
RainbowRace.current_race.start();
}
RainbowRace.stop = () => {
RainbowRace.current_race.stop();
}
Interface.definePanels(() => {
let buttons = [
Interface.createElement('div', {}, Blockbench.getIconNode('play_arrow')),
Interface.createElement('div', {}, Blockbench.getIconNode('pause')),
];
let controls = Interface.createElement('div', {id: 'rainbow_game_controls'}, buttons);
Interface.preview.append(controls);
buttons[0].onclick = RainbowRace.start;
buttons[1].onclick = RainbowRace.stop;
Blockbench.addCSS(`
#rainbow_game_controls {
width: 64px;
height: 30px;
margin: auto;
left: 0;
right: 0;
top: 1px;
background-color: var(--color-ui);
display: flex;
position: absolute;
z-index: 10;
border-bottom-right-radius: 2px;
border-bottom-left-radius: 2px;
}
#rainbow_game_controls > div {
height: 100%;
cursor: pointer;
padding: 4px;
text-align: center;
}
#rainbow_game_controls > div:hover {
color: var(--color-light);
}
`);
})
window.RainbowRace = RainbowRace;
}
//Init/Update
function initCanvas() {

View File

@ -672,6 +672,7 @@ class ReferenceImage {
static image_extensions = ['png', 'jpg', 'jpeg', 'bmp', 'tiff', 'tif', 'gif'];
static video_extensions = ['mp4', 'wmv', 'mov'];
}
ReferenceImage.supported_extensions = ['png', 'jpg', 'jpeg', 'webp', 'bmp', 'tiff', 'tif', 'gif'];
ReferenceImage.prototype.menu = new Menu([
new MenuSeparator('settings'),
{

View File

@ -194,8 +194,14 @@ class TextureLayer {
}
down_layer.expandTo(this.offset, this.offset.slice().V2_add(this.width, this.height));
down_layer.ctx.imageSmoothingEnabled = false;
down_layer.ctx.filter = `opacity(${this.opacity / 100})`;
down_layer.ctx.globalCompositeOperation = Painter.getBlendModeCompositeOperation(this.blend_mode);
down_layer.ctx.drawImage(this.canvas, this.offset[0] - down_layer.offset[0], this.offset[1] - down_layer.offset[1], this.scaled_width, this.scaled_height);
down_layer.ctx.filter = '';
down_layer.ctx.globalCompositeOperation = 'source-over';
let index = this.texture.layers.indexOf(this);
this.texture.layers.splice(index, 1);
if (this.texture.selected_layer == this) {

View File

@ -355,7 +355,9 @@ const Painter = {
if (Painter.currentPixel[0] === x && Painter.currentPixel[1] === y) return;
Painter.currentPixel = [x, y];
Painter.brushChanges = true;
UVEditor.vue.last_brush_position.V2_set(x, y);
if (!is_opposite) {
UVEditor.vue.last_brush_position.V2_set(x, y);
}
let uvFactorX = texture.width / texture.getUVWidth();
let uvFactorY = texture.display_height / texture.getUVHeight();

View File

@ -1242,34 +1242,58 @@ class Texture {
})
scope.edit((canvas) => {
let temp_canvas = document.createElement('canvas');
let temp_ctx = temp_canvas.getContext('2d');
let resizeCanvas = (ctx) => {
temp_canvas.width = ctx.canvas.width;
temp_canvas.height = ctx.canvas.height;
temp_ctx.drawImage(ctx.canvas, 0, 0);
scope.canvas.width = formResult.size[0];
scope.canvas.height = formResult.size[1];
let new_ctx = scope.canvas.getContext('2d');
new_ctx.imageSmoothingEnabled = false;
if (formResult.mode == 'crop') {
switch (formResult.fill) {
case 'transparent':
new_ctx.drawImage(scope.img, 0, 0, scope.width, scope.height);
break;
case 'color':
new_ctx.fillStyle = ColorPanel.get();
new_ctx.fillRect(0, 0, formResult.size[0], formResult.size[1])
new_ctx.clearRect(0, 0, scope.width, scope.height)
new_ctx.drawImage(scope.img, 0, 0, scope.width, scope.height);
break;
case 'repeat':
for (var x = 0; x < formResult.size[0]; x += scope.width) {
for (var y = 0; y < formResult.size[1]; y += scope.height) {
new_ctx.drawImage(scope.img, x, y, scope.width, scope.height);
if (ctx.canvas.width == scope.canvas.width && ctx.canvas.height == scope.canvas.height) {
ctx.canvas.width = formResult.size[0];
ctx.canvas.height = formResult.size[1];
} else if (formResult.mode == 'scale') {
ctx.canvas.width = Math.round(ctx.canvas.width * (formResult.size[0] / scope.canvas.width));
ctx.canvas.height = Math.round(ctx.canvas.height * (formResult.size[1] / scope.canvas.height));
}
ctx.imageSmoothingEnabled = false;
if (formResult.mode == 'crop') {
switch (formResult.fill) {
case 'transparent':
ctx.drawImage(temp_canvas, 0, 0, temp_canvas.width, temp_canvas.height);
break;
case 'color':
ctx.fillStyle = ColorPanel.get();
ctx.fillRect(0, 0, formResult.size[0], formResult.size[1])
ctx.clearRect(0, 0, temp_canvas.width, temp_canvas.height)
ctx.drawImage(temp_canvas, 0, 0, temp_canvas.width, temp_canvas.height);
break;
case 'repeat':
for (var x = 0; x < formResult.size[0]; x += temp_canvas.width) {
for (var y = 0; y < formResult.size[1]; y += temp_canvas.height) {
ctx.drawImage(temp_canvas, x, y, temp_canvas.width, temp_canvas.height);
}
}
}
break;
break;
}
} else {
ctx.drawImage(temp_canvas, 0, 0, ctx.canvas.width, ctx.canvas.height);
}
}
if (scope.layers_enabled && scope.layers.length) {
for (let layer of scope.layers) {
resizeCanvas(layer.ctx);
if (formResult.mode == 'scale') {
layer.offset[0] = Math.round(layer.offset[0] * (formResult.size[0] / scope.width));
layer.offset[1] = Math.round(layer.offset[1] * (formResult.size[1] / scope.height));
}
}
} else {
new_ctx.drawImage(scope.img, 0, 0, formResult.size[0], formResult.size[1]);
resizeCanvas(scope.ctx);
}
scope.width = formResult.size[0];
scope.height = formResult.size[1];
@ -1326,6 +1350,7 @@ class Texture {
Undo.finishEdit('Resize texture');
UVEditor.vue.updateTexture();
setTimeout(updateSelection, 100);
}
})
@ -1658,7 +1683,7 @@ class Texture {
this.ctx.filter = '';
this.ctx.globalCompositeOperation = 'source-over';
if (!Format.image_editor) {
if (!Format.image_editor && this.getMaterial()) {
this.getMaterial().map.needsUpdate = true;
}
if (update_data_url) {

View File

@ -572,6 +572,7 @@ const UVEditor = {
if (isNaN(face.uv[vkey][axis])) face.uv[vkey][axis] = start;
})
})
Mesh.preview_controller.updateUV(mesh);
})
this.displayTools()
this.disableAutoUV()
@ -729,14 +730,14 @@ const UVEditor = {
face.uv[0] = Math.min(face.uv[0], face.uv[2]);
face.uv[1] = Math.min(face.uv[1], face.uv[3]);
if (side == 'north' || side == 'south') {
width = obj.size(0);
height = obj.size(1);
width = Math.abs(obj.size(0));
height = Math.abs(obj.size(1));
} else if (side == 'east' || side == 'west') {
width = obj.size(2);
height = obj.size(1);
width = Math.abs(obj.size(2));
height = Math.abs(obj.size(1));
} else if (side == 'up' || side == 'down') {
width = obj.size(0);
height = obj.size(2);
width = Math.abs(obj.size(0));
height = Math.abs(obj.size(2));
}
if (face.rotation % 180) {
[width, height] = [height, width];
@ -2888,21 +2889,28 @@ Interface.definePanels(function() {
})
} else if (element instanceof Mesh) {
function setUV(angle) {
scope.selected_faces.forEach(fkey => {
let face = element.faces[fkey];
if (!face) return;
let sin = Math.sin(Math.degToRad(angle));
let cos = Math.cos(Math.degToRad(angle));
face.vertices.forEach(vkey => {
if (!face.uv[vkey]) return;
face.uv[vkey][0] = face.old_uv[vkey][0] - face_center[0];
face.uv[vkey][1] = face.old_uv[vkey][1] - face_center[1];
let a = (face.uv[vkey][0] * cos - face.uv[vkey][1] * sin);
let b = (face.uv[vkey][0] * sin + face.uv[vkey][1] * cos);
face.uv[vkey][0] = Math.clamp(a + face_center[0], 0, UVEditor.getUVWidth());
face.uv[vkey][1] = Math.clamp(b + face_center[1], 0, UVEditor.getUVHeight());
})
})
}
setUV(angle);
let e = 0.6;
scope.selected_faces.forEach(fkey => {
let face = element.faces[fkey];
if (!face) return;
face.vertices.forEach(vkey => {
if (!face.uv[vkey]) return;
let sin = Math.sin(Math.degToRad(angle));
let cos = Math.cos(Math.degToRad(angle));
face.uv[vkey][0] = face.old_uv[vkey][0] - face_center[0];
face.uv[vkey][1] = face.old_uv[vkey][1] - face_center[1];
let a = (face.uv[vkey][0] * cos - face.uv[vkey][1] * sin);
let b = (face.uv[vkey][0] * sin + face.uv[vkey][1] * cos);
face.uv[vkey][0] = Math.clamp(a + face_center[0], 0, UVEditor.getUVWidth());
face.uv[vkey][1] = Math.clamp(b + face_center[1], 0, UVEditor.getUVHeight());
})
let e = 0.6;
face.vertices.forEach((vkey, i) => {
for (let j = i+1; j < face.vertices.length; j++) {
let relative_angle = Math.radToDeg(Math.PI + Math.atan2(
@ -2910,16 +2918,21 @@ Interface.definePanels(function() {
face.uv[vkey][0] - face.uv[face.vertices[j]][0],
)) % 180;
if (Math.abs(relative_angle - 90) < e) {
straight_angle = angle;
straight_angle = angle - (relative_angle - 90);
if (scope.helper_lines.x == -1) scope.helper_lines.x = face.uv[vkey][0];
break;
}
if (relative_angle < e || 180 - relative_angle < e) {
straight_angle = angle;
straight_angle = angle - (relative_angle > 90 ? (relative_angle-180) : relative_angle);
if (scope.helper_lines.y == -1) scope.helper_lines.y = face.uv[vkey][1];
break;
}
}
})
})
if (straight_angle) {
setUV(straight_angle);
}
}
})
UVEditor.turnMapping()

View File

@ -384,6 +384,7 @@
"message.load_images.title": "Load Images",
"message.load_images.edit_image": "Edit Image",
"message.load_images.add_image": "Add Image",
"message.import_palette.replace_palette": "Replace old palette",
"message.import_palette.threshold": "Merge Threshold",

20
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "Blockbench",
"version": "4.9.0-beta.2",
"version": "4.9.4",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -2841,9 +2841,9 @@
}
},
"electron": {
"version": "26.6.1",
"resolved": "https://registry.npmjs.org/electron/-/electron-26.6.1.tgz",
"integrity": "sha512-4Vz9u0Jt/khPa/en2l8Jv6SWEfsK/ieWYtchl5j0clbNSjdeTucnEFOhz9B9WwsAmfQjxBnpuMZpmdBuyxq+wg==",
"version": "26.6.9",
"resolved": "https://registry.npmjs.org/electron/-/electron-26.6.9.tgz",
"integrity": "sha512-R4uWUzwUwlYwFPS+BY4Dg9KzbIpqdfiLepmcrYHHOUb0dYf2TeYtFPBGYo3kF2L0JmLy1lFtIExCRaMB+J6yow==",
"dev": true,
"requires": {
"@electron/get": "^2.0.0",
@ -2852,9 +2852,9 @@
},
"dependencies": {
"@types/node": {
"version": "18.18.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.12.tgz",
"integrity": "sha512-G7slVfkwOm7g8VqcEF1/5SXiMjP3Tbt+pXDU3r/qhlM2KkGm786DUD4xyMA2QzEElFrv/KZV9gjygv4LnkpbMQ==",
"version": "18.19.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.17.tgz",
"integrity": "sha512-SzyGKgwPzuWp2SHhlpXKzCX0pIOfcI4V2eF37nNBJOhwlegQ83omtVQ1XxZpDE06V/d6AQvfQdPfnw0tRC//Ng==",
"dev": true,
"requires": {
"undici-types": "~5.26.4"
@ -3563,9 +3563,9 @@
},
"dependencies": {
"semver": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
"dev": true,
"optional": true,
"requires": {

View File

@ -1,7 +1,7 @@
{
"name": "Blockbench",
"description": "Low-poly modeling and animation software",
"version": "4.9.3",
"version": "4.9.4",
"license": "GPL-3.0-or-later",
"author": {
"name": "JannisX11",
@ -109,7 +109,7 @@
},
"devDependencies": {
"blockbench-types": "^4.6.1",
"electron": "^26.6.1",
"electron": "^26.6.9",
"electron-builder": "^23.6.0",
"electron-notarize": "^1.0.0",
"webpack": "^5.74.0",