From 969159452b9e1e2d8afc20a35def420826b13b4b Mon Sep 17 00:00:00 2001 From: Ewan Howell <48070995+ewanhowell5195@users.noreply.github.com> Date: Thu, 19 Dec 2024 17:20:29 +0000 Subject: [PATCH] Free rotation plugin --- plugins.json | 17 +- plugins/free_rotation/about.md | 84 ++++ plugins/free_rotation/changelog.json | 15 + plugins/free_rotation/free_rotation.js | 568 +++++++++++++++++++++++++ plugins/free_rotation/icon.png | Bin 0 -> 3938 bytes plugins/free_rotation/members.yml | 3 + 6 files changed, 686 insertions(+), 1 deletion(-) create mode 100644 plugins/free_rotation/about.md create mode 100644 plugins/free_rotation/changelog.json create mode 100644 plugins/free_rotation/free_rotation.js create mode 100644 plugins/free_rotation/icon.png create mode 100644 plugins/free_rotation/members.yml 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.

+
+
+
{{ displayIcons[key]?.name || key.replace(/_/g, " ").split(" ").map(e => e.slice(0, 1).toUpperCase() + e.slice(1)).join(" ") }}
+ + {{ displayIcons[key]?.icon || 'desktop_windows' }} +
+
+
+ +
+
+ ` + }, + 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:

+

+

+

+

Usage:

+

+

+

+
+
+ +
+ +
+
+ ` + } + }, + 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 0000000000000000000000000000000000000000..dabd34720494214b39bdc9e0f91f4c2ee16196af GIT binary patch literal 3938 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