diff --git a/plugins.json b/plugins.json
index 5a731767..2b6b1a69 100644
--- a/plugins.json
+++ b/plugins.json
@@ -374,6 +374,21 @@
"creation_date": "2024-07-01",
"has_changelog": true
},
+ "free_rotation": {
+ "title": "Free Rotation",
+ "icon": "icon.png",
+ "author": "Godlander & Ewan Howell",
+ "description": "Create Java Item models without any rotation limitations.",
+ "tags": ["Minecraft: Java Edition", "Rotation"],
+ "version": "1.0.0",
+ "min_version": "4.11.2",
+ "variant": "desktop",
+ "website": "https://ewanhowell.com/plugins/free-rotation/",
+ "repository": "https://github.com/ewanhowell5195/blockbenchPlugins/tree/main/free_rotation",
+ "bug_tracker": "https://github.com/ewanhowell5195/blockbenchPlugins/issues/new?title=[Free Rotation]",
+ "creation_date": "2024-12-20",
+ "has_changelog": true
+ },
"threecore_exporter": {
"title": "ThreeCore Exporter",
"author": "Lucas, Spyeedy",
@@ -888,7 +903,7 @@
"version": "0.5.1",
"variant": "desktop",
"new_respository_format" : true
- },
+ },
"animation_to_json": {
"title": "Animation to JSON Converter",
"author": "Gaming32",
diff --git a/plugins/free_rotation/about.md b/plugins/free_rotation/about.md
new file mode 100644
index 00000000..bbd792c7
--- /dev/null
+++ b/plugins/free_rotation/about.md
@@ -0,0 +1,84 @@
+
+
This format is designed to create Minecraft: Java Edition item models without the rotation limitations imposed by the game.
+
These models cannot be re-imported, so make sure to save your project as a bbmodel.
+
This format requires Minecraft 1.21.4 or later.
+
Usage:
+
To use this plugin, start by creating a new model, or converting an existing cube based project into this format.
+
Configure the display settings. These will be respected as long as the size limits are not reached.
+
Use File > Export > Free Rotation Item to export your model into your resource pack.
+
When exporting, select which display slots you would like to export. The more you export, the larger the file size, so only export what you need.
+
+
+
\ No newline at end of file
diff --git a/plugins/free_rotation/changelog.json b/plugins/free_rotation/changelog.json
new file mode 100644
index 00000000..fa82860f
--- /dev/null
+++ b/plugins/free_rotation/changelog.json
@@ -0,0 +1,15 @@
+{
+ "1.0.0": {
+ "title": "1.0.0",
+ "date": "2024-12-20",
+ "author": "Godlander & Ewan Howell",
+ "categories": [
+ {
+ "title": "New Features",
+ "list": [
+ "Initial release"
+ ]
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/plugins/free_rotation/free_rotation.js b/plugins/free_rotation/free_rotation.js
new file mode 100644
index 00000000..b4adf04a
--- /dev/null
+++ b/plugins/free_rotation/free_rotation.js
@@ -0,0 +1,568 @@
+(() => {
+ const path = require("node:path")
+ const os = require("node:os")
+
+ let codec, format, action, properties, styles
+
+ const id = "free_rotation"
+ const name = "Free Rotation"
+ const icon = "3d_rotation"
+ const description = "Create Java Item models without any rotation limitations."
+
+ const links = {
+ websiteGodlander: {
+ text: "By Godlander",
+ link: "https://github.com/Godlander",
+ icon: "fab.fa-github",
+ colour: "#6E40C9"
+ },
+ discordGodlander: {
+ text: "Godlander's Discord",
+ link: "https://discord.gg/2s6th9SvZd",
+ icon: "fab.fa-discord",
+ colour: "#727FFF"
+ },
+ websiteEwan: {
+ text: "By Ewan Howell",
+ link: "https://ewanhowell.com/",
+ icon: "language",
+ colour: "#33E38E"
+ },
+ discordEwan: {
+ text: "Ewan's Discord",
+ link: "https://discord.ewanhowell.com/",
+ icon: "fab.fa-discord",
+ colour: "#727FFF"
+ }
+ }
+
+ let directory
+ if (os.platform() === "win32") {
+ directory = path.join(os.homedir(), "AppData", "Roaming", ".minecraft", "resourcepacks")
+ } else if (os.platform() === "darwin") {
+ directory = path.join(os.homedir(), "Library", "Application Support", "minecraft", "resourcepacks")
+ } else {
+ directory = path.join(os.homedir(), ".minecraft", "resourcepacks")
+ }
+
+ Plugin.register(id, {
+ title: name,
+ icon: "icon.png",
+ author: "Godlander & Ewan Howell",
+ description,
+ tags: ["Minecraft: Java Edition", "Rotation"],
+ version: "1.0.0",
+ min_version: "4.11.2",
+ variant: "desktop",
+ await_loading: true,
+ website: "https://ewanhowell.com/plugins/free-rotation/",
+ repository: "https://github.com/ewanhowell5195/blockbenchPlugins/tree/main/free_rotation",
+ bug_tracker: "https://github.com/ewanhowell5195/blockbenchPlugins/issues/new?title=[Free Rotation]",
+ creation_date: "2024-12-20",
+ onload() {
+ styles = Blockbench.addCSS(`
+ #format_page_free_rotation {
+ padding-bottom: 0;
+ }
+ #format_page_free_rotation .format_target {
+ margin-bottom: 6px;
+ }
+ #format_page_free_rotation div:nth-child(3),
+ #format_page_free_rotation content {
+ overflow-y: auto;
+ }
+ .format_entry[format="free_rotation"] i {
+ overflow: visible;
+ }
+ .free-rotation-links {
+ display: flex;
+ justify-content: space-around;
+ margin: 20px 35px 0;
+ }
+ .free-rotation-links * {
+ cursor: pointer;
+ }
+ .free-rotation-links > a {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 5px;
+ padding: 5px;
+ text-decoration: none;
+ flex-grow: 1;
+ flex-basis: 0;
+ color: var(--color-subtle_text);
+ text-align: center;
+ }
+ .free-rotation-links > a:hover {
+ background-color: var(--color-accent);
+ color: var(--color-light);
+ }
+ .free-rotation-links > a > i {
+ font-size: 32px;
+ width: 100%;
+ max-width: initial;
+ height: 32px;
+ text-align: center;
+ }
+ .free-rotation-links > a:hover > i {
+ color: var(--color-light) !important;
+ }
+ .free-rotation-links > a > p {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ }
+ `)
+
+ codec = new Codec("free_rotation_codec", {
+ name: "Free Rotation Codec",
+ remember: true,
+ export() {
+ const project = Project
+ for (const texture of Texture.all) {
+ if (!texture.saved) texture.save()
+ }
+ const dialog = new Dialog({
+ id: "free_rotation_export_prompt",
+ title: "Free Rotation Export",
+ buttons: [],
+ lines: [``],
+ component: {
+ data: {
+ project,
+ exportAllowed: false,
+ idError: "",
+ nameError: "",
+ displayIcons: {
+ thirdperson_lefthand: { icon: "accessibility" },
+ thirdperson_righthand: { icon: "accessibility" },
+ firstperson_lefthand: { icon: "person" },
+ firstperson_righthand: { icon: "person" },
+ head: { icon: "sentiment_satisfied" },
+ ground: { icon: "icon-ground", iconType: "custom" },
+ fixed: { icon: "filter_frames" },
+ gui: { name: "GUI", icon: "border_style" }
+ }
+ },
+ methods: {
+ check() {
+ if (!project.free_rotation_item) {
+ this.idError = "The Item ID is required"
+ } else if (project.free_rotation_item.match(/[^a-z0-9_-]/)) {
+ this.idError = "The Item ID can only include the following characters:
a-z
, 0-9
, _
, -
"
+ } else {
+ this.idError = ""
+ }
+ if (!project.free_rotation_name) {
+ this.nameError = "The Model Name is required"
+ } else if (project.free_rotation_name.match(/[^a-z0-9_-]/)) {
+ this.nameError = "The Model Name can only include the following characters:
a-z
, 0-9
, _
, -
"
+ } else {
+ this.nameError = ""
+ }
+ if (this.idError || this.nameError) {
+ this.exportAllowed = false
+ return
+ }
+ this.exportAllowed = true
+ },
+ async exportModel() {
+ dialog.close()
+
+ const dir = Blockbench.pickDirectory({
+ title: "Select resource pack to export to",
+ startpath: project.free_rotation_path
+ })
+ if (!dir) return
+
+ const definitionDir = path.join(dir, "assets", "freerot", "items")
+ const definitionFile = path.join(definitionDir, project.free_rotation_item + ".json")
+ const modelDir = path.join(dir, "assets", "freerot", "models", project.free_rotation_name)
+
+ if (fs.existsSync(definitionFile)) {
+ const check = await new Promise(fulfil => {
+ Blockbench.showMessageBox({
+ title: "Item definition already exists",
+ message: `The item definition assets/freerot/${project.free_rotation_item}.json
already exists. Are you sure you want to continue and overwrite it?`,
+ buttons: ["dialog.confirm", "dialog.cancel"]
+ }, button => {
+ if (button === 0) fulfil(true)
+ else fulfil()
+ })
+ })
+ if (!check) return
+ }
+
+ if (fs.existsSync(modelDir)) {
+ const check = await new Promise(fulfil => {
+ Blockbench.showMessageBox({
+ title: "Model already exists",
+ message: `The the model folder assets/freerot/models/${project.free_rotation_name}
already exists. Are you sure you want to continue and possibly overwrite files inside it?`,
+ buttons: ["dialog.confirm", "dialog.cancel"]
+ }, button => {
+ if (button === 0) fulfil(true)
+ else fulfil()
+ })
+ })
+ if (!check) return
+ }
+
+ const processing = new Dialog("exporting", {
+ title: "Exporting...",
+ lines: [
+ ``,
+ "Exporting...
"
+ ],
+ buttons: [],
+ cancel_on_click_outside: false,
+ onConfirm: () => false
+ }).show()
+ const close = processing.close
+ processing.close = () => {}
+
+ fs.mkdirSync(definitionDir, { recursive: true })
+ fs.mkdirSync(modelDir, { recursive: true })
+
+ const models = await codec.compile(project)
+
+ const definition = {
+ model: {
+ type: "composite",
+ models: new Array(models.length).fill().map((e, i) => ({
+ type: "model",
+ model: `freerot:${project.free_rotation_name}/${i}`
+ }))
+ }
+ }
+
+ fs.writeFileSync(definitionFile, autoStringify(definition))
+ for (const [i, model] of models.entries()) {
+ fs.writeFileSync(path.join(modelDir, `${i}.json`), model)
+ }
+ close.bind(processing)()
+ }
+ },
+ template: `
+
+
Model Details
+
+
+
+
+
+
+
+
+
+
+
+
+
Display Settings to Export
+
Free Rotation models can be quite large in file size. Select only the display settings you need in order to reduce this file size.
+
+
+
+
+
+ `
+ },
+ onOpen() {
+ if (project.free_rotation_item || project.free_rotation_name) {
+ this.content_vue.check()
+ }
+ }
+ }).show()
+ },
+ async compile(project) {
+ project ??= Project
+
+ project.select()
+
+ const mode = Modes.selected
+ Modes.options.edit.select()
+
+ let maxcoord = 24
+
+ const cubes = Cube.all.filter(e => e.export)
+
+ for (const cube of cubes) {
+ for (const position of cube.getGlobalVertexPositions()) {
+ for (const coord of position) {
+ maxcoord = Math.max(maxcoord, Math.abs(coord - 8))
+ }
+ }
+ }
+ const downscale = Math.min(4, maxcoord / 24)
+
+ const models = []
+ for (const cube of cubes) {
+ const element = {}
+ const model = {
+ textures: {},
+ elements: [element],
+ display: {}
+ }
+ let size = [
+ (cube.to[0] - cube.from[0]) / downscale,
+ (cube.to[1] - cube.from[1]) / downscale,
+ (cube.to[2] - cube.from[2]) / downscale
+ ]
+ element.from = [
+ 8 - (size[0] / 2),
+ 8 - (size[1] / 2),
+ 8 - (size[2] / 2)
+ ]
+ element.to = [
+ 8 + (size[0] / 2),
+ 8 + (size[1] / 2),
+ 8 + (size[2] / 2)
+ ]
+ element.light_emission = cube.light_emission
+ element.faces = {}
+ for (const [face, data] of Object.entries(cube.faces)) {
+ if (!data || !data.texture) continue
+ const renderedFace = {}
+ if (data.enabled) {
+ renderedFace.uv = data.uv
+ .slice()
+ .map((v, i) => (v * 16) / UVEditor.getResolution(i % 2))
+ }
+ if (data.rotation) renderedFace.rotation = data.rotation
+ if (data.texture) {
+ const texture = project.textures.find(e => e.uuid == data.texture)
+ if (!texture) {
+ console.error("Texture not found... skipping")
+ } else {
+ renderedFace.texture = "#" + texture.id
+ const path = texture.source.replaceAll(/\\/g, "/")
+ const parts = path.split("/")
+ const assetsIndex = parts.indexOf("assets")
+ if (assetsIndex === -1) model.textures[texture.id] = "unknown"
+ else {
+ const namespace = parts[assetsIndex + 1]
+ const resourcePath = parts.slice(assetsIndex + 3, -1).join("/")
+ model.textures[texture.id] = namespace + ":" + resourcePath + "/" + texture.name.slice(0, -4)
+ }
+ }
+ }
+ //if (data.cullface) renderedFace.cullface = data.cullface
+ if (data.tint >= 0) renderedFace.tintindex = data.tint
+ element.faces[face] = renderedFace
+ }
+
+ const quat = new THREE.Quaternion()
+ cube.mesh.getWorldQuaternion(quat)
+ const rotation = new THREE.Quaternion()
+
+ for (const slot of DisplayMode.slots) {
+ if (project.free_rotation_display[slot]) {
+ const scale = new THREE.Vector3(downscale, downscale, downscale)
+ const translation = cube.getWorldCenter()
+ translation.y -= 8
+
+ const display = project.display_settings[slot]
+ if (display) {
+ const dscale = new THREE.Vector3().fromArray(display.scale)
+ const dtranslation = new THREE.Vector3().fromArray(display.translation)
+ const drotation = new THREE.Quaternion().setFromEuler(
+ new THREE.Euler().fromArray([
+ Math.degToRad(display.rotation[0]),
+ Math.degToRad(display.rotation[1]),
+ Math.degToRad(display.rotation[2])
+ ], "XYZ")
+ )
+ scale.multiply(dscale)
+ rotation.multiplyQuaternions(drotation, quat)
+ translation.multiply(dscale)
+ translation.applyQuaternion(drotation)
+ translation.add(dtranslation)
+ }
+
+ model.display[slot] = {
+ rotation: (new THREE.Euler()).setFromQuaternion(rotation, "XYZ").toArray().slice(0,3).map(e => Math.radToDeg(e)),
+ translation: translation.toArray(),
+ scale: scale.toArray()
+ }
+ }
+ }
+
+ models.push(autoStringify(model))
+ }
+
+ mode.select()
+ return models
+ }
+ })
+
+ format = new ModelFormat({
+ id: "free_rotation",
+ name: "Free Rotation Item",
+ extension: "json",
+ icon: "3d_rotation",
+ category: "minecraft",
+ format_page: {
+ component: {
+ methods: {
+ create: () => format.new()
+ },
+ template: `
+
+
${description}
+
Target : Minecraft: Java Edition
+
+ About:
+
+
+ - This format is designed to create Minecraft: Java Edition item models without the rotation limitations imposed by the game.
+ - These models cannot be re-imported, so make sure to save your project as a bbmodel.
+ - This format requires Minecraft 1.21.4 or later.
+
+
+ Usage:
+
+
+ - Create a new model, or convert an existing cube based project into this format.
+ - Configure the display settings. These will be respected as long as the size limits are not reached.
+ - Use File > Export > Free Rotation Item to export your model into your resource pack.
+ - When exporting, select which display slots you would like to export. The more you export, the larger the file size, so only export what you need.
+
+
+
+
+
+
+
+
+
+ `
+ }
+ },
+ render_sides: "front",
+ model_identifier: false,
+ parent_model_id: true,
+ vertex_color_ambient_occlusion: true,
+ bone_rig: true,
+ rotate_cubes: true,
+ optional_box_uv: true,
+ uv_rotation: true,
+ java_cube_shading_properties: true,
+ java_face_properties: true,
+ cullfaces: true,
+ animated_textures: true,
+ select_texture_for_particles: true,
+ texture_mcmeta: true,
+ display_mode: true,
+ texture_folder: true,
+ codec
+ })
+ codec.format = format
+
+ action = new Action("free_rotation_export", {
+ name: "Export Free Rotation Item",
+ icon: "3d_rotation",
+ condition: { formats: [format.id] },
+ click: () => codec.export()
+ })
+ MenuBar.addAction(action, "file.export.0")
+
+ properties = [
+ new Property(ModelProject, "string", "free_rotation_item", {
+ label: "Item ID",
+ description: "The item ID of the item that the model should apply to",
+ condition: { formats: [format.id] }
+ }),
+ new Property(ModelProject, "string", "free_rotation_name", {
+ label: "Model Name",
+ description: "The name of the model file that is output",
+ condition: { formats: [format.id] }
+ }),
+ new Property(ModelProject, "string", "free_rotation_path", {
+ label: "Export Path",
+ default: directory,
+ condition: { formats: [format.id] },
+ exposed: false
+ }),
+ new Property(ModelProject, "object", "free_rotation_display", {
+ default: {
+ thirdperson_lefthand: true,
+ thirdperson_righthand: true,
+ firstperson_lefthand: true,
+ firstperson_righthand: true,
+ head: true,
+ ground: true,
+ fixed: true,
+ gui: true
+ },
+ label: "Display Settings",
+ condition: { formats: [format.id] },
+ exposed: false
+ })
+ ]
+ },
+ onunload() {
+ codec.delete()
+ format.delete()
+ action.delete()
+ styles.delete()
+ properties.forEach(e => e.delete())
+ }
+ })
+})()
\ No newline at end of file
diff --git a/plugins/free_rotation/icon.png b/plugins/free_rotation/icon.png
new file mode 100644
index 00000000..dabd3472
Binary files /dev/null and b/plugins/free_rotation/icon.png differ
diff --git a/plugins/free_rotation/members.yml b/plugins/free_rotation/members.yml
new file mode 100644
index 00000000..71a48338
--- /dev/null
+++ b/plugins/free_rotation/members.yml
@@ -0,0 +1,3 @@
+maintainers:
+ - ewanhowell5195
+ - Godlander
\ No newline at end of file