diff --git a/css/dialogs.css b/css/dialogs.css index 4d850409b..70f5374b9 100644 --- a/css/dialogs.css +++ b/css/dialogs.css @@ -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; } diff --git a/css/panels.css b/css/panels.css index 3c16b43c3..253e1fd8d 100644 --- a/css/panels.css +++ b/css/panels.css @@ -2677,6 +2677,7 @@ span.controller_state_section_info { padding: 2px; min-height: 160px; max-height: 232px; + line-height: 0; } #palette_list .color { display: inline-block; diff --git a/js/animations/animation_controllers.js b/js/animations/animation_controllers.js index 49cadc4ac..b4b8c89ee 100644 --- a/js/animations/animation_controllers.js +++ b/js/animations/animation_controllers.js @@ -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(() => {
- +
diff --git a/js/display_mode.js b/js/display_mode.js index 202654708..9784fa0c3 100644 --- a/js/display_mode.js +++ b/js/display_mode.js @@ -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'), diff --git a/js/interface/actions.js b/js/interface/actions.js index 92f5f8d6f..193a9a714 100644 --- a/js/interface/actions.js +++ b/js/interface/actions.js @@ -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() { diff --git a/js/interface/dialog.js b/js/interface/dialog.js index 70bf7155b..40c47179f 100644 --- a/js/interface/dialog.js +++ b/js/interface/dialog.js @@ -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'); diff --git a/js/interface/menu_bar.js b/js/interface/menu_bar.js index 911d2cca3..9ff8714b9 100644 --- a/js/interface/menu_bar.js +++ b/js/interface/menu_bar.js @@ -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', diff --git a/js/interface/vue_components.js b/js/interface/vue_components.js index 51dc19d0f..610fa7253 100644 --- a/js/interface/vue_components.js +++ b/js/interface/vue_components.js @@ -134,7 +134,12 @@ Vue.component('numeric-input', {
code
- ` + `, + 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: { diff --git a/js/io/codec.js b/js/io/codec.js index a76fe5dcd..410986fe4 100644 --- a/js/io/codec.js +++ b/js/io/codec.js @@ -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', diff --git a/js/io/formats/fbx.js b/js/io/formats/fbx.js index e89858f42..bdb525e82 100644 --- a/js/io/formats/fbx.js +++ b/js/io/formats/fbx.js @@ -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) { diff --git a/js/io/formats/gltf.js b/js/io/formats/gltf.js index 27f789493..6364a9dc9 100644 --- a/js/io/formats/gltf.js +++ b/js/io/formats/gltf.js @@ -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({ diff --git a/js/io/io.js b/js/io/io.js index 98287a56e..919760ab8 100644 --- a/js/io/io.js +++ b/js/io/io.js @@ -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'); diff --git a/js/outliner/cube.js b/js/outliner/cube.js index d93bfc5b6..4f07b423e 100644 --- a/js/outliner/cube.js +++ b/js/outliner/cube.js @@ -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) { diff --git a/js/outliner/group.js b/js/outliner/group.js index 415b8b69b..e2b69f509 100644 --- a/js/outliner/group.js +++ b/js/outliner/group.js @@ -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) diff --git a/js/outliner/outliner.js b/js/outliner/outliner.js index c794b3a19..1341449b5 100644 --- a/js/outliner/outliner.js +++ b/js/outliner/outliner.js @@ -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() diff --git a/js/plugin_loader.js b/js/plugin_loader.js index 268c22c40..4b87bdf72 100644 --- a/js/plugin_loader.js +++ b/js/plugin_loader.js @@ -419,7 +419,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) @@ -791,10 +792,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)) { @@ -1300,7 +1305,7 @@ BARS.defineActions(function() { Website - {{ selected_plugin.details.website }} + {{ reduceLink(selected_plugin.details.website) }} Plugin source diff --git a/js/predicate_editor.js b/js/predicate_editor.js index ac286318d..ae1a284e6 100644 --- a/js/predicate_editor.js +++ b/js/predicate_editor.js @@ -311,11 +311,11 @@ const PredicateOverrideEditor = { - + diff --git a/js/preview/preview.js b/js/preview/preview.js index 97b1c1ea2..f1e2073b9 100644 --- a/js/preview/preview.js +++ b/js/preview/preview.js @@ -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() { diff --git a/js/preview/reference_images.js b/js/preview/reference_images.js index e56c6f2c0..116288fcc 100644 --- a/js/preview/reference_images.js +++ b/js/preview/reference_images.js @@ -583,7 +583,7 @@ class ReferenceImage { new Dialog('reference_image_properties', { title: 'data.reference_image', form: { - source: {type: 'file', label: 'reference_image.image', condition: () => isApp && this.source && PathModule.isAbsolute(this.source), value: this.source, extensions: ['png', 'jpg', 'jpeg']}, + source: {type: 'file', label: 'reference_image.image', condition: () => isApp && this.source && PathModule.isAbsolute(this.source), value: this.source, extensions: ReferenceImage.supported_extensions}, layer: {type: 'select', label: 'reference_image.layer', value: this.layer, options: { background: 'reference_image.layer.background', viewport: 'reference_image.layer.viewport', @@ -624,6 +624,7 @@ class ReferenceImage { return this; } } +ReferenceImage.supported_extensions = ['png', 'jpg', 'jpeg', 'webp', 'bmp', 'tiff', 'tif', 'gif']; ReferenceImage.prototype.menu = new Menu([ new MenuSeparator('settings'), { @@ -859,7 +860,7 @@ BARS.defineActions(function() { } Blockbench.import({ resource_id: 'reference_image', - extensions: ['png', 'jpg', 'jpeg', 'bmp', 'tiff', 'tif', 'gif'], + extensions: ReferenceImage.supported_extensions, type: 'Image', readtype: 'image' }, async function(files) { diff --git a/js/texturing/layers.js b/js/texturing/layers.js index e11f67413..f2ff1c77b 100644 --- a/js/texturing/layers.js +++ b/js/texturing/layers.js @@ -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) { diff --git a/js/texturing/painter.js b/js/texturing/painter.js index 0c9a2100b..6e79ff3c8 100644 --- a/js/texturing/painter.js +++ b/js/texturing/painter.js @@ -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(); diff --git a/js/texturing/textures.js b/js/texturing/textures.js index 55515d49d..a683a0527 100644 --- a/js/texturing/textures.js +++ b/js/texturing/textures.js @@ -1240,34 +1240,58 @@ class Texture { }) scope.edit((canvas) => { - - 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); + 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); + + 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]; @@ -1324,6 +1348,7 @@ class Texture { Undo.finishEdit('Resize texture'); + UVEditor.vue.updateTexture(); setTimeout(updateSelection, 100); } }) @@ -1656,7 +1681,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) { diff --git a/js/texturing/uv.js b/js/texturing/uv.js index d840147bb..df8b33a44 100644 --- a/js/texturing/uv.js +++ b/js/texturing/uv.js @@ -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]; @@ -2882,21 +2883,28 @@ Interface.definePanels(function() { }) } else if (element instanceof Mesh) { - scope.selected_faces.forEach(fkey => { - let face = element.faces[fkey]; - if (!face) return; - face.vertices.forEach(vkey => { - if (!face.uv[vkey]) return; + 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.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()); + 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()); + }) }) - let e = 0.6; + } + setUV(angle); + let e = 0.6; + scope.selected_faces.forEach(fkey => { + let face = element.faces[fkey]; + if (!face) return; face.vertices.forEach((vkey, i) => { for (let j = i+1; j < face.vertices.length; j++) { let relative_angle = Math.radToDeg(Math.PI + Math.atan2( @@ -2904,16 +2912,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() diff --git a/lang/en.json b/lang/en.json index 169ae44c2..8774a75ba 100644 --- a/lang/en.json +++ b/lang/en.json @@ -382,6 +382,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", diff --git a/package-lock.json b/package-lock.json index 0cb0e4a2e..922380a2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index 022f8b03e..2657f8f3b 100644 --- a/package.json +++ b/package.json @@ -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",