diff --git a/.gitignore b/.gitignore index 3c3629e..25c8fdb 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +package-lock.json \ No newline at end of file diff --git a/dist/hytale_plugin.js b/dist/hytale_plugin.js index ed08fb3..35c9e81 100644 --- a/dist/hytale_plugin.js +++ b/dist/hytale_plugin.js @@ -1,4 +1,10 @@ (() => { + var __defProp = Object.defineProperty; + var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); + }; + // src/cleanup.ts var list = []; function track(...items) { @@ -23,6 +29,218 @@ } }; + // src/translations/en.json + var en_exports = {}; + __export(en_exports, { + actions: () => actions, + default: () => en_default, + directory: () => directory, + error: () => error, + formats: () => formats, + menu: () => menu, + plugin: () => plugin, + settings: () => settings2, + textures: () => textures, + toggle: () => toggle + }); + var plugin = { + title: "Hytale Models", + description: "Create models and animations for Hytale" + }; + var menu = { + import_attachment: "Import Attachment", + reload_all: "Reload All Attachments" + }; + var error = { + no_textures: "No textures found for this model." + }; + var actions = { + reload_attachments: "Reload Attachments", + reload_all_attachments: "Reload All Attachments", + hytale_add_quad: "Add Quad", + cut_selection: "Cut texture selection", + paste_texture: "Paste into texture", + pasted_layer_name: "pasted" + }; + var textures = { + import: "Import Textures", + message: "No textures were found for this model. How would you like to import textures?", + select_files: "Select Files", + select_folder: "Select Folder", + skip: "Skip" + }; + var directory = { + select_texture_folder: "Select Texture Folder", + file_not_found: "File not found", + export_blockymodel: "Export Hytale BlockyModel", + export_blockymodel_desc: "Export a BlockyModel file" + }; + var formats = { + character: { + name: "Hytale Character", + description: "Create character and attachment models using Hytale's blockymodel format" + }, + prop: { + name: "Hytale Prop", + description: "Create prop models using Hytale's blockymodel format" + }, + page: { + informations: "Information", + resources: "Resources", + info_list: "* One texture can be applied to a model at a time\n* UV sizes are linked to the size of each cube and cannot be modified, except by stretching the cube\n* Models can have a maximum of 255 nodes", + resource_list: "* [Modeling Overview and Style Guide](https://hytale.com/news/2025/12/an-introduction-to-making-models-for-hytale)\n* [Modeling Tutorial](https://youtu.be/Q07i3wmGy0Y)" + }, + category: "Hytale" + }; + var settings2 = { + duplicate_bones: { + name: "Duplicate Bone Names", + description: "Allow creating duplicate groups names in Hytale formats. Multiple groups with the same name can be used to apply animations to multiple nodes at once." + }, + photoshop_copy: { + name: "Copy-Paste with Magenta Alpha", + description: "Copy image selections with magenta background and remove magenta when pasting to help transfer transparency to Photoshop" + }, + group_pivot: { + name: "Show Group Pivot Indicator", + description: "Show a dot in Edit mode indicating the rotation pivot point for animations" + }, + preview_texture: { + name: "Preview Selected Texture", + description: "When selecting a texture in a Hytale format, preview the texture on the model instantly" + } + }; + var toggle = { + toggle_attachment_editing: "Toggle Attachment Editing" + }; + var en_default = { + plugin, + menu, + error, + actions, + textures, + directory, + formats, + settings: settings2, + toggle + }; + + // src/translations/pt-BR.json + var pt_BR_exports = {}; + __export(pt_BR_exports, { + actions: () => actions2, + default: () => pt_BR_default, + directory: () => directory2, + error: () => error2, + formats: () => formats2, + menu: () => menu2, + plugin: () => plugin2, + settings: () => settings3, + textures: () => textures2, + toggle: () => toggle2 + }); + var plugin2 = { + title: "Modelos Hytale", + description: "Crie modelos e anima\xE7\xF5es para o Hytale" + }; + var menu2 = { + import_attachment: "Importar Anexo", + reload_all: "Recarregar Todos os Anexos" + }; + var error2 = { + no_textures: "Nenhuma textura encontrada para este modelo." + }; + var actions2 = { + reload_attachments: "Recarregar Anexos", + reload_all_attachments: "Recarregar Todos os Anexos", + hytale_add_quad: "Adicionar Quad", + cut_selection: "Recortar sele\xE7\xE3o da textura", + paste_texture: "Colar na textura", + pasted_layer_name: "colado" + }; + var textures2 = { + import: "Importar Texturas", + message: "Nenhuma textura foi encontrada para este modelo. Como voc\xEA deseja importar as texturas?", + select_files: "Selecionar Arquivos", + select_folder: "Selecionar Pasta", + skip: "Pular" + }; + var directory2 = { + select_texture_folder: "Selecionar Pasta de Texturas", + file_not_found: "Arquivo n\xE3o encontrado", + export_blockymodel: "Exportar BlockyModel do Hytale", + export_blockymodel_desc: "Exportar um arquivo BlockyModel" + }; + var formats2 = { + character: { + name: "Personagem Hytale", + description: "Crie modelos de personagens e anexos usando o formato blockymodel do Hytale" + }, + prop: { + name: "Objeto Hytale", + description: "Crie modelos de objetos usando o formato blockymodel do Hytale" + }, + page: { + informations: "Informa\xE7\xF5es", + resources: "Recursos", + info_list: "* Apenas uma textura pode ser aplicada a um modelo por vez\n* Os tamanhos de UV s\xE3o vinculados ao tamanho de cada cubo e n\xE3o podem ser modificados, exceto esticando o cubo\n* Os modelos podem ter no m\xE1ximo 255 n\xF3s", + resource_list: "* [Guia de Modelagem e Estilo](https://hytale.com/news/2025/12/an-introduction-to-making-models-for-hytale)\n* [Tutorial de Modelagem](https://youtu.be/Q07i3wmGy0Y)" + }, + category: "Hytale" + }; + var settings3 = { + duplicate_bones: { + name: "Duplicar Nomes de Ossos", + description: "Permite criar grupos com nomes duplicados nos formatos do Hytale. V\xE1rios grupos com o mesmo nome podem ser usados para aplicar anima\xE7\xF5es a v\xE1rios n\xF3s ao mesmo tempo." + }, + photoshop_copy: { + name: "Copiar e Colar com Alpha Magenta", + description: "Copia sele\xE7\xF5es de imagem com fundo magenta e remove o magenta ao colar, ajudando a transferir transpar\xEAncia para o Photoshop" + }, + group_pivot: { + name: "Mostrar Indicador de Piv\xF4 do Grupo", + description: "Mostra um ponto no modo de Edi\xE7\xE3o indicando o ponto de piv\xF4 de rota\xE7\xE3o para anima\xE7\xF5es" + }, + preview_texture: { + name: "Pr\xE9-visualizar Textura Selecionada", + description: "Ao selecionar uma textura em um formato Hytale, a textura \xE9 exibida instantaneamente no modelo" + } + }; + var toggle2 = { + toggle_attachment_editing: "Alternar Edi\xE7\xE3o de Anexos" + }; + var pt_BR_default = { + plugin: plugin2, + menu: menu2, + error: error2, + actions: actions2, + textures: textures2, + directory: directory2, + formats: formats2, + settings: settings3, + toggle: toggle2 + }; + + // src/i18n.ts + var translations = { + en: en_exports, + pt: pt_BR_exports + }; + function t(path) { + const langCode = Language.code.split("_")[0]; + const currentLangObj = translations[langCode] || translations["en"]; + const resolvePath = (obj, path2) => { + return path2.split(".").reduce((prev, curr) => { + return prev ? prev[curr] : null; + }, obj); + }; + let result = resolvePath(currentLangObj, path); + if (!result && langCode !== "en") { + result = resolvePath(translations["en"], path); + } + return result || path; + } + // src/util.ts function qualifiesAsMainShape(object) { return object instanceof Cube && object.rotation.allEqual(0); @@ -59,27 +277,27 @@ return [...new Set(paths)]; } function loadTexturesFromPaths(paths, preferredName) { - const textures = []; + const textures3 = []; for (let texturePath of paths) { - let texture = Texture.all.find((t) => t.path == texturePath); + let texture = Texture.all.find((t2) => t2.path == texturePath); if (!texture) { texture = new Texture().fromPath(texturePath).add(false, true); } - textures.push(texture); + textures3.push(texture); } - if (textures.length > 0) { - let primary = preferredName && textures.find((t) => t.name.startsWith(preferredName)) || textures[0]; - if (!Texture.all.find((t) => t.use_as_default)) { + if (textures3.length > 0) { + let primary = preferredName && textures3.find((t2) => t2.name.startsWith(preferredName)) || textures3[0]; + if (!Texture.all.find((t2) => t2.use_as_default)) { primary.use_as_default = true; } } - return textures; + return textures3; } function promptForTextures(dirname) { Blockbench.showMessageBox({ - title: "Import Textures", - message: "No textures were found for this model. How would you like to import textures?", - buttons: ["Select Files", "Select Folder", "Skip"] + title: t("textures.import"), + message: t("textures.message"), + buttons: [t("textures.select_files"), t("textures.select_folder"), t("textures.skip")] }, (choice) => { let project = Project; if (choice === 2 || !project) return; @@ -98,7 +316,7 @@ }); } else if (choice === 1) { let folderPath = Blockbench.pickDirectory({ - title: "Select Texture Folder", + title: t("directory.select_textures_folder"), startpath: dirname, resource_id: "texture" }); @@ -107,7 +325,7 @@ let files = fs.readdirSync(folderPath); let pngFiles = files.filter((f) => f.match(/\.png$/i)); if (pngFiles.length === 0) { - Blockbench.showQuickMessage("No PNG files found in selected folder"); + Blockbench.showQuickMessage(t("textures.no_textures_found"), 3e3); return; } let paths = pngFiles.map((f) => PathModule.join(folderPath, f)); @@ -722,8 +940,8 @@ } }); let export_action = new Action("export_blockymodel", { - name: "Export Hytale Blockymodel", - description: "Export a blockymodel file", + name: t("action.export_blockymodel"), + description: t("action.export_blockymodel_desc"), icon: "icon-format_hytale", category: "file", condition: { formats: FORMAT_IDS }, @@ -782,42 +1000,33 @@ }; let format_page = { content: [ - { type: "h3", text: tl("mode.start.format.informations") }, - { - text: `* One texture can be applied to a model at a time - * UV sizes are linked to the size of each cube and cannot be modified, except by stretching the cube - * Models can have a maximum of 255 nodes`.replace(/(\t| {4,4})+/g, "") - }, - { type: "h3", text: tl("mode.start.format.resources") }, - { - text: [ - "* [Modeling Overview and Style Guide](https://hytale.com/news/2025/12/an-introduction-to-making-models-for-hytale)", - "* [Modeling Tutorial](https://youtu.be/Q07i3wmGy0Y)" - ].join("\n") - } + { type: "h3", text: t("formats.page.informations") }, + { text: t("formats.page.info_list") }, + { type: "h3", text: t("formats.page.resources") }, + { text: t("formats.page.resource_list") } ] }; let format_character = new ModelFormat("hytale_character", { - name: "Hytale Character", - description: "Create character and attachment models using Hytale's blockymodel format", + name: t("formats.character.name"), + description: t("formats.character.description"), icon: "icon-format_hytale", format_page, block_size: 64, ...common // TODO: Auto-reload attachments on tab switch. Needs dirty tracking and setting toggle to avoid losing unsaved changes /* - onActivation() { - common.onActivation?.(); - setTimeout(() => reload_all_attachments?.click(), 0); - } - */ + onActivation() { + common.onActivation?.(); + setTimeout(() => reload_all_attachments?.click(), 0); + } + */ }); let format_prop = new ModelFormat("hytale_prop", { - name: "Hytale Prop", - description: "Create prop models using Hytale's blockymodel format", + name: t("formats.prop.name"), + description: t("formats.prop.description"), icon: "icon-format_hytale", format_page, - block_size: 32, + block_size: 64, ...common }); let int_setting = new Setting("hytale_integer_size", { @@ -838,7 +1047,7 @@ track(format_character); track(format_prop); Language.addTranslations("en", { - "format_category.hytale": "Hytale" + "format_category.hytale": t("formats.category") }); } function isHytaleFormat() { @@ -921,9 +1130,9 @@ } }); let setting = new Setting("hytale_duplicate_bone_names", { - name: "Duplicate Bone Names", + name: t("settings.duplicate_bones.name"), category: "edit", - description: "Allow creating duplicate groups names in Hytale formats. Multiple groups with the same name can be used to apply animations to multiple nodes at once.", + description: t("settings.duplicate_bones.description"), type: "toggle", value: false }); @@ -1184,8 +1393,8 @@ } function setupTextureHandling() { let setting = new Setting("preview_selected_texture", { - name: "Preview Selected Texture", - description: "When selecting a texture in a Hytale format, preview the texture on the model instantly", + name: t("settings.preview_texture.name"), + description: t("settings.preview_texture.description"), category: "preview", type: "toggle", value: true @@ -1222,7 +1431,7 @@ tex.group = textureGroup.uuid; updateUVSize(tex); } - let texture = newTextures.find((t) => t.name.startsWith(attachmentName)) ?? newTextures[0]; + let texture = newTextures.find((t2) => t2.name.startsWith(attachmentName)) ?? newTextures[0]; return texture.uuid; } function setupAttachmentTextures() { @@ -1237,7 +1446,7 @@ let collection = getCollection(this.cube); if (collection && "texture" in collection) { if (collection.texture) { - let texture = Texture.all.find((t) => t.uuid == collection.texture); + let texture = Texture.all.find((t2) => t2.uuid == collection.texture); if (texture) return texture; } return null; @@ -1274,14 +1483,14 @@ } } ]; - Texture.all.forEach((t) => { + Texture.all.forEach((t2) => { arr.push({ - name: t.name, + name: t2.name, // @ts-expect-error - icon: t.img, - marked: t.uuid == context.texture, + icon: t2.img, + marked: t2.uuid == context.texture, click() { - applyTexture(t.uuid, "Apply texture to collection"); + applyTexture(t2.uuid, "Apply texture to collection"); } }); }); @@ -1308,7 +1517,7 @@ let collections = Collection.selected.slice(); let remove_elements = []; let remove_groups = []; - let textures = []; + let textures3 = []; let texture_groups = []; for (let collection of collections) { if (collection.export_codec === "blockymodel") { @@ -1318,8 +1527,8 @@ } let texture_group = TextureGroup.all.find((tg) => tg.name === collection.name); if (texture_group) { - let textures2 = Texture.all.filter((t) => t.group === texture_group.uuid); - textures.safePush(...textures2); + let textures22 = Texture.all.filter((t2) => t2.group === texture_group.uuid); + textures3.safePush(...textures22); texture_groups.push(texture_group); } } @@ -1331,13 +1540,13 @@ outliner: true, // @ts-expect-error texture_groups, - textures + textures: textures3 }); collections.forEach((c) => Collection.all.remove(c)); collections.empty(); - textures.forEach((t) => t.remove(true)); - textures.empty(); - texture_groups.forEach((t) => t.remove()); + textures3.forEach((t2) => t2.remove(true)); + textures3.empty(); + texture_groups.forEach((t2) => t2.remove()); texture_groups.empty(); remove_groups.forEach((group) => group.remove()); remove_groups.empty(); @@ -1349,46 +1558,56 @@ }); track(shared_delete); let import_as_attachment = new Action("import_as_hytale_attachment", { - name: "Import Attachment", + name: t("menu.import_attachment"), icon: "fa-hat-cowboy", condition: { formats: FORMAT_IDS }, click() { - Filesystem.importFile({ - extensions: ["blockymodel"], - type: "Blockymodel", - multiple: true, - startpath: Project.export_path.replace(/[\\\/]\w+.\w+$/, "") + osfs + "Attachments" - }, (files) => { - for (let file of files) { - let json = autoParseJSON(file.content); - let attachment_name = file.name.replace(/\.\w+$/, ""); - let content = Codecs.blockymodel.parse(json, file.path, { attachment: attachment_name }); - let name = file.name.split(".")[0]; - let new_groups = content.new_groups; - let root_groups = new_groups.filter((group) => !new_groups.includes(group.parent)); - let collection = new Collection({ - name, - children: root_groups.map((g) => g.uuid), - export_codec: "blockymodel", - visibility: true - }).add(); - collection.export_path = file.path; - let texturesToProcess = content.new_textures; - if (texturesToProcess.length === 0) { - let dirname = PathModule.dirname(file.path); - let texturePaths = discoverTexturePaths(dirname, attachment_name); - for (let texPath of texturePaths) { - let tex = new Texture().fromPath(texPath).add(false); - texturesToProcess.push(tex); + Filesystem.importFile( + { + extensions: ["blockymodel"], + type: "Blockymodel", + multiple: true, + startpath: Project.export_path.replace(/[\\\/]\w+.\w+$/, "") + osfs + "Attachments" + }, + (files) => { + for (let file of files) { + let json = autoParseJSON(file.content); + let attachment_name = file.name.replace(/\.\w+$/, ""); + let content = Codecs.blockymodel.parse(json, file.path, { + attachment: attachment_name + }); + let name = file.name.split(".")[0]; + let new_groups = content.new_groups; + let root_groups = new_groups.filter( + (group) => !new_groups.includes(group.parent) + ); + let collection = new Collection({ + name, + children: root_groups.map((g) => g.uuid), + export_codec: "blockymodel", + visibility: true + }).add(); + collection.export_path = file.path; + let texturesToProcess = content.new_textures; + if (texturesToProcess.length === 0) { + let dirname = PathModule.dirname(file.path); + let texturePaths = discoverTexturePaths(dirname, attachment_name); + for (let texPath of texturePaths) { + let tex = new Texture().fromPath(texPath).add(false); + texturesToProcess.push(tex); + } } + let textureUuid = processAttachmentTextures( + attachment_name, + texturesToProcess + ); + if (textureUuid) { + collection.texture = textureUuid; + } + Canvas.updateAllFaces(); } - let textureUuid = processAttachmentTextures(attachment_name, texturesToProcess); - if (textureUuid) { - collection.texture = textureUuid; - } - Canvas.updateAllFaces(); } - }); + ); } }); track(import_as_attachment); @@ -1410,7 +1629,7 @@ }); } let reload_attachment_action = new Action("reload_hytale_attachment", { - name: "Reload Attachment", + name: t("actions.reload_attachments"), icon: "refresh", condition: () => Collection.selected.length && Modes.edit, click() { @@ -1422,7 +1641,7 @@ Collection.menu.addAction(reload_attachment_action, 10); track(reload_attachment_action); reload_all_attachments = new Action("reload_all_hytale_attachments", { - name: "Reload All Attachments", + name: t("actions.reload_all_attachments"), icon: "sync", condition: { formats: FORMAT_IDS }, click() { @@ -1474,13 +1693,13 @@ default: true }); track(property); - function weightedCubicBezier(t) { + function weightedCubicBezier(t2) { let P0 = 0, P1 = 0.05, P2 = 0.95, P3 = 1; let W0 = 2, W1 = 1, W2 = 2, W3 = 1; - let b0 = (1 - t) ** 3; - let b1 = 3 * (1 - t) ** 2 * t; - let b2 = 3 * (1 - t) * t ** 2; - let b3 = t ** 3; + let b0 = (1 - t2) ** 3; + let b1 = 3 * (1 - t2) ** 2 * t2; + let b2 = 3 * (1 - t2) * t2 ** 2; + let b3 = t2 ** 3; let w0 = b0 * W0; let w1 = b1 * W1; let w2 = b2 * W2; @@ -1597,7 +1816,7 @@ }); track(is_piece_property); let add_quad_action = new Action("hytale_add_quad", { - name: "Add Quad", + name: t("action.hytale_add_quad.name"), icon: "highlighter_size_5", category: "edit", condition: { formats: FORMAT_IDS, modes: ["edit"] }, @@ -1653,7 +1872,7 @@ UVEditor.getSelectedFaces(base_quad, true).replace([fkey]); base_quad.select(); Canvas.updateView({ elements: [base_quad], element_aspects: { transform: true, geometry: true, faces: true } }); - Undo.finishEdit("Add quad", { outliner: true, elements: selected, selection: true }); + Undo.finishEdit(t("action.hytale_add_quad.name"), { outliner: true, elements: selected, selection: true }); Vue.nextTick(function() { if (settings.create_rename.value) { base_quad.rename(); @@ -1935,8 +2154,9 @@ // src/photoshop_copy_paste.ts function setupPhotoshopTools() { let setting = new Setting("copy_paste_magenta_alpha", { - name: "Copy-Paste with Magenta Alpha", - description: "Copy image selections with magenta background and remove magenta when pasting to help transfer transparency to Photoshop", + name: t("settings.photoshop_copy.name"), + description: t("settings.photoshop_copy.description"), + category: "paint", type: "toggle", category: "paint", value: false @@ -1964,7 +2184,11 @@ copy_canvas.width = rect.width; copy_canvas.height = rect.height; selection.maskCanvas(copy_ctx, [rect.start_x, rect.start_y]); - copy_ctx.drawImage(canvas, -rect.start_x + offset[0], -rect.start_y + offset[1]); + copy_ctx.drawImage( + canvas, + -rect.start_x + offset[0], + -rect.start_y + offset[1] + ); Clipbench.image = { x: rect.start_x, y: rect.start_y, @@ -1996,7 +2220,9 @@ }); } if (cut) { - SharedActions.runSpecific("delete", "image_content", event, { message: "Cut texture selection" }); + SharedActions.runSpecific("delete", "image_content", event, { + message: t("actions.cut_selection") + }); } } }); @@ -2017,7 +2243,10 @@ } let offset; if (Clipbench.image) { - offset = [Math.clamp(Clipbench.image.x, 0, texture.width), Math.clamp(Clipbench.image.y, 0, texture.height)]; + offset = [ + Math.clamp(Clipbench.image.x, 0, texture.width), + Math.clamp(Clipbench.image.y, 0, texture.height) + ]; offset[0] = Math.clamp(offset[0], 0, texture.width - frame.width); offset[1] = Math.clamp(offset[1], 0, texture.height - frame.height); } @@ -2025,8 +2254,16 @@ if (old_frame || texture.currentFrame) { offset[1] += texture.display_height * ((texture.currentFrame || 0) - old_frame); } - let layer = new TextureLayer({ name: "pasted", offset }, texture); - let image_data = frame.ctx.getImageData(0, 0, frame.width, frame.height); + let layer = new TextureLayer( + { name: t("actions.pasted_layer_name"), offset }, + texture + ); + let image_data = frame.ctx.getImageData( + 0, + 0, + frame.width, + frame.height + ); for (let i = 0; i < image_data.data.length; i += 4) { if (image_data.data[i] == 255 && image_data.data[i + 1] == 0 && image_data.data[i + 2] == 255) { image_data.data[i + 0] = 0; @@ -2041,8 +2278,9 @@ layer.addForEditing(); layer.setLimbo(); texture.updateChangesAfterEdit(); - Undo.finishEdit("Paste into texture"); - if (Toolbox.selected.id != "selection_tool") BarItems.move_layer_tool.select(); + Undo.finishEdit(t("actions.paste_texture")); + if (Toolbox.selected.id != "selection_tool") + BarItems.move_layer_tool.select(); updateInterfacePanels(); BARS.updateConditions(); } @@ -2127,7 +2365,10 @@ size ]; let geometry = new THREE.BufferGeometry(); - geometry.setAttribute("position", new THREE.Float32BufferAttribute(vertices, 3)); + geometry.setAttribute( + "position", + new THREE.Float32BufferAttribute(vertices, 3) + ); let material = new THREE.LineBasicMaterial({ vertexColors: true }); super(geometry, material); this.updateColors(); @@ -2156,7 +2397,10 @@ ...gizmo_colors.b.toArray(), ...gizmo_colors.b.toArray() ]; - this.geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3)); + this.geometry.setAttribute( + "color", + new THREE.Float32BufferAttribute(colors, 3) + ); } }; ThickLineAxisHelper.prototype.constructor = ThickLineAxisHelper; @@ -2184,8 +2428,8 @@ setting; constructor() { this.setting = new Setting("show_group_pivot_indicator", { - name: "Show Group Pivot Indicator", - description: "Show a dot in Edit mode indicating the rotation pivot point for animations", + name: t("settings.group_pivot.name"), + description: t("settings.group_pivot.description"), category: "preview", type: "toggle", value: true @@ -2202,7 +2446,10 @@ this.dot.visible = false; Canvas.scene.add(this.dot); this.listener = Blockbench.on("update_selection", () => this.update()); - this.cameraListener = Blockbench.on("update_camera_position", () => this.updateScale()); + this.cameraListener = Blockbench.on( + "update_camera_position", + () => this.updateScale() + ); this.update(); } updateScale() { @@ -2228,7 +2475,9 @@ this.dot.visible = false; return; } - this.dot.material.color.copy(this.getAccentColor()); + this.dot.material.color.copy( + this.getAccentColor() + ); let mesh = group.mesh; if (mesh) { let worldPos = new THREE.Vector3(); @@ -2362,7 +2611,7 @@ `); StateMemory.init("hytale_attachments_hidden", "boolean"); attachmentsHidden = StateMemory.get("hytale_attachments_hidden") ?? false; - let toggle = new Toggle("toggle_attachment_editing", { + let toggle3 = new Toggle("toggle_attachment_editing", { name: "Toggle Attachment Editing", description: "Lock or unlock attachment elements for editing", icon: "fa-paperclip", @@ -2377,7 +2626,7 @@ }); let outlinerPanel = Panels.outliner; if (outlinerPanel && outlinerPanel.toolbars.length > 0) { - outlinerPanel.toolbars[0].add(toggle, -1); + outlinerPanel.toolbars[0].add(toggle3, -1); } let hookFinishedEdit = Blockbench.on("finished_edit", scheduleVisibilityUpdate); let hookSelectMode = Blockbench.on("select_mode", scheduleVisibilityUpdate); @@ -2385,7 +2634,7 @@ if (attachmentsHidden) { setTimeout(applyOutlinerVisibility, 100); } - track(toggle, hookFinishedEdit, hookSelectMode, hookSelection, style, { + track(toggle3, hookFinishedEdit, hookSelectMode, hookSelection, style, { delete() { Panels.outliner?.node?.querySelectorAll(`.${HIDDEN_CLASS}`).forEach((el) => { el.classList.remove(HIDDEN_CLASS); @@ -3170,7 +3419,7 @@ body.hytale-uv-outline-only #uv_frame .selection_rectangle { author: "JannisX11, Kanno", icon: "icon.png", version: package_default.version, - description: "Create models and animations for Hytale", + description: t("plugin.description"), tags: ["Hytale"], variant: "both", min_version: "5.0.5", diff --git a/src/attachments.ts b/src/attachments.ts index 0c77446..d93e586 100644 --- a/src/attachments.ts +++ b/src/attachments.ts @@ -9,6 +9,7 @@ import { setupAttachmentTextures, processAttachmentTextures, } from "./attachment_texture"; +import { t } from "./i18n"; export { AttachmentCollection } from "./attachment_texture"; export let reload_all_attachments: Action; @@ -74,55 +75,69 @@ export function setupAttachments() { }); track(shared_delete); - let import_as_attachment = new Action('import_as_hytale_attachment', { - name: 'Import Attachment', - icon: 'fa-hat-cowboy', - condition: {formats: FORMAT_IDS}, - click() { - Filesystem.importFile({ - extensions: ['blockymodel'], - type: 'Blockymodel', - multiple: true, - startpath: Project.export_path.replace(/[\\\/]\w+.\w+$/, '') + osfs + 'Attachments' - }, (files) => { - for (let file of files) { - let json = autoParseJSON(file.content as string); - let attachment_name = file.name.replace(/\.\w+$/, ''); - let content: any = Codecs.blockymodel.parse(json, file.path, {attachment: attachment_name}); - let name = file.name.split('.')[0] - - let new_groups = content.new_groups as Group[]; - let root_groups = new_groups.filter(group => !new_groups.includes(group.parent as Group)); - - let collection = new Collection({ - name, - children: root_groups.map(g => g.uuid), - export_codec: 'blockymodel', - visibility: true, - }).add() as AttachmentCollection; - collection.export_path = file.path; - - let texturesToProcess: Texture[] = content.new_textures as Texture[]; - - if (texturesToProcess.length === 0) { - let dirname = PathModule.dirname(file.path); - let texturePaths = discoverTexturePaths(dirname, attachment_name); - for (let texPath of texturePaths) { - let tex = new Texture().fromPath(texPath).add(false); - texturesToProcess.push(tex); - } - } - - let textureUuid = processAttachmentTextures(attachment_name, texturesToProcess); - if (textureUuid) { - collection.texture = textureUuid; - } - - Canvas.updateAllFaces(); - } - }) - } - }); + let import_as_attachment = new Action("import_as_hytale_attachment", { + name: t("menu.import_attachment"), + icon: "fa-hat-cowboy", + condition: { formats: FORMAT_IDS }, + click() { + Filesystem.importFile( + { + extensions: ["blockymodel"], + type: "Blockymodel", + multiple: true, + startpath: + Project.export_path.replace(/[\\\/]\w+.\w+$/, "") + + osfs + + "Attachments", + }, + (files) => { + for (let file of files) { + let json = autoParseJSON(file.content as string); + let attachment_name = file.name.replace(/\.\w+$/, ""); + let content: any = Codecs.blockymodel.parse(json, file.path, { + attachment: attachment_name, + }); + let name = file.name.split(".")[0]; + + let new_groups = content.new_groups as Group[]; + let root_groups = new_groups.filter( + (group) => !new_groups.includes(group.parent as Group) + ); + + let collection = new Collection({ + name, + children: root_groups.map((g) => g.uuid), + export_codec: "blockymodel", + visibility: true, + }).add() as AttachmentCollection; + collection.export_path = file.path; + + let texturesToProcess: Texture[] = + content.new_textures as Texture[]; + + if (texturesToProcess.length === 0) { + let dirname = PathModule.dirname(file.path); + let texturePaths = discoverTexturePaths(dirname, attachment_name); + for (let texPath of texturePaths) { + let tex = new Texture().fromPath(texPath).add(false); + texturesToProcess.push(tex); + } + } + + let textureUuid = processAttachmentTextures( + attachment_name, + texturesToProcess + ); + if (textureUuid) { + collection.texture = textureUuid; + } + + Canvas.updateAllFaces(); + } + } + ); + }, + }); track(import_as_attachment); let toolbar = Panels.collections.toolbars[0]; toolbar.add(import_as_attachment); @@ -148,7 +163,7 @@ export function setupAttachments() { } let reload_attachment_action = new Action('reload_hytale_attachment', { - name: 'Reload Attachment', + name: t('actions.reload_attachments'), icon: 'refresh', condition: () => Collection.selected.length && Modes.edit, click() { @@ -161,7 +176,7 @@ export function setupAttachments() { track(reload_attachment_action); reload_all_attachments = new Action('reload_all_hytale_attachments', { - name: 'Reload All Attachments', + name: t('actions.reload_all_attachments'), icon: 'sync', condition: {formats: FORMAT_IDS}, click() { diff --git a/src/blockymodel.ts b/src/blockymodel.ts index 7693c48..f7d5974 100644 --- a/src/blockymodel.ts +++ b/src/blockymodel.ts @@ -5,6 +5,7 @@ import { parseAnimationFile } from "./blockyanim" import { track } from "./cleanup" import { Config } from "./config" import { FORMAT_IDS } from "./formats" +import { t } from "./i18n" import { cubeIsQuad, getMainShape, qualifiesAsMainShape } from "./util" type BlockymodelJSON = { @@ -131,9 +132,9 @@ function loadTexturesFromPaths(paths: string[], preferredName?: string): Texture */ function promptForTextures(dirname: string) { Blockbench.showMessageBox({ - title: 'Import Textures', - message: 'No textures were found for this model. How would you like to import textures?', - buttons: ['Select Files', 'Select Folder', 'Skip'], + title: t('textures.import'), + message: t('textures.message'), + buttons: [t('textures.select_files'), t('textures.select_folder'), t('textures.skip')], }, (choice) => { let project = Project; if (choice === 2 || !project) return; @@ -155,17 +156,17 @@ function promptForTextures(dirname: string) { } else if (choice === 1) { // Select folder containing textures let folderPath = Blockbench.pickDirectory({ - title: 'Select Texture Folder', - startpath: dirname, - resource_id: 'texture' - }); + title: t("directory.select_textures_folder"), + startpath: dirname, + resource_id: "texture", + }); if (folderPath && Project === project) { let fs = requireNativeModule('fs'); let files = fs.readdirSync(folderPath); let pngFiles = files.filter((f: string) => f.match(/\.png$/i)); if (pngFiles.length === 0) { - Blockbench.showQuickMessage('No PNG files found in selected folder'); + Blockbench.showQuickMessage(t('textures.no_textures_found'), 3000); return; } @@ -827,8 +828,8 @@ export function setupBlockymodelCodec(): Codec { }, }) let export_action = new Action('export_blockymodel', { - name: 'Export Hytale Blockymodel', - description: 'Export a blockymodel file', + name: t('action.export_blockymodel'), + description: t('action.export_blockymodel_desc'), icon: 'icon-format_hytale', category: 'file', condition: {formats: FORMAT_IDS}, diff --git a/src/element.ts b/src/element.ts index 0f0b586..e535a8c 100644 --- a/src/element.ts +++ b/src/element.ts @@ -2,6 +2,8 @@ //! Licensed under the GNU General Public License, see LICENSE.MD import { track } from "./cleanup"; + +import { t } from "./i18n"; import { FORMAT_IDS, isHytaleFormat } from "./formats"; import { cubeIsQuad } from "./util"; @@ -115,7 +117,7 @@ export function setupElements() { track(is_piece_property); let add_quad_action = new Action('hytale_add_quad', { - name: 'Add Quad', + name: t('action.hytale_add_quad.name'), icon: 'highlighter_size_5', category: 'edit', condition: {formats: FORMAT_IDS, modes: ['edit']}, @@ -178,7 +180,7 @@ export function setupElements() { base_quad.select(); Canvas.updateView({elements: [base_quad], element_aspects: {transform: true, geometry: true, faces: true}}) - Undo.finishEdit('Add quad', {outliner: true, elements: selected, selection: true}); + Undo.finishEdit(t('action.hytale_add_quad.name'), {outliner: true, elements: selected, selection: true}); Vue.nextTick(function() { if (settings.create_rename.value) { diff --git a/src/formats.ts b/src/formats.ts index 42342a8..9dd2424 100644 --- a/src/formats.ts +++ b/src/formats.ts @@ -3,6 +3,7 @@ import { setupBlockymodelCodec } from "./blockymodel"; import { track } from "./cleanup"; +import { t } from "./i18n"; export const FORMAT_IDS = [ 'hytale_character', @@ -16,7 +17,6 @@ export function setupFormats() { category: 'hytale', target: 'Hytale', codec, - forward_direction: '+z', single_texture_default: true, animation_files: true, @@ -42,41 +42,36 @@ export function setupFormats() { } } let format_page: FormatPage = { - content: [ - {type: 'h3', text: tl('mode.start.format.informations')}, - {text: `* One texture can be applied to a model at a time - * UV sizes are linked to the size of each cube and cannot be modified, except by stretching the cube - * Models can have a maximum of 255 nodes`.replace(/(\t| {4,4})+/g, '') - }, - {type: 'h3', text: tl('mode.start.format.resources')}, - {text: ['* [Modeling Overview and Style Guide](https://hytale.com/news/2025/12/an-introduction-to-making-models-for-hytale)', - '* [Modeling Tutorial](https://youtu.be/Q07i3wmGy0Y)'].join('\n') - } - ] + content: [ + { type: "h3", text: t("formats.page.informations") }, + { text: t("formats.page.info_list") }, + { type: "h3", text: t("formats.page.resources") }, + { text: t("formats.page.resource_list") }, + ], }; - let format_character = new ModelFormat('hytale_character', { - name: 'Hytale Character', - description: 'Create character and attachment models using Hytale\'s blockymodel format', - icon: 'icon-format_hytale', - format_page, - block_size: 64, - ...common - // TODO: Auto-reload attachments on tab switch. Needs dirty tracking and setting toggle to avoid losing unsaved changes - /* + let format_character = new ModelFormat("hytale_character", { + name: t("formats.character.name"), + description: t("formats.character.description"), + icon: "icon-format_hytale", + format_page, + block_size: 64, + ...common, + // TODO: Auto-reload attachments on tab switch. Needs dirty tracking and setting toggle to avoid losing unsaved changes + /* onActivation() { common.onActivation?.(); setTimeout(() => reload_all_attachments?.click(), 0); } */ }); - let format_prop = new ModelFormat('hytale_prop', { - name: 'Hytale Prop', - description: 'Create prop models using Hytale\'s blockymodel format', - icon: 'icon-format_hytale', - format_page, - block_size: 32, - ...common + let format_prop = new ModelFormat("hytale_prop", { + name: t("formats.prop.name"), + description: t("formats.prop.description"), + icon: "icon-format_hytale", + format_page, + block_size: 64, + ...common, }); // Integer size @@ -100,9 +95,9 @@ export function setupFormats() { track(format_character); track(format_prop); - Language.addTranslations('en', { - 'format_category.hytale': 'Hytale' - }) + Language.addTranslations("en", { + "format_category.hytale": t("formats.category"), + }); } export function isHytaleFormat() { diff --git a/src/i18n.ts b/src/i18n.ts new file mode 100644 index 0000000..eb5e74d --- /dev/null +++ b/src/i18n.ts @@ -0,0 +1,31 @@ +/// + + +import * as en from "./translations/en.json"; +import * as pt from "./translations/pt-BR.json"; + +type TranslationSchema = typeof en; + + +const translations: Record = { + en, + pt, +}; + +export function t(path: string): string { + const langCode = Language.code.split("_")[0]; + + const currentLangObj = translations[langCode] || translations["en"]; + const resolvePath = (obj: any, path: string) => { + return path.split(".").reduce((prev, curr) => { + return prev ? prev[curr] : null; + }, obj); + }; + + let result = resolvePath(currentLangObj, path); + if (!result && langCode !== "en") { + result = resolvePath(translations["en"], path); + } + + return result || path; +} diff --git a/src/name_overlap.ts b/src/name_overlap.ts index a5360b0..4b9845a 100644 --- a/src/name_overlap.ts +++ b/src/name_overlap.ts @@ -3,6 +3,7 @@ import { track } from "./cleanup"; import { isHytaleFormat } from "./formats"; +import { t } from "./i18n"; // @ts-expect-error const Animation = window.Animation as typeof _Animation; @@ -89,13 +90,13 @@ export function setupNameOverlap() { } }) - let setting = new Setting('hytale_duplicate_bone_names', { - name: 'Duplicate Bone Names', - category: 'edit', - description: 'Allow creating duplicate groups names in Hytale formats. Multiple groups with the same name can be used to apply animations to multiple nodes at once.', - type: 'toggle', - value: false - }) + let setting = new Setting("hytale_duplicate_bone_names", { + name: t("settings.duplicate_bones.name"), + category: "edit", + description: t("settings.duplicate_bones.description"), + type: "toggle", + value: false, + }); let override = Group.addBehaviorOverride({ condition: () => isHytaleFormat() && setting.value == true, // @ts-ignore diff --git a/src/photoshop_copy_paste.ts b/src/photoshop_copy_paste.ts index d68f47d..c78577f 100644 --- a/src/photoshop_copy_paste.ts +++ b/src/photoshop_copy_paste.ts @@ -3,162 +3,199 @@ import { track } from "./cleanup"; import { FORMAT_IDS } from "./formats"; +import { t } from "./i18n"; declare global { const Clipbench: any } export function setupPhotoshopTools() { - - let setting = new Setting('copy_paste_magenta_alpha', { - name: 'Copy-Paste with Magenta Alpha', - description: 'Copy image selections with magenta background and remove magenta when pasting to help transfer transparency to Photoshop', - type: 'toggle', - category: 'paint', - value: false - }) - track(setting); - - let shared_copy = SharedActions.add('copy', { - subject: 'image_content_photoshop', - condition: () => Prop.active_panel == 'uv' && Modes.paint && Texture.getDefault() && FORMAT_IDS.includes(Format.id) && setting.value == true, - priority: 2, - run(event, cut) { - let texture = Texture.getDefault(); - let selection = texture.selection; - - let {canvas, ctx, offset} = texture.getActiveCanvas(); - - if (selection.override != null) { - Clipbench.image = { - x: offset[0], y: offset[1], - frame: texture.currentFrame, - data: '' - } - } else { - let rect = selection.getBoundingRect(); - let copy_canvas = document.createElement('canvas'); - let copy_ctx = copy_canvas.getContext('2d'); - copy_canvas.width = rect.width; - copy_canvas.height = rect.height; - - selection.maskCanvas(copy_ctx, [rect.start_x, rect.start_y]); - copy_ctx.drawImage(canvas, -rect.start_x + offset[0], -rect.start_y + offset[1]); - - Clipbench.image = { - x: rect.start_x, - y: rect.start_y, - frame: texture.currentFrame, - data: '' - } - canvas = copy_canvas; - } - - // Add pink background - let canvas_copy_magenta = document.createElement('canvas'); - let copy_ctx_magenta = canvas_copy_magenta.getContext('2d'); - canvas_copy_magenta.width = canvas.width; - canvas_copy_magenta.height = canvas.height; - copy_ctx_magenta.fillStyle = '#ff00ff'; - copy_ctx_magenta.fillRect(0, 0, canvas.width, canvas.height); - copy_ctx_magenta.drawImage(canvas, 0, 0); - canvas = canvas_copy_magenta; - - Clipbench.image.data = canvas.toDataURL('image/png', 1); - - if (isApp) { - let clipboard = requireNativeModule('clipboard'); - // @ts-expect-error - let img = nativeImage.createFromDataURL(Clipbench.image.data); - clipboard.writeImage(img); - } else { - canvas.toBlob(blob => { - navigator.clipboard.write([ - new ClipboardItem({ - [blob.type]: blob, - }), - ]); - }); - } - - if (cut) { - SharedActions.runSpecific('delete', 'image_content', event, {message: 'Cut texture selection'}); - } + let setting = new Setting("copy_paste_magenta_alpha", { + name: t("settings.photoshop_copy.name"), + description: t("settings.photoshop_copy.description"), + category: "paint", + type: "toggle", + value: false, + }); + track(setting); + + let shared_copy = SharedActions.add("copy", { + subject: "image_content_photoshop", + condition: () => + Prop.active_panel == "uv" && + Modes.paint && + Texture.getDefault() && + FORMAT_IDS.includes(Format.id) && + setting.value == true, + priority: 2, + run(event, cut) { + let texture = Texture.getDefault(); + let selection = texture.selection; + + let { canvas, ctx, offset } = texture.getActiveCanvas(); + + if (selection.override != null) { + Clipbench.image = { + x: offset[0], + y: offset[1], + frame: texture.currentFrame, + data: "", + }; + } else { + let rect = selection.getBoundingRect(); + let copy_canvas = document.createElement("canvas"); + let copy_ctx = copy_canvas.getContext("2d"); + copy_canvas.width = rect.width; + copy_canvas.height = rect.height; + + selection.maskCanvas(copy_ctx, [rect.start_x, rect.start_y]); + copy_ctx.drawImage( + canvas, + -rect.start_x + offset[0], + -rect.start_y + offset[1] + ); + + Clipbench.image = { + x: rect.start_x, + y: rect.start_y, + frame: texture.currentFrame, + data: "", + }; + canvas = copy_canvas; + } + + // Add pink background + let canvas_copy_magenta = document.createElement("canvas"); + let copy_ctx_magenta = canvas_copy_magenta.getContext("2d"); + canvas_copy_magenta.width = canvas.width; + canvas_copy_magenta.height = canvas.height; + copy_ctx_magenta.fillStyle = "#ff00ff"; + copy_ctx_magenta.fillRect(0, 0, canvas.width, canvas.height); + copy_ctx_magenta.drawImage(canvas, 0, 0); + canvas = canvas_copy_magenta; + + Clipbench.image.data = canvas.toDataURL("image/png", 1); + + if (isApp) { + let clipboard = requireNativeModule("clipboard"); + // @ts-expect-error + let img = nativeImage.createFromDataURL(Clipbench.image.data); + clipboard.writeImage(img); + } else { + canvas.toBlob((blob) => { + navigator.clipboard.write([ + new ClipboardItem({ + [blob.type]: blob, + }), + ]); + }); + } + + if (cut) { + SharedActions.runSpecific("delete", "image_content", event, { + message: t("actions.cut_selection"), + }); + } + }, + }); + track(shared_copy); + let shared_paste = SharedActions.add("paste", { + subject: "image_content_photoshop", + condition: () => + Prop.active_panel == "uv" && + Modes.paint && + Texture.getDefault() && + FORMAT_IDS.includes(Format.id) && + setting.value == true, + priority: 2, + run(event) { + let texture = Texture.getDefault(); + + async function loadFromDataUrl(data_url: string) { + let frame = new CanvasFrame(); + await frame.loadFromURL(data_url); + + Undo.initEdit({ textures: [texture], bitmap: true }); + if (!texture.layers_enabled) { + texture.flags.add("temporary_layers"); + texture.activateLayers(false); } - }) - track(shared_copy); - let shared_paste = SharedActions.add('paste', { - subject: 'image_content_photoshop', - condition: () => Prop.active_panel == 'uv' && Modes.paint && Texture.getDefault() && FORMAT_IDS.includes(Format.id) && setting.value == true, - priority: 2, - run(event) { - - let texture = Texture.getDefault(); - - async function loadFromDataUrl(data_url: string) { - let frame = new CanvasFrame(); - await frame.loadFromURL(data_url); - - Undo.initEdit({textures: [texture], bitmap: true}); - if (!texture.layers_enabled) { - texture.flags.add('temporary_layers'); - texture.activateLayers(false); - } - let offset; - if (Clipbench.image) { - offset = [Math.clamp(Clipbench.image.x, 0, texture.width), Math.clamp(Clipbench.image.y, 0, texture.height)]; - offset[0] = Math.clamp(offset[0], 0, texture.width-frame.width); - offset[1] = Math.clamp(offset[1], 0, texture.height-frame.height); - } - let old_frame = Clipbench.image?.frame || 0; - if (old_frame || texture.currentFrame) { - offset[1] += texture.display_height * ((texture.currentFrame||0) - old_frame); - } - let layer = new TextureLayer({name: 'pasted', offset}, texture); - let image_data = frame.ctx.getImageData(0, 0, frame.width, frame.height); - - // Filter magenta - for (let i = 0; i < image_data.data.length; i += 4) { - if (image_data.data[i] == 255 && image_data.data[i+1] == 0 && image_data.data[i+2] == 255) { - image_data.data[i+0] = 0; - image_data.data[i+1] = 0; - image_data.data[i+2] = 0; - image_data.data[i+3] = 0; - } - } - - - layer.setSize(frame.width, frame.height); - layer.ctx.putImageData(image_data, 0, 0); - if (!offset) layer.center(); - - layer.addForEditing(); - layer.setLimbo(); - texture.updateChangesAfterEdit(); - - Undo.finishEdit('Paste into texture'); - if (Toolbox.selected.id != 'selection_tool') (BarItems.move_layer_tool as Tool).select(); - updateInterfacePanels(); - BARS.updateConditions(); - } - - - if (isApp) { - let clipboard = requireNativeModule('clipboard'); - var image = clipboard.readImage().toDataURL(); - loadFromDataUrl(image); - } else { - navigator.clipboard.read().then(content => { - if (content && content[0] && content[0].types.includes('image/png')) { - content[0].getType('image/png').then(blob => { - let url = URL.createObjectURL(blob); - loadFromDataUrl(url); - }) - } - }).catch(() => {}) - } - + let offset; + if (Clipbench.image) { + offset = [ + Math.clamp(Clipbench.image.x, 0, texture.width), + Math.clamp(Clipbench.image.y, 0, texture.height), + ]; + offset[0] = Math.clamp(offset[0], 0, texture.width - frame.width); + offset[1] = Math.clamp(offset[1], 0, texture.height - frame.height); + } + let old_frame = Clipbench.image?.frame || 0; + if (old_frame || texture.currentFrame) { + offset[1] += + texture.display_height * ((texture.currentFrame || 0) - old_frame); } - }); - track(shared_paste); + let layer = new TextureLayer( + { name: t("actions.pasted_layer_name"), offset }, + texture + ); + let image_data = frame.ctx.getImageData( + 0, + 0, + frame.width, + frame.height + ); + + // Filter magenta + for (let i = 0; i < image_data.data.length; i += 4) { + if ( + image_data.data[i] == 255 && + image_data.data[i + 1] == 0 && + image_data.data[i + 2] == 255 + ) { + image_data.data[i + 0] = 0; + image_data.data[i + 1] = 0; + image_data.data[i + 2] = 0; + image_data.data[i + 3] = 0; + } + } + + layer.setSize(frame.width, frame.height); + layer.ctx.putImageData(image_data, 0, 0); + if (!offset) layer.center(); + + layer.addForEditing(); + layer.setLimbo(); + texture.updateChangesAfterEdit(); + + Undo.finishEdit(t("actions.paste_texture")); + if (Toolbox.selected.id != "selection_tool") + (BarItems.move_layer_tool as Tool).select(); + updateInterfacePanels(); + BARS.updateConditions(); + } + + if (isApp) { + let clipboard = requireNativeModule("clipboard"); + var image = clipboard.readImage().toDataURL(); + loadFromDataUrl(image); + } else { + navigator.clipboard + .read() + .then((content) => { + if ( + content && + content[0] && + content[0].types.includes("image/png") + ) { + content[0].getType("image/png").then((blob) => { + let url = URL.createObjectURL(blob); + loadFromDataUrl(url); + }); + } + }) + .catch(() => {}); + } + }, + }); + track(shared_paste); } diff --git a/src/pivot_marker.ts b/src/pivot_marker.ts index 822110a..6e64ba1 100644 --- a/src/pivot_marker.ts +++ b/src/pivot_marker.ts @@ -1,79 +1,141 @@ //! Copyright (C) 2025 Hypixel Studios Canada inc. //! Licensed under the GNU General Public License, see LICENSE.MD +import { t } from "./i18n"; + declare global { - const gizmo_colors: Record + const gizmo_colors: Record; } export const ThickLineAxisHelper = class ThickLineAxisHelper extends THREE.LineSegments { - constructor( size: number = 1 ) { - - let a = 0.04, b = 0.025; - - let vertices = [ - 0, a, 0, size, a, 0, - 0, 0, b, size, 0, b, - 0, 0, -b, size, 0, -b, - - 0, 0, a, 0, size, a, - b, 0, 0, b, size, 0, - -b, 0, 0, -b, size, 0, - - a, 0, 0, a, 0, size, - 0, b, 0, 0, b, size, - 0, -b, 0, 0, -b, size, - ]; - - let geometry = new THREE.BufferGeometry(); - geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) ); - let material = new THREE.LineBasicMaterial( { vertexColors: true } ); - - super(geometry, material); - this.updateColors(); - - material.transparent = true; - material.depthTest = false; - this.renderOrder = 800; - } - updateColors() { - let colors = [ - ...gizmo_colors.r.toArray(), ...gizmo_colors.r.toArray(), - ...gizmo_colors.r.toArray(), ...gizmo_colors.r.toArray(), - ...gizmo_colors.r.toArray(), ...gizmo_colors.r.toArray(), - - ...gizmo_colors.g.toArray(), ...gizmo_colors.g.toArray(), - ...gizmo_colors.g.toArray(), ...gizmo_colors.g.toArray(), - ...gizmo_colors.g.toArray(), ...gizmo_colors.g.toArray(), - - ...gizmo_colors.b.toArray(), ...gizmo_colors.b.toArray(), - ...gizmo_colors.b.toArray(), ...gizmo_colors.b.toArray(), - ...gizmo_colors.b.toArray(), ...gizmo_colors.b.toArray(), - ] - this.geometry.setAttribute( 'color', new THREE.Float32BufferAttribute( colors, 3 ) ); - } -} + constructor(size: number = 1) { + let a = 0.04, + b = 0.025; + + let vertices = [ + 0, + a, + 0, + size, + a, + 0, + 0, + 0, + b, + size, + 0, + b, + 0, + 0, + -b, + size, + 0, + -b, + + 0, + 0, + a, + 0, + size, + a, + b, + 0, + 0, + b, + size, + 0, + -b, + 0, + 0, + -b, + size, + 0, + + a, + 0, + 0, + a, + 0, + size, + 0, + b, + 0, + 0, + b, + size, + 0, + -b, + 0, + 0, + -b, + size, + ]; + + let geometry = new THREE.BufferGeometry(); + geometry.setAttribute( + "position", + new THREE.Float32BufferAttribute(vertices, 3) + ); + let material = new THREE.LineBasicMaterial({ vertexColors: true }); + + super(geometry, material); + this.updateColors(); + + material.transparent = true; + material.depthTest = false; + this.renderOrder = 800; + } + updateColors() { + let colors = [ + ...gizmo_colors.r.toArray(), + ...gizmo_colors.r.toArray(), + ...gizmo_colors.r.toArray(), + ...gizmo_colors.r.toArray(), + ...gizmo_colors.r.toArray(), + ...gizmo_colors.r.toArray(), + + ...gizmo_colors.g.toArray(), + ...gizmo_colors.g.toArray(), + ...gizmo_colors.g.toArray(), + ...gizmo_colors.g.toArray(), + ...gizmo_colors.g.toArray(), + ...gizmo_colors.g.toArray(), + + ...gizmo_colors.b.toArray(), + ...gizmo_colors.b.toArray(), + ...gizmo_colors.b.toArray(), + ...gizmo_colors.b.toArray(), + ...gizmo_colors.b.toArray(), + ...gizmo_colors.b.toArray(), + ]; + this.geometry.setAttribute( + "color", + new THREE.Float32BufferAttribute(colors, 3) + ); + } +}; ThickLineAxisHelper.prototype.constructor = ThickLineAxisHelper; - export class CustomPivotMarker { - original_helpers: THREE.LineSegments[]; - - constructor() { - this.original_helpers = Canvas.pivot_marker.children.slice() as THREE.LineSegments[]; - let [helper1, helper2] = this.original_helpers; - let helper1_new = new ThickLineAxisHelper(1); - let helper2_new = new ThickLineAxisHelper(1); - helper1_new.rotation.copy(helper1.rotation); - helper2_new.rotation.copy(helper2.rotation); - - Canvas.pivot_marker.children.empty(); - Canvas.pivot_marker.add(helper1_new, helper2_new); - } - - delete() { - Canvas.pivot_marker.children.empty(); - Canvas.pivot_marker.add(...this.original_helpers); - } + original_helpers: THREE.LineSegments[]; + + constructor() { + this.original_helpers = + Canvas.pivot_marker.children.slice() as THREE.LineSegments[]; + let [helper1, helper2] = this.original_helpers; + let helper1_new = new ThickLineAxisHelper(1); + let helper2_new = new ThickLineAxisHelper(1); + helper1_new.rotation.copy(helper1.rotation); + helper2_new.rotation.copy(helper2.rotation); + + Canvas.pivot_marker.children.empty(); + Canvas.pivot_marker.add(helper1_new, helper2_new); + } + + delete() { + Canvas.pivot_marker.children.empty(); + Canvas.pivot_marker.add(...this.original_helpers); + } } /** @@ -82,101 +144,108 @@ export class CustomPivotMarker { * shows at the parent group's pivot. */ export class GroupPivotIndicator { - dot: THREE.Mesh; - listener: Deletable; - cameraListener: Deletable; - setting: Setting; - - constructor() { - this.setting = new Setting('show_group_pivot_indicator', { - name: 'Show Group Pivot Indicator', - description: 'Show a dot in Edit mode indicating the rotation pivot point for animations', - category: 'preview', - type: 'toggle', - value: true - }); - - let geometry = new THREE.SphereGeometry(0.65, 12, 12); - let material = new THREE.MeshBasicMaterial({ - color: this.getAccentColor(), - transparent: true, - opacity: 0.9, - depthTest: false - }); - this.dot = new THREE.Mesh(geometry, material); - this.dot.renderOrder = 900; - this.dot.visible = false; - - Canvas.scene.add(this.dot); - - this.listener = Blockbench.on('update_selection', () => this.update()); - this.cameraListener = Blockbench.on('update_camera_position', () => this.updateScale()); - this.update(); - } - - updateScale() { - if (!this.dot.visible) return; - // @ts-expect-error - let scale = Preview.selected.calculateControlScale(this.dot.position) || 0.8; - this.dot.scale.setScalar(scale * 0.7); - } - - getAccentColor(): THREE.Color { - let cssColor = getComputedStyle(document.body).getPropertyValue('--color-accent').trim(); - return new THREE.Color(cssColor || '#3e90ff'); - } - - update() { - if (!this.setting.value) { - this.dot.visible = false; - return; - } - if (Modes.paint) { - this.dot.visible = false; - return; - } - - let group = this.getRelevantGroup(); - if (!group) { - this.dot.visible = false; - return; - } - - // Update color in case accent changed - (this.dot.material as THREE.MeshBasicMaterial).color.copy(this.getAccentColor()); - - // Get world position of group pivot - let mesh = group.mesh; - if (mesh) { - let worldPos = new THREE.Vector3(); - mesh.getWorldPosition(worldPos); - this.dot.position.copy(worldPos); - this.dot.visible = true; - this.updateScale(); - } else { - this.dot.visible = false; - } - } - - getRelevantGroup(): Group | null { - let sel = Outliner.selected[0]; - if (!sel) return null; - - if (sel instanceof Group) { - return sel; - } - if (sel.parent instanceof Group) { - return sel.parent; - } - return null; - } - - delete() { - Canvas.scene.remove(this.dot); - this.dot.geometry.dispose(); - (this.dot.material as THREE.MeshBasicMaterial).dispose(); - this.listener.delete(); - this.cameraListener.delete(); - this.setting.delete(); - } + dot: THREE.Mesh; + listener: Deletable; + cameraListener: Deletable; + setting: Setting; + + constructor() { + this.setting = new Setting("show_group_pivot_indicator", { + name: t("settings.group_pivot.name"), + description: t("settings.group_pivot.description"), + category: "preview", + type: "toggle", + value: true, + }); + + let geometry = new THREE.SphereGeometry(0.65, 12, 12); + let material = new THREE.MeshBasicMaterial({ + color: this.getAccentColor(), + transparent: true, + opacity: 0.9, + depthTest: false, + }); + this.dot = new THREE.Mesh(geometry, material); + this.dot.renderOrder = 900; + this.dot.visible = false; + + Canvas.scene.add(this.dot); + + this.listener = Blockbench.on("update_selection", () => this.update()); + this.cameraListener = Blockbench.on("update_camera_position", () => + this.updateScale() + ); + this.update(); + } + + updateScale() { + if (!this.dot.visible) return; + // @ts-expect-error + let scale = + Preview.selected.calculateControlScale(this.dot.position) || 0.8; + this.dot.scale.setScalar(scale * 0.7); + } + + getAccentColor(): THREE.Color { + let cssColor = getComputedStyle(document.body) + .getPropertyValue("--color-accent") + .trim(); + return new THREE.Color(cssColor || "#3e90ff"); + } + + update() { + if (!this.setting.value) { + this.dot.visible = false; + return; + } + if (Modes.paint) { + this.dot.visible = false; + return; + } + + let group = this.getRelevantGroup(); + if (!group) { + this.dot.visible = false; + return; + } + + // Update color in case accent changed + (this.dot.material as THREE.MeshBasicMaterial).color.copy( + this.getAccentColor() + ); + + // Get world position of group pivot + let mesh = group.mesh; + if (mesh) { + let worldPos = new THREE.Vector3(); + mesh.getWorldPosition(worldPos); + this.dot.position.copy(worldPos); + this.dot.visible = true; + this.updateScale(); + } else { + this.dot.visible = false; + } + } + + getRelevantGroup(): Group | null { + let sel = Outliner.selected[0]; + if (!sel) return null; + + if (sel instanceof Group) { + return sel; + } + if (sel.parent instanceof Group) { + return sel.parent; + } + return null; + } + + delete() { + Canvas.scene.remove(this.dot); + this.dot.geometry.dispose(); + (this.dot.material as THREE.MeshBasicMaterial).dispose(); + this.listener.delete(); + this.cameraListener.delete(); + this.setting.delete(); + } } diff --git a/src/plugin.ts b/src/plugin.ts index c09fcf0..e3743ee 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -17,6 +17,7 @@ import { setupTextureHandling, updateUVSize } from "./texture"; import { setupNameOverlap } from "./name_overlap"; import { setupUVOutline } from "./uv_outline"; import { setupTempFixes } from './temp_fixes' +import { t } from "./i18n"; import { setupPreviewScenes } from "./preview_scenes"; BBPlugin.register('hytale_plugin', { @@ -24,7 +25,7 @@ BBPlugin.register('hytale_plugin', { author: 'JannisX11, Kanno', icon: 'icon.png', version: Package.version, - description: 'Create models and animations for Hytale', + description: t('plugin.description'), tags: ['Hytale'], variant: 'both', min_version: '5.0.5', diff --git a/src/texture.ts b/src/texture.ts index 4ae4e7b..9730921 100644 --- a/src/texture.ts +++ b/src/texture.ts @@ -4,6 +4,7 @@ import { AttachmentCollection } from "./attachments"; import { track } from "./cleanup"; import { isHytaleFormat } from "./formats"; +import { t } from "./i18n"; export function updateUVSize(texture: Texture) { let size = [texture.width, texture.display_height]; @@ -17,13 +18,13 @@ export function updateUVSize(texture: Texture) { export function setupTextureHandling() { - let setting = new Setting('preview_selected_texture', { - name: 'Preview Selected Texture', - description: 'When selecting a texture in a Hytale format, preview the texture on the model instantly', - category: 'preview', - type: 'toggle', - value: true - }) + let setting = new Setting("preview_selected_texture", { + name: t("settings.preview_texture.name"), + description: t("settings.preview_texture.description"), + category: "preview", + type: "toggle", + value: true, + }); track(setting); // Auto-set selected texture: for grouped textures update the collection, otherwise set as default diff --git a/src/translations/en.json b/src/translations/en.json new file mode 100644 index 0000000..1fb67c4 --- /dev/null +++ b/src/translations/en.json @@ -0,0 +1,72 @@ +{ + "plugin": { + "title": "Hytale Models", + "description": "Create models and animations for Hytale" + }, + "menu": { + "import_attachment": "Import Attachment", + "reload_all": "Reload All Attachments" + }, + "error": { + "no_textures": "No textures found for this model." + }, + "actions": { + "reload_attachments": "Reload Attachments", + "reload_all_attachments": "Reload All Attachments", + "hytale_add_quad": "Add Quad", + "cut_selection": "Cut texture selection", + "paste_texture": "Paste into texture", + "pasted_layer_name": "pasted" + }, + "textures": { + "import": "Import Textures", + "message": "No textures were found for this model. How would you like to import textures?", + "select_files": "Select Files", + "select_folder": "Select Folder", + "skip": "Skip" + }, + "directory": { + "select_texture_folder": "Select Texture Folder", + "file_not_found": "File not found", + "export_blockymodel": "Export Hytale BlockyModel", + "export_blockymodel_desc": "Export a BlockyModel file" + }, + "formats": { + "character": { + "name": "Hytale Character", + "description": "Create character and attachment models using Hytale's blockymodel format" + }, + "prop": { + "name": "Hytale Prop", + "description": "Create prop models using Hytale's blockymodel format" + }, + "page": { + "informations": "Information", + "resources": "Resources", + "info_list": "* One texture can be applied to a model at a time\n* UV sizes are linked to the size of each cube and cannot be modified, except by stretching the cube\n* Models can have a maximum of 255 nodes", + "resource_list": "* [Modeling Overview and Style Guide](https://hytale.com/news/2025/12/an-introduction-to-making-models-for-hytale)\n* [Modeling Tutorial](https://youtu.be/Q07i3wmGy0Y)" + }, + "category": "Hytale" + }, + "settings": { + "duplicate_bones": { + "name": "Duplicate Bone Names", + "description": "Allow creating duplicate groups names in Hytale formats. Multiple groups with the same name can be used to apply animations to multiple nodes at once." + }, + "photoshop_copy": { + "name": "Copy-Paste with Magenta Alpha", + "description": "Copy image selections with magenta background and remove magenta when pasting to help transfer transparency to Photoshop" + }, + "group_pivot": { + "name": "Show Group Pivot Indicator", + "description": "Show a dot in Edit mode indicating the rotation pivot point for animations" + }, + "preview_texture": { + "name": "Preview Selected Texture", + "description": "When selecting a texture in a Hytale format, preview the texture on the model instantly" + } + }, + "toggle": { + "toggle_attachment_editing": "Toggle Attachment Editing" + } +} diff --git a/src/translations/pt-BR.json b/src/translations/pt-BR.json new file mode 100644 index 0000000..e7a5c3d --- /dev/null +++ b/src/translations/pt-BR.json @@ -0,0 +1,73 @@ +{ + "plugin": { + "title": "Modelos Hytale", + "description": "Crie modelos e animações para o Hytale" + }, + "menu": { + "import_attachment": "Importar Anexo", + "reload_all": "Recarregar Todos os Anexos" + }, + "error": { + "no_textures": "Nenhuma textura encontrada para este modelo." + }, + "actions": { + "reload_attachments": "Recarregar Anexos", + "reload_all_attachments": "Recarregar Todos os Anexos", + "hytale_add_quad": "Adicionar Quad", + "cut_selection": "Recortar seleção da textura", + "paste_texture": "Colar na textura", + "pasted_layer_name": "colado" + }, + "textures": { + "import": "Importar Texturas", + "message": "Nenhuma textura foi encontrada para este modelo. Como você deseja importar as texturas?", + "select_files": "Selecionar Arquivos", + "select_folder": "Selecionar Pasta", + "skip": "Pular" + }, + "directory": { + "select_texture_folder": "Selecionar Pasta de Texturas", + "file_not_found": "Arquivo não encontrado", + "export_blockymodel": "Exportar BlockyModel do Hytale", + "export_blockymodel_desc": "Exportar um arquivo BlockyModel" + }, + "formats": { + "character": { + "name": "Personagem Hytale", + "description": "Crie modelos de personagens e anexos usando o formato blockymodel do Hytale" + }, + + "prop": { + "name": "Objeto Hytale", + "description": "Crie modelos de objetos usando o formato blockymodel do Hytale" + }, + "page": { + "informations": "Informações", + "resources": "Recursos", + "info_list": "* Apenas uma textura pode ser aplicada a um modelo por vez\n* Os tamanhos de UV são vinculados ao tamanho de cada cubo e não podem ser modificados, exceto esticando o cubo\n* Os modelos podem ter no máximo 255 nós", + "resource_list": "* [Guia de Modelagem e Estilo](https://hytale.com/news/2025/12/an-introduction-to-making-models-for-hytale)\n* [Tutorial de Modelagem](https://youtu.be/Q07i3wmGy0Y)" + }, + "category": "Hytale" + }, + "settings": { + "duplicate_bones": { + "name": "Duplicar Nomes de Ossos", + "description": "Permite criar grupos com nomes duplicados nos formatos do Hytale. Vários grupos com o mesmo nome podem ser usados para aplicar animações a vários nós ao mesmo tempo." + }, + "photoshop_copy": { + "name": "Copiar e Colar com Alpha Magenta", + "description": "Copia seleções de imagem com fundo magenta e remove o magenta ao colar, ajudando a transferir transparência para o Photoshop" + }, + "group_pivot": { + "name": "Mostrar Indicador de Pivô do Grupo", + "description": "Mostra um ponto no modo de Edição indicando o ponto de pivô de rotação para animações" + }, + "preview_texture": { + "name": "Pré-visualizar Textura Selecionada", + "description": "Ao selecionar uma textura em um formato Hytale, a textura é exibida instantaneamente no modelo" + } + }, + "toggle": { + "toggle_attachment_editing": "Alternar Edição de Anexos" + } +} diff --git a/tsconfig.json b/tsconfig.json index 6d839e9..6b42552 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,15 @@ { "compilerOptions": { - "target": "es2023", - "module": "es2022", - "moduleResolution": "node", - "allowUmdGlobalAccess": true, - "lib": ["ES2023", "DOM", "DOM.Iterable"], - "types": ["blockbench-types"], - "rootDir": "src", - "strictNullChecks": false + "target": "es2023", + "module": "es2022", + "moduleResolution": "node", + "allowUmdGlobalAccess": true, + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "types": ["blockbench-types"], + "rootDir": "src", + "strictNullChecks": false, + "resolveJsonModule": true, + // "esModuleInterop": true }, "exclude": [ "node_modules"