diff --git a/plugins.json b/plugins.json index cf989f1c..a36e0e5e 100644 --- a/plugins.json +++ b/plugins.json @@ -628,10 +628,14 @@ "display_anim_preview": { "title": "Java Display Animator", "author": "rieyi", - "description": "Preview frame-baked Minecraft Java item animations per display context and export complete resource packs and animation-driving datapacks.", + "description": "Preview frame-baked Minecraft Java item animations per display context and export multiple animations as complete resource packs and animation-driving datapacks.", "icon": "icon.png", - "tags": ["Minecraft: Java Edition", "Animation", "Exporter"], - "version": "1.0.0", + "tags": [ + "Minecraft: Java Edition", + "Animation", + "Exporter" + ], + "version": "1.1.2", "min_version": "5.1.5", "variant": "desktop", "creation_date": "2026-08-07", @@ -640,7 +644,9 @@ "bug_tracker": "https://github.com/rieyi/display-anim-preview/issues", "await_loading": true, "contributes": { - "formats": ["display_animation_sequence"] + "formats": [ + "display_animation_sequence" + ] } }, "duplicate_renamer": { diff --git a/plugins/display_anim_preview/about.md b/plugins/display_anim_preview/about.md index 2b6041fe..ba06364a 100644 --- a/plugins/display_anim_preview/about.md +++ b/plugins/display_anim_preview/about.md @@ -1,25 +1,44 @@ # Java Display Animator -Preview and export frame-baked Minecraft Java item animations directly from Blockbench. +Create, preview, and export frame-baked Minecraft Java item animations in Blockbench Desktop. Choose which display contexts animate, then generate a resource pack and its animation-driving datapack together. -## Features +## Plugin feature management -- Uses Blockbench's official timeline for animation preview. -- Enables or disables animation independently for first person, third person, GUI, ground, head, - and item-frame display contexts. -- Bakes skeletal animation at 20 FPS and deduplicates identical model frames. -- Checks model bounds, untextured faces, missing texture references, and particle textures. -- Generates a complete resource pack with `display_context` and `custom_model_data` routing. -- Generates a datapack that drives the exported animation. +Use **Tools → Java Display Animator → Java Display Animator Project Settings** to manage the project in one paged window. Its six pages are **General**, **Player Arms**, **Animations**, **Pack Files**, **Datapack**, and **Developer API**. Configure the item and project names, animation selection and default animation, 1–20 FPS sampling, output locations, datapack settings, and command examples. -## Usage +Changes update the current project's settings immediately. Save the `.bbmodel` to retain them on disk; closing the settings window does not save or overwrite the model automatically. Internal object settings remain serialized but are hidden from Blockbench's native project form. -1. Create a **Java Display Animation** project and animate its groups. -2. Configure transforms in Blockbench's Display mode. -3. Open the Command Palette and run **Open Display Animation Preview**. -4. Choose which display contexts should animate. -5. Run **Export Resource Pack and Datapack** from the Command Palette. -6. Install the generated packs in Minecraft Java 26.2 and use the commands shown after export. +The Tools submenu also groups the display preview, first-person preview, bounds checks, and export. Pack export is additionally available under **File → Export** and in command search. -The interface uses English by default and includes Simplified Chinese translations through -Blockbench's translation API. +## First-person animation authoring + +In **Animate** mode, the dockable **First-person Animation Preview** shows the item from the left- or right-hand first-person view while the main viewport remains available for editing and bone selection. It follows the official timeline during playback and scrubbing. Changing the preview hand does not move the main editor camera or create another timeline. + +Open or reopen it using the timeline eye button, **Animation → Open First-person Preview**, or the plugin's Tools submenu. New **Java Display Animation** projects automatically open the separate **Display Animation Preview** controls, including when the panel was previously folded. Animation switches are saved independently for each display context. + +## Player-skin arms + +Enable optional arms in **Project Settings → Player Arms** to create editable left/right arm bindings. Animate their position and rotation alongside the item. First-person exports include the arms, and the generated give command supplies the player's skin profile in Minecraft. Other display contexts retain the item without added arms. Editor arm placeholders show the rig and poses; verify the final skin appearance in Minecraft. + +**Player-skin arms currently require Minecraft Java 1.21.11 or newer.** Keep shader packs disabled; arm scale keyframes are unsupported. The version notice is visible before and after enabling arms. + +## Export and bounds checks + +- Select multiple animation tracks and a default animation, with sampling from 1 to 20 FPS. +- Preview independent animation settings for first-person, third-person, GUI, ground, head, item-frame, and other supported display contexts. +- Run cancellable quick mathematical or exact isolated bounds checks. Closing the mode chooser or pressing Escape cancels without starting a scan. +- Bake in an isolated project and deduplicate identical model frames across animations. +- Create resource packs and datapacks, or transactionally insert project-owned files into existing unpacked packs. +- Use short per-animation commands and a dynamic macro API; each generated item keeps its own playback state. + +## Quick start and compatibility + +1. Create a **Java Display Animation** project, build the item, and animate its groups. +2. Configure transforms in **Display** mode and choose which display contexts animate. +3. Open **Project Settings** to select animations, FPS, optional arms, and output locations. +4. Choose **File → Export → Export Resource Pack and Datapack**, review warnings, and install the generated packs. +5. Run the commands shown after export and verify the result in Minecraft. + +Requires **Blockbench Desktop 5.1.5+**. The current exporter targets **Minecraft Java 26.2**. The arm feature's minimum version does not imply that generated packs load unchanged on every version above 1.21.11. + +[Source and complete guides](https://github.com/rieyi/display-anim-preview) · [Report an issue](https://github.com/rieyi/display-anim-preview/issues) diff --git a/plugins/display_anim_preview/changelog.json b/plugins/display_anim_preview/changelog.json index 53651c19..2d0233f8 100644 --- a/plugins/display_anim_preview/changelog.json +++ b/plugins/display_anim_preview/changelog.json @@ -17,5 +17,22 @@ ] } ] + }, + "1.1.2": { + "title": "1.1.2", + "date": "2026-10-04", + "author": "rieyi", + "categories": [ + { + "title": "Features since 1.0.0", + "list": [ + "Add a feature management panel.", + "Provide a first-person preview window in Animate mode to preview the first-person result while editing animations.", + "Multiple animation tracks and configurable project sampling at 1-20 FPS.", + "Optional animated player-skin arms in first-person views; require Minecraft Java 1.21.11+ and are incompatible with shader packs.", + "Quick/exact isolated bounds checks and insertion into existing packs." + ] + } + ] } } diff --git a/plugins/display_anim_preview/display_anim_preview.js b/plugins/display_anim_preview/display_anim_preview.js index 46ccc14b..dec1c29a 100644 --- a/plugins/display_anim_preview/display_anim_preview.js +++ b/plugins/display_anim_preview/display_anim_preview.js @@ -2,12 +2,18 @@ (() => { // src/i18n.ts var EN = { + "dap.menu.name": "Java Display Animator", + "dap.fp.open": "Open First-person Preview", + "dap.fp.open_desc": "Open the live animation preview with native display framing", + "dap.fp.name": "First-person Animation Preview", + "dap.fp.side": "First-person Display Context", "dap.format.name": "Java Display Animation", "dap.format.description": "Create frame-baked keyframe animation projects for Minecraft Java Edition", "dap.property.name": "Display Context Animation", "dap.property.description": "Choose whether each item display context plays the frame animation", "dap.panel.name": "Display Animation Preview", "dap.panel.slot": "Display Context", + "dap.panel.animation": "Preview Animation", "dap.panel.animate": "Animate Current Display Context", "dap.panel.animate_hint": "Enabled contexts follow custom_model_data frames; disabled contexts stay on frame 0.", "dap.panel.play": "Play / Pause", @@ -15,22 +21,112 @@ "dap.panel.loop": "Loop Playback", "dap.panel.low_fps": "Low FPS", "dap.panel.low_fps_hint": "Preview at the animation snapping rate to simulate non-interpolated in-game playback", + "dap.panel.preview_fps": "FPS", + "dap.panel.preview_fps_hint": "Project animation rate from 1 to Minecraft's 20 FPS limit. This controls preview, exported model frames, and datapack playback pacing.", "dap.action.open": "Open Display Animation Preview", "dap.action.open_desc": "Preview animation by display context with independent playback controls", "dap.action.bounds": "Check Animation Model Bounds", "dap.action.bounds_desc": "List baked frames outside Minecraft's -16 to 32 model limits", "dap.action.export": "Export Resource Pack and Datapack", - "dap.action.export_desc": "Bake the animation and generate a complete resource pack and animation-driving datapack", + "dap.action.export_desc": "Select and bake multiple animations, then generate a complete resource pack and animation-driving datapack", + "dap.action.settings": "Java Display Animator Project Settings", + "dap.action.settings_desc": "Edit animations, pack folders, item mapping, and datapack runtime settings", + "dap.settings.title": "Java Display Animator Project Settings", + "dap.settings.no_project": "Open a project before editing export settings", + "dap.settings.page.general": "General", + "dap.settings.page.hands": "Player Arms", + "dap.settings.page.animations": "Animations", + "dap.settings.page.files": "Pack Files", + "dap.settings.page.datapack": "Datapack", + "dap.settings.page.api": "Developer API", + "dap.settings.general_desc": "Project identity and generated item settings.", + "dap.settings.hands_desc": "Create or delete the position-placeholder arm rig for first-person player-skin hands.", + "dap.settings.animations_desc": "Choose the complete exported animation group and its default animation.", + "dap.settings.files_desc": "Create new packs or point directly at existing unpacked pack folders. Resource-pack and datapack locations are independent.", + "dap.settings.datapack_desc": "Advanced runtime names used by the generated animation driver.", + "dap.settings.api_desc": "Quick reference generated from the current project settings.", + "dap.settings.copy": "Copy", + "dap.settings.copied": "Copied to clipboard", + "dap.settings.copy_failed": "Could not access the clipboard", + "dap.settings.clipboard_permission": "Java Display Animator needs clipboard access to copy this datapack API reference.", + "dap.settings.api_item": "Item model ID", + "dap.settings.api_item_note": "Use this ID in the minecraft:item_model component.", + "dap.settings.api_common": "Common functions", + "dap.settings.api_common_note": "Give the animated item or stop the current animation.", + "dap.settings.api_short": "Short functions for testing and fixed animation calls", + "dap.settings.api_short_note": "Replace with an exported animation key.", + "dap.settings.api_macro": "Macro functions for dynamic datapack calls", + "dap.settings.api_macro_note": "Use these when the animation name or mode is supplied dynamically.", + "dap.settings.api_context": "Run as a specified player", + "dap.settings.api_context_note": "Public functions operate on executor @s; use execute as for another player.", + "dap.settings.api_animation_placeholder": "animation", + "dap.settings.api_player_placeholder": "player", + "dap.settings.api_no_animation": "No animation is currently selected for export.", + "dap.settings.api_reference": "Datapack reference: public functions are under data/jsb/function/{project}/. Functions operate on executor @s. Each unstackable item stores its own playback state in custom_data.jsb; custom_model_data.strings[0] stores the animation key and floats[0] stores the local frame.", + "dap.settings.shared_root": "Shared Create Root", + "dap.settings.resource_folder": "Resource-Pack Folder / Parent", + "dap.settings.datapack_folder": "Datapack Folder / Parent", + "dap.settings.folder_empty": "Choose a folder or leave empty to ask during export", + "dap.settings.browse": "Browse...", + "dap.settings.saved_hint": "Changes are stored in this Blockbench project immediately; save the project to keep them.", + "dap.settings.close": "Close", + "dap.settings.valid_identifier": "Valid Minecraft identifier", + "dap.settings.invalid_identifier": "Use lowercase a-z, 0-9, underscore, hyphen, or period; reserved path names are not allowed.", + "dap.settings.valid_runtime_name": "Valid runtime name", + "dap.settings.invalid_runtime_name": "Use 1-16 characters: letters, numbers, period, underscore, plus, or hyphen.", + "dap.settings.valid_playing_tag": "Valid playback tag", + "dap.settings.invalid_playing_tag": "Use letters, numbers, period, underscore, plus, or hyphen; spaces are not allowed.", + "dap.settings.folder_optional": "No folder configured; Blockbench will ask during export.", + "dap.settings.folder_missing": "The selected path does not exist.", + "dap.settings.folder_unreadable": "This path is not a readable folder or permission has not been granted.", + "dap.settings.folder_valid_parent": "Valid output parent folder", + "dap.settings.folder_no_pack_meta": "Insert mode requires pack.mcmeta in this folder.", + "dap.settings.folder_invalid_pack_meta": "pack.mcmeta is not valid JSON with a pack object.", + "dap.settings.folder_valid_pack": "Valid existing unpacked pack", + "dap.settings.folder_no_assets": "pack.mcmeta is valid; the missing assets folder will be created during insertion.", + "dap.settings.folder_no_data": "pack.mcmeta is valid; the missing data folder will be created during insertion.", + "dap.settings.folder_valid_shared": "Both named packs exist under resource-packs/ and datapacks/.", + "dap.settings.folder_invalid_shared": "Expected resource-packs//pack.mcmeta and datapacks//pack.mcmeta under this root.", + "dap.settings.create_desc": "Create named pack folders inside the selected output location.", + "dap.settings.insert_desc": "Read and update existing unpacked packs while preserving unrelated files.", + "dap.settings.shared_desc": "Keep resource-packs/ and datapacks/ under one map output root.", + "dap.settings.separate_desc": "Configure resource-pack and datapack locations independently.", + "dap.settings.resource_only_desc": "Generate or update only models, item definitions, and textures.", + "dap.settings.datapack_only_desc": "Generate or update only the animation driver; requires a matching resource pack.", + "dap.settings.animations_valid": "{count} animations selected; every generated key is valid and unique.", + "dap.settings.animations_empty": "Select at least one animation.", + "dap.settings.animations_invalid": "Resolve invalid or duplicate animation keys: {details}", + "dap.export.open_settings_hint": "Open Java Display Animator Project Settings to correct this value, then export again.", + "dap.export.property.name": "Animation Export Selection", + "dap.export.property.description": "Remembers export settings only after files are written successfully", "dap.export.title": "Export Resource Pack and Datapack", + "dap.export.select_title": "Select Animations to Export", + "dap.export.select_help": "Choose the animations to bake. Each Minecraft key is generated from its Blockbench animation name and is used by the play, loop, and frame commands.", + "dap.export.selection": "Selection", + "dap.export.select_all": "Select All", + "dap.export.select_none": "Select None", + "dap.export.select_required": "Select at least one animation before continuing.", + "dap.export.animation_row": "Key: {key} \xB7 Duration: {duration}s \xB7 Source: {source_fps} FPS \xB7 Output: {game_frames} frames at {game_fps} FPS", + "dap.export.invalid_key": "invalid key", + "dap.export.key_conflict_title": "Animation Names Cannot Be Exported", + "dap.export.key_conflict_message": "Rename the listed animations so every generated Minecraft key is valid and unique:\n\n{details}", + "dap.export.key_conflict_entry": 'Key "{key}": {animations}', + "dap.export.animation_summary": "{animation} \u2192 {key} \xB7 {source_fps} FPS \u2192 {game_frames} game frames", + "dap.export.default_animation": "Default Animation", + "dap.export.default_missing": "The selected default animation is no longer available.", "dap.export.output": "Export Contents", + "dap.export.write_mode": "Write Mode", + "dap.export.write_mode.create": "Create New Pack", + "dap.export.write_mode.insert": "Insert into Existing Pack", "dap.export.pack_name": "Pack Name", - "dap.export.asset_namespace": "Asset Namespace", - "dap.export.item_model": "Item Model Name", + "dap.export.project_name": "Project Name", + "dap.export.project_name_hint": "Unique module ID under the fixed jsb namespace. Used in paths and commands.", "dap.export.base_item": "Mapped Item", "dap.export.display_name": "Item Display Name", - "dap.export.data_namespace": "Datapack Namespace", "dap.export.frame_objective": "Frame Scoreboard", "dap.export.mode_objective": "Mode Scoreboard", + "dap.export.max_frame_objective": "Maximum Frame Scoreboard", + "dap.export.objective_conflict": "Frame, mode, and maximum-frame scoreboards must use three different names.", "dap.export.playing_tag": "Playback Tag", "dap.export.mode.both_default": "Resource Pack + Datapack (Shared Root)", "dap.export.mode.both_separate": "Resource Pack + Datapack (Separate Parents)", @@ -40,6 +136,10 @@ "dap.export.cancel_export": "Cancel Export", "dap.export.export_anyway": "Export Anyway", "dap.export.warnings_title": "Review Export Warnings", + "dap.export.open_bounds_check": "Open Bounds Checker", + "dap.export.problem_frames": "Problem frames", + "dap.export.locate_frame": "Frame {frame}", + "dap.export.located_frame": "Located {animation}, frame {frame}", "dap.export.cancelled": "Export cancelled; no files were generated", "dap.export.preparing_files": "Warnings confirmed; preparing pack files\u2026", "dap.export.overwrite": "Overwrite and Export", @@ -48,21 +148,78 @@ "dap.export.pick_shared": "Select export root (creates resource-packs/{pack}/ and datapacks/{pack}/)", "dap.export.pick_resource": "Select resource-pack parent folder (creates {pack}/)", "dap.export.pick_datapack": "Select datapack parent folder (creates {pack}/)", + "dap.export.pick_existing_resource": "Select an existing unpacked resource-pack folder", + "dap.export.pick_existing_datapack": "Select an existing unpacked datapack folder", "dap.export.baking": "Baking {frames} frames at {fps} FPS\u2026", + "dap.export.baking_animation": "Baking {animation}: {frames} frames at {fps} FPS\u2026", "dap.export.failed": "Export Failed", "dap.export.no_frames": "No frames were baked. Make sure the animation contains keyframes.", + "dap.export.no_frames_for_animation": "No frames were baked for {animation}. Make sure the animation contains keyframes.", + "dap.export.no_animated_context_title": "No Display Context Plays Animation", + "dap.export.no_animated_context_message": "Every display context has animation disabled. The resource pack will contain only frame 0 of the default animation {default_animation}; the other selected animations will not have a visible playback route.", + "dap.export.datapack_only_title": "Datapack Requires a Matching Resource Pack", + "dap.export.datapack_only_message": "This datapack uses the selected animation keys and frame counts. Pair it only with a resource pack exported with the same animation mapping.", "dap.export.resampled_title": "Resampled to the Game Frame Rate", - "dap.export.resampled_message": "The animation snapping rate is {source_fps} FPS, while Minecraft displays at most {game_fps} frames per second.\n\nThis export will use {frames} frames at {game_fps} FPS to preserve its duration.", + "dap.export.resampled_message": "The animation snapping rate is {source_fps} FPS, while the project export rate is {game_fps} FPS.\n\nThis export will use {frames} frames at {game_fps} FPS to preserve its duration.", "dap.export.texture_mismatch_title": "Texture Resolution Mismatch", "dap.export.texture_mismatch": "Project UV resolution is {project_width}\xD7{project_height}, but these textures use different dimensions:\n\n{textures}\n\nExported UVs may be offset. Update File \u2192 Project Settings before exporting.", "dap.export.bounds_title": "Some Frames Exceed Model Bounds", "dap.export.resource_description": "{name} ({frames} frames @ {fps} FPS)", "dap.export.datapack_description": "Frame animation driver for {name} ({frames} frames)", + "dap.export.resource_description_multi": "{name} ({animations} animations, {frames} sampled frames @ {fps} FPS)", + "dap.export.datapack_description_multi": "Multi-animation frame driver for {name} ({animations} animations, {frames} frames)", "dap.export.complete": "Export Complete", + "dap.export.complete_heading": "Export succeeded", + "dap.export.item_model_id": "Item model ID: {id}", "dap.export.locations": "Verified and wrote {count} files.\n\n{locations}", + "dap.export.write_success": "Successfully verified and wrote {count} files", + "dap.export.output_locations": "Output locations", "dap.export.optimization": "Space optimization: sampled {sampled} frames, wrote {unique} unique models, and deduplicated {duplicates} frames.\nModel JSON: {before} \u2192 {after}.", + "dap.export.space_optimization": "Space optimization:", + "dap.export.optimization_statistics": "Sampled {sampled} frames, wrote {unique} unique models, and deduplicated {duplicates} frames.", + "dap.export.model_json_size": "Model JSON: {before} \u2192 {after}", + "dap.export.animation_report": "{animation}: {frames} sampled frames", "dap.export.omitted": "Omitted {faces} untextured faces and removed {elements} elements without visible faces.", + "dap.export.animation_keys": "Animation keys: {keys}", "dap.export.commands": "In-game commands:", + "dap.settings.developer_tips": "Developer tips", + "dap.settings.developer_tips_help": "Show in-game feedback when the datapack activates, gives the item, or starts and stops playback. Turn off for a fully silent datapack.", + "dap.settings.hand_rendering": "Render player-skin hands", + "dap.settings.hand_rendering_help": "Creates standard 4\xD74\xD712 box-UV placeholder arm cubes (left at 0,0,4, right at 12,0,4) textured with the built-in 16\xD716 default texture; Exported position and rotation follow the arm groups, including their static placement and animation. Preview dimensions are independent; left/right marker scales remain fixed. Exports a Minecraft 26.2 core entity shader, which may conflict with packs replacing the same shader.", + "dap.hand.skin_version_help": "Player-skin arms currently require Minecraft Java 1.21.11 or newer.", + "dap.hand.shader_incompatible": "Incompatible with shader packs", + "dap.hand.shader_incompatible_help": "Disable shader packs when using player-skin arms. Shader packs can bypass the arm texture conversion and display player-head textures instead.", + "dap.hand.undo_create": "Create player hand rig", + "dap.hand.undo_delete": "Delete player hand rig", + "dap.hand.delete": "Delete player hand binding\u2026", + "dap.hand.delete_title": "Delete Player Hand Binding", + "dap.hand.delete_message": "Plugin-created arm groups and their animation tracks will be removed. Adopted lefthand/righthand groups are only unbound and are not deleted.", + "dap.hand.delete_confirm": "Delete binding", + "dap.hand.scale_unsupported": "Animation {animation} contains scale keyframes on {group}. Player-hand scale animation is unsupported because the shader uses exact scale values to identify each arm.", + "dap.hand.rig_missing": 'Player-hand rendering is enabled, but the arm binding is missing. Re-enable "Render player arms" in Project Settings to recreate it.', + "dap.settings.exact_bounds_export": "Run exact bounds check before export", + "dap.settings.exact_bounds_export_help": "Validate the isolated export bake and remember exact per-animation results. Disable this to bake without range warnings or detection status.", + "dap.export.summary": "Export summary", + "dap.export.developer_info": "Developer information", + "dap.export.developer_tips_status": "Developer tips: {status}", + "dap.export.hand_rendering_status": "Player-skin hands: {status}", + "dap.export.developer_tips_enabled": "Enabled", + "dap.export.developer_tips_disabled": "Disabled", + "dap.export.hand_rendering_warning_title": "Player-Skin Hands Use a Core Shader", + "dap.export.hand_rendering_warning_message": "This export replaces Minecraft 26.2's entity core shader and can conflict with shader/resource packs that replace the same file. Left and right arm poses are sampled per frame and exported to both first-person display contexts.", + "dap.export.hand_rendering_resource_only_title": "A Profile-Bearing Player Head Is Required", + "dap.export.hand_rendering_resource_only_message": "Resource-pack-only export cannot assign the viewer's skin. Use a minecraft:player_head carrying minecraft:profile and the generated item_model, or export the matching datapack and run its give function.", + "dap.export.preflight_title": "Review File Changes", + "dap.export.preflight_message": "The JSB project module will make these changes:\n\n{summary}", + "dap.export.preflight_added": "New project files", + "dap.export.preflight_updated": "Updated project files", + "dap.export.preflight_removed": "Removed stale project files", + "dap.export.preflight_merged": "Merged load/tick tags", + "dap.export.confirm_write": "Write Changes", + "dap.export.conflict_title": "Path Conflict", + "dap.export.conflict_message": "The following paths already exist but are not owned by this JSB project. No files were written:\n\n{paths}", + "dap.export.invalid_identifier_title": "Invalid Pack or Project Name", + "dap.export.invalid_identifier_message": "Use a safe lowercase Minecraft-style ID containing only a-z, 0-9, underscore, hyphen, or period. The project name cannot be _generated, . or ..", "dap.export.write_error": "An error occurred while writing files:\n{error}", "dap.export.target_exists": "Target Pack Already Exists", "dap.export.target_exists_message": "Continuing overwrites matching files but does not delete other files:\n\n{summary}", @@ -71,16 +228,59 @@ "dap.export.busy": "The previous export is still running", "dap.export.no_animation_title": "No Animation to Export", "dap.export.no_animation_message": "Create an animation with keyframes before exporting.", - "dap.export.fps_exact": "Exports {frames} frames at 20 FPS, one frame per game tick.", + "dap.export.fps_exact": "Exports {frames} frames at the project rate of {game_fps} FPS.", "dap.export.fps_resample": "The current snapping rate is {source_fps} FPS ({source_frames} source samples). Export resamples to {game_fps} FPS ({game_frames} frames) while preserving duration.", - "dap.export.folder_help": "On macOS, Open selects a parent folder; the plugin then creates / inside it.
The default mode creates resource-packs// and datapacks//.", + "dap.export.folder_help": "Create mode selects a parent folder and creates / inside it. Insert mode selects an existing unpacked pack folder with pack.mcmeta. Paths are not stored in the project.", "dap.bounds.no_animation": "No animation is available to check", "dap.bounds.passed_title": "Model Bounds Check Passed", + "dap.bounds.passed_heading": "No out-of-range frames found", "dap.bounds.passed_message": "Checked {frames} frames at {fps} FPS. All coordinates are within -16 to 32.", + "dap.bounds.passed_animation": "Animation checked: {animation}", + "dap.bounds.passed_animations": "Animations checked: {animations}", + "dap.bounds.panel_title": "Model Bounds Check", + "dap.bounds.panel_checked_animations": "Checked {animations} animations", + "dap.bounds.panel_failed": "Found problems in {frames} frames", + "dap.bounds.panel_passed": "All {frames} frames passed", + "dap.bounds.panel_all_passed": "All animations passed", + "dap.bounds.animation_failed": "{frames} problem frames", + "dap.bounds.animation_passed": "Passed \xB7 {frames} frames", + "dap.bounds.panel_hint": "Select a frame to locate it, outline affected parts, and open the nearest influencing keyframe. After editing, run the check again.", + "dap.bounds.recheck": "Check Again", + "dap.bounds.mode.quick": "Quick Math Check", + "dap.bounds.mode.exact": "Exact Isolated Check", + "dap.bounds.mode.export_bake": "Isolated Export Bake", + "dap.bounds.choose_title": "Choose Bounds Check Mode", + "dap.bounds.choose_message": "Quick Check uses non-destructive matrix math. Exact Check creates a disposable in-memory project and verifies every frame with Blockbench's Java codec.", + "dap.bounds.choose_cancel": "Cancel", + "dap.bounds.progress_preparing": "Preparing range check\u2026", + "dap.bounds.progress_title": "{mode}: {animation}", + "dap.bounds.progress_frames": "Animation frame {frame}/{frames} \xB7 Total {completed}/{total}", + "dap.bounds.progress_status": "{mode}: {animation} {frame}/{frames}", + "dap.bounds.export_bake_complete": "Isolated export bake complete", + "dap.bounds.export_bake_complete_detail": "Generated {frames} frames from {animations} animations. You can continue the export workflow.", + "dap.bounds.cancel": "Cancel Check", + "dap.bounds.cancelling": "Cancelling\u2026", + "dap.bounds.cancelled": "Bounds check cancelled; no partial result was cached", + "dap.bounds.cache_reused": "Reused valid results for {animations} animations", + "dap.bounds.cache_all_reused": "The model has not changed; all valid range results were reused", + "dap.bounds.status.unchecked": "Not checked", + "dap.bounds.status.stale": "Changed after check", + "dap.bounds.status.quick_passed": "Quick passed", + "dap.bounds.status.quick_failed": "Quick warning", + "dap.bounds.status.exact_passed": "Exact passed", + "dap.bounds.status.exact_failed": "Exact warning", + "dap.bounds.checking": "Checking {animation} at the project animation rate\u2026", + "dap.bounds.checking_animations": "Checking {animations} animations at the project animation rate\u2026", + "dap.bounds.check_in_progress": "A model bounds check is already running", + "dap.bounds.located": "Located {animation}, frame {frame}", + "dap.bounds.edit_in_progress_title": "Finish the Current Edit First", + "dap.bounds.edit_in_progress_message": "Blockbench still has an unfinished model or keyframe edit. Press Enter or click the viewport to commit it, then run the bounds check again. No check was started.", + "dap.bounds.check_failed_title": "Bounds Check Failed", + "dap.bake.active_edit": "Bounds checking and export cannot bake frames while another Blockbench edit transaction is active.", "dap.bounds.frame": "Frame {frame}: {parts}, {field}.{axis} = {value}", "dap.bounds.parts_many": "{names} and {count} parts", "dap.bounds.summary": "Parts exceed Minecraft model bounds in {frames} frames (every axis must remain between -16 and 32).", - "dap.bounds.guidance": "Out-of-range frames may render offset or disappear. Use the datapack next/prev functions to inspect these frames, then reduce the affected motion in Blockbench:", + "dap.bounds.guidance": "Out-of-range frames may render offset or disappear. Use the datapack frame command to inspect these frames, then reduce the affected motion in Blockbench:", "dap.bounds.omitted": "\u2026and {count} more out-of-range frames.", "dap.rollback.title": "Incomplete Rollback \u2014 Do Not Save", "dap.rollback.message": "The keyframe count changed after baking: {before} before, {after} now ({lost} missing).\n\nUndo immediately with Ctrl+Z, or close without saving and reopen the file.", @@ -91,17 +291,26 @@ "dap.error.file_verify": "Written file failed content verification: {path}", "dap.error.manifest_not_written": "Export manifest was not written: {path}", "dap.error.manifest_verify": "Written export manifest failed verification: {path}", + "dap.error.duplicate_target": "Two generated packs resolve to the same target folder: {path}", + "dap.error.unsafe_path": "Generated file path is unsafe and was rejected: {path}", "dap.error.no_models": "No models are available for the item definition", "dap.error.external_texture": 'Texture "{label}" still references external atlas "{value}". Minecraft 26.2 item models cannot mix item and block atlases.', "dap.error.texture_not_generated": "Model references a texture that was not generated: {value}", - "dap.datapack.loaded": "Datapack loaded. Run /function {namespace}:give to get the animated item.", - "dap.datapack.item_given": "Animated item given. Use play_loop, play_once, or next/prev while holding it.", + "dap.error.manifest_invalid": "The JSB project manifest is invalid: {path}", + "dap.error.invalid_pack": "Insert mode requires an unpacked pack folder with a valid pack.mcmeta: {path}", + "dap.error.shared_tag_invalid": "The existing function tag is invalid and was not modified: {path}", + "dap.error.path_conflicts": "Existing files are not owned by this JSB project and cannot be overwritten:\n{paths}", + "dap.error.rollback_partial": "The export failed and the automatic rollback could not restore some files; check these paths manually:\n{paths}", + "dap.datapack.loaded": "Datapack loaded. Run /function {namespace}/give to get the animated item.", + "dap.datapack.item_given": "Animated item given ({animation}, frame 0). While holding it, call /function {namespace}/play/, /function {namespace}/loop/, or /function {namespace}/frame/ {frame:12}; use /function {namespace}/stop to stop.", "dap.datapack.hold_item": "Hold the animated item in your main hand first.", - "dap.datapack.loop_started": "Loop playback started (20 FPS, frames 0-{last_frame}).", - "dap.datapack.once_started": "Single playback started and will stop on frame {last_frame}.", + "dap.datapack.loop_started": "Loop playback started for {animation} ({fps} FPS, frames 0-{last_frame}).", + "dap.datapack.once_started": "Single playback started for {animation}; after frame {last_frame}, it will reset to the default animation's frame 0.", "dap.datapack.current_frame": "Current frame: ", "dap.datapack.reset": "Reset to frame 0.", - "dap.datapack.stopped": "Playback stopped on the current frame.", + "dap.datapack.stopped": "Playback stopped and reset to the default animation's frame 0.", + "dap.datapack.invalid_animation": "Unknown animation key: {animation}", + "dap.datapack.invalid_mode": "Invalid playback mode: {mode}. Use once or loop.", "dap.slot.thirdperson_righthand": "Third Person - Right Hand", "dap.slot.thirdperson_lefthand": "Third Person - Left Hand", "dap.slot.firstperson_righthand": "First Person - Right Hand", @@ -114,35 +323,131 @@ "dap.slot.on_shelf": "On Shelf" }; var ZH = { + "dap.menu.name": "Java \u9010\u5E27\u663E\u793A\u52A8\u753B", "dap.format.name": "Java \u9010\u5E27\u663E\u793A\u52A8\u753B", "dap.format.description": "\u4E3A Minecraft Java \u7248\u521B\u5EFA\u9010\u5E27\u51E0\u4F55\u70D8\u7119\u5173\u952E\u5E27\u52A8\u753B\u5DE5\u7A0B", "dap.property.name": "\u663E\u793A\u4F4D\u7F6E\u52A8\u753B", "dap.property.description": "\u5206\u522B\u51B3\u5B9A\u6BCF\u4E2A\u7269\u54C1\u663E\u793A\u4F4D\u7F6E\u662F\u5426\u64AD\u653E\u9010\u5E27\u52A8\u753B", + "dap.fp.open": "\u5F00\u542F\u7B2C\u4E00\u4EBA\u79F0\u9884\u89C8", + "dap.fp.open_desc": "\u6253\u5F00\u4E0E\u539F\u751F\u663E\u793A\u753B\u5E45\u4E00\u81F4\u7684\u7B2C\u4E00\u4EBA\u79F0\u52A8\u753B\u5B9E\u65F6\u9884\u89C8\u9762\u677F", + "dap.fp.name": "\u7B2C\u4E00\u4EBA\u79F0\u52A8\u753B\u9884\u89C8", + "dap.fp.side": "\u7B2C\u4E00\u4EBA\u79F0\u663E\u793A\u4F4D\u7F6E", "dap.panel.name": "\u663E\u793A\u4F4D\u7F6E\u52A8\u753B\u9884\u89C8", "dap.panel.slot": "\u663E\u793A\u4F4D\u7F6E", + "dap.panel.animation": "\u9884\u89C8\u52A8\u753B", "dap.panel.animate": "\u5F53\u524D\u663E\u793A\u4F4D\u7F6E\u64AD\u653E\u52A8\u753B", "dap.panel.animate_hint": "\u542F\u7528\u540E\u968F custom_model_data \u64AD\u653E\u9010\u5E27\u52A8\u753B\uFF0C\u5173\u95ED\u65F6\u56FA\u5B9A\u4F7F\u7528\u7B2C 0 \u5E27\u3002", "dap.panel.play": "\u64AD\u653E / \u6682\u505C", "dap.panel.play_disabled": "\u5F53\u524D\u663E\u793A\u4F4D\u7F6E\u5DF2\u5173\u95ED\u52A8\u753B\u3002", "dap.panel.loop": "\u5FAA\u73AF\u64AD\u653E", "dap.panel.low_fps": "\u4F4E\u5E27", - "dap.panel.low_fps_hint": "\u6309\u52A8\u753B\u5438\u9644\u5E27\u7387\u9010\u5E27\u9884\u89C8\uFF0C\u6A21\u62DF\u6E38\u620F\u5185\u65E0\u63D2\u503C\u64AD\u653E", + "dap.panel.low_fps_hint": "\u6309\u53F3\u4FA7\u624B\u52A8\u5E27\u7387\u9010\u5E27\u9884\u89C8\uFF0C\u6A21\u62DF\u6E38\u620F\u5185\u65E0\u63D2\u503C\u64AD\u653E", + "dap.panel.preview_fps": "\u9884\u89C8 FPS", + "dap.panel.preview_fps_hint": "\u53EF\u624B\u52A8\u8BBE\u7F6E 1\u201320 FPS\uFF0C\u4E0A\u9650\u4E0E Minecraft \u6BCF\u79D2 20 \u6E38\u620F\u523B\u4E00\u81F4\uFF1B\u540C\u65F6\u63A7\u5236\u9884\u89C8\u3001\u5BFC\u51FA\u6A21\u578B\u5E27\u6570\u548C\u6570\u636E\u5305\u64AD\u653E\u8282\u594F\u3002", "dap.action.open": "\u6253\u5F00\u663E\u793A\u4F4D\u7F6E\u52A8\u753B\u9884\u89C8", "dap.action.open_desc": "\u6309\u663E\u793A\u4F4D\u7F6E\u9884\u89C8\u52A8\u753B\u5E76\u63D0\u4F9B\u72EC\u7ACB\u64AD\u653E\u63A7\u4EF6", "dap.action.bounds": "\u68C0\u67E5\u52A8\u753B\u6A21\u578B\u8303\u56F4", "dap.action.bounds_desc": "\u5217\u51FA\u70D8\u7119\u540E\u8D85\u51FA Minecraft -16 \u5230 32 \u6A21\u578B\u9650\u5236\u7684\u5E27", "dap.action.export": "\u5BFC\u51FA\u8D44\u6E90\u5305\u548C\u6570\u636E\u5305", - "dap.action.export_desc": "\u70D8\u7119\u52A8\u753B\u5E76\u751F\u6210\u5B8C\u6574\u8D44\u6E90\u5305\u548C\u52A8\u753B\u9A71\u52A8\u6570\u636E\u5305", + "dap.action.export_desc": "\u9009\u62E9\u5E76\u70D8\u7119\u591A\u6BB5\u52A8\u753B\uFF0C\u7136\u540E\u751F\u6210\u5B8C\u6574\u8D44\u6E90\u5305\u548C\u52A8\u753B\u9A71\u52A8\u6570\u636E\u5305", + "dap.action.settings": "Java \u663E\u793A\u52A8\u753B\u5668\u9879\u76EE\u8BBE\u7F6E", + "dap.action.settings_desc": "\u5728\u5236\u4F5C\u8FC7\u7A0B\u4E2D\u4FEE\u6539\u52A8\u753B\u3001\u5305\u76EE\u5F55\u3001\u7269\u54C1\u6620\u5C04\u548C\u6570\u636E\u5305\u9A71\u52A8\u8BBE\u7F6E", + "dap.settings.title": "Java \u663E\u793A\u52A8\u753B\u5668\u9879\u76EE\u8BBE\u7F6E", + "dap.settings.no_project": "\u8BF7\u5148\u6253\u5F00\u4E00\u4E2A\u9879\u76EE", + "dap.settings.page.general": "\u5E38\u89C4", + "dap.settings.page.hands": "\u73A9\u5BB6\u624B\u81C2", + "dap.settings.page.animations": "\u52A8\u753B", + "dap.settings.page.files": "\u5305\u6587\u4EF6", + "dap.settings.page.datapack": "\u6570\u636E\u5305", + "dap.settings.page.api": "\u5F00\u53D1\u63A5\u53E3", + "dap.settings.general_desc": "\u8BBE\u7F6E\u9879\u76EE\u6807\u8BC6\u548C\u751F\u6210\u7269\u54C1\u3002", + "dap.settings.hands_desc": "\u521B\u5EFA\u6216\u5220\u9664\u7B2C\u4E00\u4EBA\u79F0\u73A9\u5BB6\u624B\u81C2\u7684\u4F4D\u7F6E\u5360\u4F4D\u9AA8\u67B6\u3002", + "dap.settings.animations_desc": "\u9009\u62E9\u8981\u5BFC\u51FA\u7684\u5B8C\u6574\u52A8\u753B\u7EC4\u548C\u9ED8\u8BA4\u52A8\u753B\u3002", + "dap.settings.files_desc": "\u521B\u5EFA\u65B0\u5305\uFF0C\u6216\u76F4\u63A5\u6307\u5411\u5DF2\u89E3\u538B\u7684\u73B0\u6709\u5305\u76EE\u5F55\u3002\u8D44\u6E90\u5305\u4E0E\u6570\u636E\u5305\u5206\u522B\u914D\u7F6E\u3002", + "dap.settings.datapack_desc": "\u914D\u7F6E\u52A8\u753B\u9A71\u52A8\u4F7F\u7528\u7684\u9AD8\u7EA7\u8FD0\u884C\u65F6\u540D\u79F0\u3002", + "dap.settings.api_desc": "\u6839\u636E\u5F53\u524D\u9879\u76EE\u8BBE\u7F6E\u663E\u793A\u8C03\u7528\u901F\u67E5\u3002", + "dap.settings.copy": "\u590D\u5236", + "dap.settings.copied": "\u5DF2\u590D\u5236\u5230\u526A\u8D34\u677F", + "dap.settings.copy_failed": "\u65E0\u6CD5\u8BBF\u95EE\u526A\u8D34\u677F", + "dap.settings.clipboard_permission": "Java \u663E\u793A\u52A8\u753B\u5668\u9700\u8981\u8BBF\u95EE\u526A\u8D34\u677F\uFF0C\u624D\u80FD\u590D\u5236\u6570\u636E\u5305\u63A5\u53E3\u5F15\u7528\u3002", + "dap.settings.api_item": "\u7269\u54C1\u6A21\u578B ID", + "dap.settings.api_item_note": "\u7528\u4E8E minecraft:item_model \u7269\u54C1\u7EC4\u4EF6\u3002", + "dap.settings.api_common": "\u901A\u7528\u51FD\u6570", + "dap.settings.api_common_note": "\u7ED9\u4E88\u52A8\u753B\u7269\u54C1\uFF0C\u6216\u505C\u6B62\u5F53\u524D\u52A8\u753B\u3002", + "dap.settings.api_short": "\u6D4B\u8BD5\u4E0E\u56FA\u5B9A\u52A8\u753B\u8C03\u7528", + "dap.settings.api_short_note": "\u5C06 <\u52A8\u753B\u540D> \u66FF\u6362\u4E3A\u5BFC\u51FA\u7684\u52A8\u753B key\u3002", + "dap.settings.api_macro": "\u6570\u636E\u5305\u52A8\u6001\u8C03\u7528\u5B8F\u63A5\u53E3", + "dap.settings.api_macro_note": "\u52A8\u753B\u540D\u6216\u64AD\u653E\u6A21\u5F0F\u9700\u8981\u7531\u5730\u56FE\u903B\u8F91\u52A8\u6001\u4F20\u5165\u65F6\u4F7F\u7528\u3002", + "dap.settings.api_context": "\u6307\u5B9A\u73A9\u5BB6\u4F5C\u4E3A\u6267\u884C\u8005", + "dap.settings.api_context_note": "\u516C\u5F00\u51FD\u6570\u4EE5\u6267\u884C\u8005 @s \u4E3A\u76EE\u6807\uFF1B\u64CD\u4F5C\u5176\u4ED6\u73A9\u5BB6\u65F6\u4F7F\u7528 execute as\u3002", + "dap.settings.api_animation_placeholder": "\u52A8\u753B\u540D", + "dap.settings.api_player_placeholder": "\u73A9\u5BB6", + "dap.settings.api_no_animation": "\u5F53\u524D\u6CA1\u6709\u9009\u62E9\u9700\u8981\u5BFC\u51FA\u7684\u52A8\u753B\u3002", + "dap.settings.api_reference": "\u6570\u636E\u5305\u5F15\u7528\uFF1A\u516C\u5F00\u51FD\u6570\u4F4D\u4E8E data/jsb/function/{project}/\u3002\u51FD\u6570\u4EE5\u6267\u884C\u8005 @s \u4E3A\u76EE\u6807\uFF1B\u6BCF\u4EF6\u4E0D\u53EF\u5806\u53E0\u7269\u54C1\u5728 custom_data.jsb \u4E2D\u4FDD\u5B58\u72EC\u7ACB\u64AD\u653E\u72B6\u6001\uFF0Ccustom_model_data.strings[0] \u4FDD\u5B58\u52A8\u753B key\uFF0Cfloats[0] \u4FDD\u5B58\u5C40\u90E8\u5E27\u3002", + "dap.settings.shared_root": "\u5171\u4EAB\u521B\u5EFA\u6839\u76EE\u5F55", + "dap.settings.resource_folder": "\u8D44\u6E90\u5305\u76EE\u5F55 / \u7236\u76EE\u5F55", + "dap.settings.datapack_folder": "\u6570\u636E\u5305\u76EE\u5F55 / \u7236\u76EE\u5F55", + "dap.settings.folder_empty": "\u9009\u62E9\u76EE\u5F55\uFF0C\u6216\u7559\u7A7A\u4EE5\u5728\u5BFC\u51FA\u65F6\u8BE2\u95EE", + "dap.settings.browse": "\u6D4F\u89C8\u2026", + "dap.settings.saved_hint": "\u4FEE\u6539\u4F1A\u7ACB\u5373\u5199\u5165\u5F53\u524D Blockbench \u9879\u76EE\uFF1B\u8BF7\u4FDD\u5B58\u9879\u76EE\u4EE5\u4FDD\u7559\u3002", + "dap.settings.close": "\u5173\u95ED", + "dap.settings.valid_identifier": "Minecraft \u6807\u8BC6\u7B26\u6709\u6548", + "dap.settings.invalid_identifier": "\u4EC5\u4F7F\u7528\u5C0F\u5199 a-z\u30010-9\u3001\u4E0B\u5212\u7EBF\u3001\u8FDE\u5B57\u7B26\u6216\u53E5\u70B9\uFF0C\u4E14\u4E0D\u80FD\u4F7F\u7528\u4FDD\u7559\u8DEF\u5F84\u540D\u3002", + "dap.settings.valid_runtime_name": "\u8FD0\u884C\u65F6\u540D\u79F0\u6709\u6548", + "dap.settings.invalid_runtime_name": "\u4F7F\u7528 1\u201316 \u4E2A\u5B57\u7B26\uFF1A\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u53E5\u70B9\u3001\u4E0B\u5212\u7EBF\u3001\u52A0\u53F7\u6216\u8FDE\u5B57\u7B26\u3002", + "dap.settings.valid_playing_tag": "\u64AD\u653E\u6807\u8BB0\u6709\u6548", + "dap.settings.invalid_playing_tag": "\u4EC5\u4F7F\u7528\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u53E5\u70B9\u3001\u4E0B\u5212\u7EBF\u3001\u52A0\u53F7\u6216\u8FDE\u5B57\u7B26\uFF0C\u4E0D\u80FD\u5305\u542B\u7A7A\u683C\u3002", + "dap.settings.folder_optional": "\u672A\u914D\u7F6E\u76EE\u5F55\uFF1B\u5BFC\u51FA\u65F6\u7531 Blockbench \u8BE2\u95EE\u3002", + "dap.settings.folder_missing": "\u6240\u9009\u8DEF\u5F84\u4E0D\u5B58\u5728\u3002", + "dap.settings.folder_unreadable": "\u8BE5\u8DEF\u5F84\u4E0D\u662F\u53EF\u8BFB\u6587\u4EF6\u5939\uFF0C\u6216\u5C1A\u672A\u6388\u6743\u8BBF\u95EE\u3002", + "dap.settings.folder_valid_parent": "\u6709\u6548\u7684\u8F93\u51FA\u7236\u76EE\u5F55", + "dap.settings.folder_no_pack_meta": "\u63D2\u5165\u6A21\u5F0F\u8981\u6C42\u8BE5\u76EE\u5F55\u4E2D\u5B58\u5728 pack.mcmeta\u3002", + "dap.settings.folder_invalid_pack_meta": "pack.mcmeta \u4E0D\u662F\u5305\u542B pack \u5BF9\u8C61\u7684\u6709\u6548 JSON\u3002", + "dap.settings.folder_valid_pack": "\u6709\u6548\u7684\u5DF2\u89E3\u538B\u73B0\u6709\u5305", + "dap.settings.folder_no_assets": "pack.mcmeta \u6709\u6548\uFF1B\u7F3A\u5C11\u7684 assets \u76EE\u5F55\u5C06\u5728\u63D2\u5165\u65F6\u521B\u5EFA\u3002", + "dap.settings.folder_no_data": "pack.mcmeta \u6709\u6548\uFF1B\u7F3A\u5C11\u7684 data \u76EE\u5F55\u5C06\u5728\u63D2\u5165\u65F6\u521B\u5EFA\u3002", + "dap.settings.folder_valid_shared": "resource-packs/ \u548C datapacks/ \u4E0B\u7684\u4E24\u4E2A\u540C\u540D\u5305\u5747\u5B58\u5728\u3002", + "dap.settings.folder_invalid_shared": "\u8BE5\u6839\u76EE\u5F55\u4E0B\u5E94\u5B58\u5728 resource-packs/<\u5305\u540D>/pack.mcmeta \u548C datapacks/<\u5305\u540D>/pack.mcmeta\u3002", + "dap.settings.create_desc": "\u5728\u6240\u9009\u8F93\u51FA\u4F4D\u7F6E\u5185\u521B\u5EFA\u6307\u5B9A\u5305\u540D\u7684\u6587\u4EF6\u5939\u3002", + "dap.settings.insert_desc": "\u8BFB\u53D6\u5E76\u66F4\u65B0\u5DF2\u89E3\u538B\u73B0\u6709\u5305\uFF0C\u4FDD\u7559\u65E0\u5173\u6587\u4EF6\u3002", + "dap.settings.shared_desc": "\u5C06 resource-packs/<\u5305> \u548C datapacks/<\u5305> \u4FDD\u5B58\u5728\u540C\u4E00\u5730\u56FE\u8F93\u51FA\u6839\u76EE\u5F55\u3002", + "dap.settings.separate_desc": "\u5206\u522B\u914D\u7F6E\u8D44\u6E90\u5305\u4E0E\u6570\u636E\u5305\u4F4D\u7F6E\u3002", + "dap.settings.resource_only_desc": "\u4EC5\u751F\u6210\u6216\u66F4\u65B0\u6A21\u578B\u3001\u7269\u54C1\u5B9A\u4E49\u548C\u7EB9\u7406\u3002", + "dap.settings.datapack_only_desc": "\u4EC5\u751F\u6210\u6216\u66F4\u65B0\u52A8\u753B\u9A71\u52A8\uFF1B\u5FC5\u987B\u914D\u5408\u6620\u5C04\u4E00\u81F4\u7684\u8D44\u6E90\u5305\u3002", + "dap.settings.animations_valid": "\u5DF2\u9009 {count} \u6BB5\u52A8\u753B\uFF1B\u6240\u6709\u751F\u6210 key \u5747\u6709\u6548\u4E14\u552F\u4E00\u3002", + "dap.settings.animations_empty": "\u81F3\u5C11\u9009\u62E9\u4E00\u6BB5\u52A8\u753B\u3002", + "dap.settings.animations_invalid": "\u8BF7\u89E3\u51B3\u65E0\u6548\u6216\u91CD\u590D\u52A8\u753B key\uFF1A{details}", + "dap.export.open_settings_hint": "\u8BF7\u6253\u5F00\u201CJava \u663E\u793A\u52A8\u753B\u5668\u9879\u76EE\u8BBE\u7F6E\u201D\u4FEE\u6B63\u540E\u91CD\u65B0\u5BFC\u51FA\u3002", + "dap.export.property.name": "\u52A8\u753B\u5BFC\u51FA\u9009\u62E9", + "dap.export.property.description": "\u4EC5\u5728\u6587\u4EF6\u6210\u529F\u5199\u5165\u540E\u8BB0\u4F4F\u5168\u90E8\u5BFC\u51FA\u8BBE\u7F6E", "dap.export.title": "\u5BFC\u51FA\u8D44\u6E90\u5305\u548C\u6570\u636E\u5305", + "dap.export.select_title": "\u9009\u62E9\u8981\u5BFC\u51FA\u7684\u52A8\u753B", + "dap.export.select_help": "\u9009\u62E9\u8981\u70D8\u7119\u7684\u52A8\u753B\u3002Minecraft key \u7531 Blockbench \u52A8\u753B\u540D\u81EA\u52A8\u751F\u6210\uFF0C\u5E76\u4F9B play\u3001loop \u4E0E frame \u547D\u4EE4\u4F7F\u7528\u3002", + "dap.export.selection": "\u9009\u62E9\u64CD\u4F5C", + "dap.export.select_all": "\u5168\u9009", + "dap.export.select_none": "\u5168\u4E0D\u9009", + "dap.export.select_required": "\u8BF7\u81F3\u5C11\u9009\u62E9\u4E00\u6BB5\u52A8\u753B\u540E\u518D\u7EE7\u7EED\u3002", + "dap.export.animation_row": "Key\uFF1A{key} \xB7 \u65F6\u957F\uFF1A{duration} \u79D2 \xB7 \u6E90\u5E27\u7387\uFF1A{source_fps} FPS \xB7 \u8F93\u51FA\uFF1A{game_fps} FPS\u3001{game_frames} \u5E27", + "dap.export.invalid_key": "\u65E0\u6548 key", + "dap.export.key_conflict_title": "\u52A8\u753B\u540D\u79F0\u65E0\u6CD5\u5BFC\u51FA", + "dap.export.key_conflict_message": "\u8BF7\u91CD\u547D\u540D\u4E0B\u5217\u52A8\u753B\uFF0C\u786E\u4FDD\u6BCF\u4E2A\u81EA\u52A8\u751F\u6210\u7684 Minecraft key \u90FD\u6709\u6548\u4E14\u552F\u4E00\uFF1A\n\n{details}", + "dap.export.key_conflict_entry": "Key\u201C{key}\u201D\uFF1A{animations}", + "dap.export.animation_summary": "{animation} \u2192 {key} \xB7 {source_fps} FPS \u2192 {game_frames} \u4E2A\u6E38\u620F\u5E27", + "dap.export.default_animation": "\u9ED8\u8BA4\u52A8\u753B", + "dap.export.default_missing": "\u6240\u9009\u9ED8\u8BA4\u52A8\u753B\u5DF2\u4E0D\u5B58\u5728\u3002", "dap.export.output": "\u5BFC\u51FA\u5185\u5BB9", + "dap.export.write_mode": "\u5199\u5165\u65B9\u5F0F", + "dap.export.write_mode.create": "\u521B\u5EFA\u65B0\u5305", + "dap.export.write_mode.insert": "\u63D2\u5165\u73B0\u6709\u5305", "dap.export.pack_name": "\u5305\u540D", - "dap.export.asset_namespace": "\u8D44\u6E90\u547D\u540D\u7A7A\u95F4", - "dap.export.item_model": "\u7269\u54C1\u6A21\u578B\u540D", + "dap.export.project_name": "\u9879\u76EE\u540D", + "dap.export.project_name_hint": "\u56FA\u5B9A jsb \u547D\u540D\u7A7A\u95F4\u4E0B\u7684\u552F\u4E00\u6A21\u5757 ID\uFF0C\u7528\u4E8E\u76EE\u5F55\u3001\u7269\u54C1\u6A21\u578B\u548C\u51FD\u6570\u547D\u4EE4\u3002", "dap.export.base_item": "\u6620\u5C04\u7269\u54C1", "dap.export.display_name": "\u7269\u54C1\u663E\u793A\u540D", - "dap.export.data_namespace": "\u6570\u636E\u5305\u547D\u540D\u7A7A\u95F4", "dap.export.frame_objective": "\u5E27\u8BB0\u5206\u677F", "dap.export.mode_objective": "\u6A21\u5F0F\u8BB0\u5206\u677F", + "dap.export.max_frame_objective": "\u6700\u5927\u5E27\u8BB0\u5206\u677F", + "dap.export.objective_conflict": "\u5E27\u3001\u6A21\u5F0F\u548C\u6700\u5927\u5E27\u8BB0\u5206\u677F\u5FC5\u987B\u4F7F\u7528\u4E09\u4E2A\u4E0D\u540C\u7684\u540D\u79F0\u3002", "dap.export.playing_tag": "\u64AD\u653E\u6807\u8BB0", "dap.export.mode.both_default": "\u8D44\u6E90\u5305 + \u6570\u636E\u5305\uFF08\u5171\u7528\u6839\u76EE\u5F55\uFF09", "dap.export.mode.both_separate": "\u8D44\u6E90\u5305 + \u6570\u636E\u5305\uFF08\u5206\u522B\u9009\u62E9\u7236\u76EE\u5F55\uFF09", @@ -152,6 +457,10 @@ "dap.export.cancel_export": "\u53D6\u6D88\u5BFC\u51FA", "dap.export.export_anyway": "\u4ECD\u7136\u5BFC\u51FA", "dap.export.warnings_title": "\u8BF7\u786E\u8BA4\u5BFC\u51FA\u8B66\u544A", + "dap.export.open_bounds_check": "\u6253\u5F00\u8303\u56F4\u68C0\u6D4B", + "dap.export.problem_frames": "\u95EE\u9898\u5E27", + "dap.export.locate_frame": "\u7B2C {frame} \u5E27", + "dap.export.located_frame": "\u5DF2\u5B9A\u4F4D\u5230 {animation} \u7684\u7B2C {frame} \u5E27", "dap.export.cancelled": "\u5DF2\u53D6\u6D88\u5BFC\u51FA\uFF0C\u6CA1\u6709\u751F\u6210\u6587\u4EF6", "dap.export.preparing_files": "\u5DF2\u786E\u8BA4\u8B66\u544A\uFF0C\u6B63\u5728\u751F\u6210\u5305\u6587\u4EF6\u2026", "dap.export.overwrite": "\u8986\u76D6\u5E76\u5BFC\u51FA", @@ -160,21 +469,78 @@ "dap.export.pick_shared": "\u9009\u62E9\u5BFC\u51FA\u6839\u76EE\u5F55\uFF08\u5C06\u521B\u5EFA resource-packs/{pack}/ \u548C datapacks/{pack}/\uFF09", "dap.export.pick_resource": "\u9009\u62E9\u8D44\u6E90\u5305\u7236\u76EE\u5F55\uFF08\u5C06\u521B\u5EFA {pack}/\uFF09", "dap.export.pick_datapack": "\u9009\u62E9\u6570\u636E\u5305\u7236\u76EE\u5F55\uFF08\u5C06\u521B\u5EFA {pack}/\uFF09", + "dap.export.pick_existing_resource": "\u9009\u62E9\u5DF2\u6709\u4E14\u5DF2\u89E3\u538B\u7684\u8D44\u6E90\u5305\u6587\u4EF6\u5939", + "dap.export.pick_existing_datapack": "\u9009\u62E9\u5DF2\u6709\u4E14\u5DF2\u89E3\u538B\u7684\u6570\u636E\u5305\u6587\u4EF6\u5939", "dap.export.baking": "\u6B63\u5728\u4EE5 {fps} FPS \u70D8\u7119 {frames} \u5E27\u2026", + "dap.export.baking_animation": "\u6B63\u5728\u70D8\u7119 {animation}\uFF1A\u4EE5 {fps} FPS \u8F93\u51FA {frames} \u5E27\u2026", "dap.export.failed": "\u5BFC\u51FA\u5931\u8D25", "dap.export.no_frames": "\u6CA1\u6709\u70D8\u7119\u51FA\u4EFB\u4F55\u5E27\u3002\u8BF7\u786E\u8BA4\u52A8\u753B\u4E2D\u5305\u542B\u5173\u952E\u5E27\u3002", + "dap.export.no_frames_for_animation": "\u6CA1\u6709\u4E3A {animation} \u70D8\u7119\u51FA\u4EFB\u4F55\u5E27\u3002\u8BF7\u786E\u8BA4\u8BE5\u52A8\u753B\u4E2D\u5305\u542B\u5173\u952E\u5E27\u3002", + "dap.export.no_animated_context_title": "\u6CA1\u6709\u663E\u793A\u4F4D\u7F6E\u542F\u7528\u52A8\u753B", + "dap.export.no_animated_context_message": "\u6240\u6709\u663E\u793A\u4F4D\u7F6E\u90FD\u5173\u95ED\u4E86\u52A8\u753B\u3002\u8D44\u6E90\u5305\u53EA\u4F1A\u5199\u5165\u9ED8\u8BA4\u52A8\u753B {default_animation} \u7684\u7B2C 0 \u5E27\uFF1B\u5176\u4ED6\u5DF2\u9009\u52A8\u753B\u4E0D\u4F1A\u6709\u53EF\u89C1\u7684\u64AD\u653E\u8DEF\u5F84\u3002", + "dap.export.datapack_only_title": "\u6570\u636E\u5305\u9700\u8981\u5339\u914D\u7684\u8D44\u6E90\u5305", + "dap.export.datapack_only_message": "\u6B64\u6570\u636E\u5305\u4F7F\u7528\u672C\u6B21\u6240\u9009\u52A8\u753B\u7684 key \u548C\u5E27\u6570\uFF0C\u53EA\u80FD\u4E0E\u4F7F\u7528\u76F8\u540C\u52A8\u753B\u6620\u5C04\u5BFC\u51FA\u7684\u8D44\u6E90\u5305\u914D\u5957\u4F7F\u7528\u3002", "dap.export.resampled_title": "\u5DF2\u91CD\u91C7\u6837\u4E3A\u6E38\u620F\u5E27\u7387", - "dap.export.resampled_message": "\u52A8\u753B\u5438\u9644\u5E27\u7387\u4E3A {source_fps} FPS\uFF0C\u800C Minecraft \u6BCF\u79D2\u6700\u591A\u663E\u793A {game_fps} \u5E27\u3002\n\n\u672C\u6B21\u5BFC\u51FA\u5C06\u4F7F\u7528 {game_fps} FPS \u7684 {frames} \u5E27\uFF0C\u5E76\u4FDD\u6301\u539F\u52A8\u753B\u65F6\u957F\u3002", + "dap.export.resampled_message": "\u52A8\u753B\u5438\u9644\u5E27\u7387\u4E3A {source_fps} FPS\uFF0C\u5DE5\u7A0B\u5BFC\u51FA\u5E27\u7387\u4E3A {game_fps} FPS\u3002\n\n\u672C\u6B21\u5BFC\u51FA\u5C06\u4F7F\u7528 {game_fps} FPS \u7684 {frames} \u5E27\uFF0C\u5E76\u4FDD\u6301\u539F\u52A8\u753B\u65F6\u957F\u3002", "dap.export.texture_mismatch_title": "\u7EB9\u7406\u5206\u8FA8\u7387\u4E0D\u4E00\u81F4", "dap.export.texture_mismatch": "\u5DE5\u7A0B UV \u5206\u8FA8\u7387\u4E3A {project_width}\xD7{project_height}\uFF0C\u4F46\u4EE5\u4E0B\u7EB9\u7406\u4F7F\u7528\u4E86\u4E0D\u540C\u5C3A\u5BF8\uFF1A\n\n{textures}\n\n\u5BFC\u51FA\u7684 UV \u53EF\u80FD\u504F\u79FB\u3002\u8BF7\u5728\u5BFC\u51FA\u524D\u66F4\u65B0\u201C\u6587\u4EF6 \u2192 \u9879\u76EE\u8BBE\u7F6E\u201D\u3002", "dap.export.bounds_title": "\u90E8\u5206\u5E27\u8D85\u51FA\u6A21\u578B\u8303\u56F4", "dap.export.resource_description": "{name}\uFF08{frames} \u5E27 @ {fps} FPS\uFF09", "dap.export.datapack_description": "{name} \u7684\u9010\u5E27\u52A8\u753B\u9A71\u52A8\uFF08{frames} \u5E27\uFF09", + "dap.export.resource_description_multi": "{name}\uFF08{animations} \u6BB5\u52A8\u753B\uFF0C{frames} \u4E2A\u91C7\u6837\u5E27 @ {fps} FPS\uFF09", + "dap.export.datapack_description_multi": "{name} \u7684\u591A\u6BB5\u9010\u5E27\u52A8\u753B\u9A71\u52A8\uFF08{animations} \u6BB5\u52A8\u753B\uFF0C\u5171 {frames} \u5E27\uFF09", "dap.export.complete": "\u5BFC\u51FA\u5B8C\u6210", + "dap.export.complete_heading": "\u5BFC\u51FA\u6210\u529F", + "dap.export.item_model_id": "\u7269\u54C1\u6A21\u578B ID\uFF1A{id}", "dap.export.locations": "\u5DF2\u9A8C\u8BC1\u5E76\u5199\u5165 {count} \u4E2A\u6587\u4EF6\u3002\n\n{locations}", + "dap.export.write_success": "\u5DF2\u6210\u529F\u9A8C\u8BC1\u5E76\u5199\u5165 {count} \u4E2A\u6587\u4EF6", + "dap.export.output_locations": "\u8F93\u51FA\u4F4D\u7F6E", "dap.export.optimization": "\u7A7A\u95F4\u4F18\u5316\uFF1A\u91C7\u6837 {sampled} \u5E27\uFF0C\u5199\u5165 {unique} \u4E2A\u552F\u4E00\u6A21\u578B\uFF0C\u53BB\u91CD {duplicates} \u5E27\u3002\n\u6A21\u578B JSON\uFF1A{before} \u2192 {after}\u3002", + "dap.export.space_optimization": "\u7A7A\u95F4\u4F18\u5316\uFF1A", + "dap.export.optimization_statistics": "\u91C7\u6837 {sampled} \u5E27\uFF0C\u5199\u5165 {unique} \u4E2A\u552F\u4E00\u6A21\u578B\uFF0C\u53BB\u91CD {duplicates} \u5E27\u3002", + "dap.export.model_json_size": "\u6A21\u578B JSON\uFF1A{before} \u2192 {after}", + "dap.export.animation_report": "{animation}\uFF1A\u91C7\u6837 {frames} \u5E27", "dap.export.omitted": "\u5DF2\u5FFD\u7565 {faces} \u4E2A\u672A\u8D34\u56FE\u9762\uFF0C\u5E76\u79FB\u9664 {elements} \u4E2A\u6CA1\u6709\u53EF\u89C1\u9762\u7684\u5143\u7D20\u3002", + "dap.export.animation_keys": "\u52A8\u753B key\uFF1A{keys}", "dap.export.commands": "\u6E38\u620F\u5185\u547D\u4EE4\uFF1A", + "dap.settings.developer_tips": "\u5F00\u53D1\u8005\u63D0\u793A", + "dap.settings.developer_tips_help": "\u5F00\u542F\u6570\u636E\u5305\u6FC0\u6D3B\u3001\u7ED9\u4E88\u7269\u54C1\u548C\u64AD\u653E\u63A7\u5236\u65F6\u7684\u6E38\u620F\u5185\u63D0\u793A\uFF1B\u5173\u95ED\u540E\u6570\u636E\u5305\u5B8C\u5168\u9759\u9ED8\uFF0C\u9002\u5408\u6B63\u5F0F\u5730\u56FE\u3002", + "dap.settings.hand_rendering": "\u6E32\u67D3\u73A9\u5BB6\u76AE\u80A4\u624B\u90E8", + "dap.settings.hand_rendering_help": "\u751F\u6210\u6807\u51C6 4\xD74\xD712 \u7BB1\u578B UV \u624B\u81C2\u5360\u4F4D\u6A21\u578B\uFF08\u5DE6 0,0,4\u3001\u53F3 12,0,4\uFF09\uFF0C\u4F7F\u7528\u8F6F\u4EF6\u5185\u7F6E 16\xD716 \u9ED8\u8BA4\u7EB9\u7406\uFF1B\u5BFC\u51FA\u4F4D\u7F6E\u4E0E\u65CB\u8F6C\u8BFB\u53D6\u624B\u81C2\u7EC4\u7684\u9759\u6001\u6446\u653E\u53CA\u52A8\u753B\uFF1B\u9884\u89C8\u5C3A\u5BF8\u72EC\u7ACB\uFF0C\u5DE6\u53F3\u624B scale \u6807\u8BB0\u56FA\u5B9A\u4E0D\u53D8\u3002\u4F1A\u5BFC\u51FA Minecraft 26.2 \u6838\u5FC3\u5B9E\u4F53\u7740\u8272\u5668\uFF0C\u53EF\u80FD\u4E0E\u66FF\u6362\u540C\u540D\u7740\u8272\u5668\u7684\u8D44\u6E90\u5305\u51B2\u7A81\u3002", + "dap.hand.skin_version_help": "\u624B\u81C2\u76AE\u80A4\u76EE\u524D\u4EC5\u652F\u6301 Minecraft Java 1.21.11 \u53CA\u4EE5\u4E0A\u7248\u672C\u3002", + "dap.hand.shader_incompatible": "\u4E0E\u5149\u5F71\u4E0D\u517C\u5BB9", + "dap.hand.shader_incompatible_help": "\u4F7F\u7528\u73A9\u5BB6\u76AE\u80A4\u624B\u81C2\u65F6\u8BF7\u5173\u95ED\u5149\u5F71\u3002\u5F00\u542F\u5149\u5F71\u4F1A\u4F7F\u624B\u81C2\u8D34\u56FE\u8F6C\u6362\u5931\u6548\uFF0C\u663E\u793A\u4E3A\u73A9\u5BB6\u5934\u90E8\u8D34\u56FE\u3002", + "dap.hand.undo_create": "\u521B\u5EFA\u73A9\u5BB6\u624B\u81C2\u7ED1\u5B9A", + "dap.hand.undo_delete": "\u5220\u9664\u73A9\u5BB6\u624B\u81C2\u7ED1\u5B9A", + "dap.hand.delete": "\u5220\u9664\u624B\u81C2\u7ED1\u5B9A\u2026", + "dap.hand.delete_title": "\u5220\u9664\u73A9\u5BB6\u624B\u81C2\u7ED1\u5B9A", + "dap.hand.delete_message": "\u63D2\u4EF6\u521B\u5EFA\u7684\u624B\u81C2\u7EC4\u53CA\u5176\u52A8\u753B\u8F68\u5C06\u88AB\u5220\u9664\u3002\u63A5\u7BA1\u7684 lefthand/righthand \u7EC4\u53EA\u4F1A\u89E3\u9664\u7ED1\u5B9A\uFF0C\u4E0D\u4F1A\u5220\u9664\u3002", + "dap.hand.delete_confirm": "\u5220\u9664\u7ED1\u5B9A", + "dap.hand.scale_unsupported": "\u52A8\u753B {animation} \u5728 {group} \u4E0A\u542B\u6709\u7F29\u653E\u5173\u952E\u5E27\u3002\u7740\u8272\u5668\u4F9D\u8D56\u7CBE\u786E\u7F29\u653E\u503C\u8BC6\u522B\u5DE6\u53F3\u624B\uFF0C\u56E0\u6B64\u4E0D\u652F\u6301\u624B\u81C2\u7F29\u653E\u52A8\u753B\u3002", + "dap.hand.rig_missing": "\u5DF2\u542F\u7528\u73A9\u5BB6\u624B\u81C2\u6E32\u67D3\uFF0C\u4F46\u627E\u4E0D\u5230\u624B\u81C2\u7ED1\u5B9A\u3002\u8BF7\u5728\u5DE5\u7A0B\u8BBE\u7F6E\u4E2D\u91CD\u65B0\u52FE\u9009\u201C\u6E32\u67D3\u73A9\u5BB6\u76AE\u80A4\u624B\u90E8\u201D\u4EE5\u91CD\u65B0\u521B\u5EFA\u3002", + "dap.settings.exact_bounds_export": "\u5BFC\u51FA\u524D\u8FDB\u884C\u7CBE\u786E\u8303\u56F4\u68C0\u6D4B", + "dap.settings.exact_bounds_export_help": "\u6821\u9A8C\u9694\u79BB\u5BFC\u51FA\u70D8\u7119\u5E76\u8BB0\u5FC6\u6BCF\u6BB5\u52A8\u753B\u7684\u7CBE\u786E\u7ED3\u679C\uFF1B\u5173\u95ED\u540E\u4ECD\u4F1A\u9694\u79BB\u70D8\u7119\uFF0C\u4F46\u4E0D\u751F\u6210\u8303\u56F4\u8B66\u544A\u6216\u68C0\u6D4B\u72B6\u6001\u3002", + "dap.export.summary": "\u5BFC\u51FA\u6458\u8981", + "dap.export.developer_info": "\u5F00\u53D1\u8005\u4FE1\u606F", + "dap.export.developer_tips_status": "\u5F00\u53D1\u8005\u63D0\u793A\u4FE1\u606F\uFF1A{status}", + "dap.export.hand_rendering_status": "\u73A9\u5BB6\u76AE\u80A4\u624B\u90E8\uFF1A{status}", + "dap.export.developer_tips_enabled": "\u5F00\u542F", + "dap.export.developer_tips_disabled": "\u5173\u95ED", + "dap.export.hand_rendering_warning_title": "\u73A9\u5BB6\u76AE\u80A4\u624B\u90E8\u4F1A\u4F7F\u7528\u6838\u5FC3\u7740\u8272\u5668", + "dap.export.hand_rendering_warning_message": "\u672C\u6B21\u5BFC\u51FA\u4F1A\u66FF\u6362 Minecraft 26.2 \u7684\u5B9E\u4F53\u6838\u5FC3\u7740\u8272\u5668\uFF0C\u53EF\u80FD\u4E0E\u66FF\u6362\u540C\u540D\u6587\u4EF6\u7684\u7740\u8272\u5668\u6216\u8D44\u6E90\u5305\u51B2\u7A81\u3002\u5DE6\u53F3\u624B\u59FF\u6001\u4F1A\u9010\u5E27\u91C7\u6837\uFF0C\u5E76\u5BFC\u51FA\u5230\u5DE6\u53F3\u4E24\u79CD\u7B2C\u4E00\u4EBA\u79F0\u663E\u793A\u4F4D\u7F6E\u3002", + "dap.export.hand_rendering_resource_only_title": "\u5FC5\u987B\u4F7F\u7528\u5E26\u73A9\u5BB6\u6863\u6848\u7684\u73A9\u5BB6\u5934\u9885", + "dap.export.hand_rendering_resource_only_message": "\u4EC5\u5BFC\u51FA\u8D44\u6E90\u5305\u65F6\u65E0\u6CD5\u81EA\u52A8\u5199\u5165\u89C2\u5BDF\u8005\u76AE\u80A4\u3002\u8BF7\u4F7F\u7528\u540C\u65F6\u5E26 minecraft:profile \u4E0E\u751F\u6210 item_model \u7684 minecraft:player_head\uFF0C\u6216\u914D\u5957\u5BFC\u51FA\u6570\u636E\u5305\u5E76\u8FD0\u884C\u5176 give \u51FD\u6570\u3002", + "dap.export.preflight_title": "\u786E\u8BA4\u6587\u4EF6\u53D8\u66F4", + "dap.export.preflight_message": "\u672C\u6B21 JSB \u9879\u76EE\u6A21\u5757\u5C06\u6267\u884C\u4EE5\u4E0B\u53D8\u66F4\uFF1A\n\n{summary}", + "dap.export.preflight_added": "\u65B0\u589E\u9879\u76EE\u6587\u4EF6", + "dap.export.preflight_updated": "\u66F4\u65B0\u9879\u76EE\u6587\u4EF6", + "dap.export.preflight_removed": "\u5220\u9664\u65E7\u9879\u76EE\u6587\u4EF6", + "dap.export.preflight_merged": "\u5408\u5E76 load/tick \u6807\u7B7E", + "dap.export.confirm_write": "\u5199\u5165\u53D8\u66F4", + "dap.export.conflict_title": "\u8DEF\u5F84\u51B2\u7A81", + "dap.export.conflict_message": "\u4EE5\u4E0B\u8DEF\u5F84\u5DF2\u7ECF\u5B58\u5728\uFF0C\u4F46\u4E0D\u5C5E\u4E8E\u5F53\u524D JSB \u9879\u76EE\u3002\u6CA1\u6709\u5199\u5165\u4EFB\u4F55\u6587\u4EF6\uFF1A\n\n{paths}", + "dap.export.invalid_identifier_title": "\u5305\u540D\u6216\u9879\u76EE\u540D\u65E0\u6548", + "dap.export.invalid_identifier_message": "\u8BF7\u4F7F\u7528\u5B89\u5168\u7684\u5C0F\u5199 Minecraft ID\uFF0C\u4EC5\u5305\u542B a-z\u30010-9\u3001\u4E0B\u5212\u7EBF\u3001\u77ED\u6A2A\u7EBF\u6216\u70B9\u3002\u9879\u76EE\u540D\u4E0D\u80FD\u662F _generated\u3001. \u6216 ..\u3002", "dap.export.write_error": "\u5199\u5165\u6587\u4EF6\u65F6\u53D1\u751F\u9519\u8BEF\uFF1A\n{error}", "dap.export.target_exists": "\u76EE\u6807\u5305\u5DF2\u5B58\u5728", "dap.export.target_exists_message": "\u7EE7\u7EED\u64CD\u4F5C\u4F1A\u8986\u76D6\u540C\u540D\u6587\u4EF6\uFF0C\u4F46\u4E0D\u4F1A\u5220\u9664\u5176\u4ED6\u6587\u4EF6\uFF1A\n\n{summary}", @@ -183,16 +549,59 @@ "dap.export.busy": "\u4E0A\u4E00\u6B21\u5BFC\u51FA\u4ECD\u5728\u8FDB\u884C\u4E2D", "dap.export.no_animation_title": "\u6CA1\u6709\u53EF\u5BFC\u51FA\u7684\u52A8\u753B", "dap.export.no_animation_message": "\u8BF7\u5148\u521B\u5EFA\u5305\u542B\u5173\u952E\u5E27\u7684\u52A8\u753B\u3002", - "dap.export.fps_exact": "\u5C06\u4EE5 20 FPS \u5BFC\u51FA {frames} \u5E27\uFF0C\u6BCF\u4E2A\u6E38\u620F\u523B\u5BF9\u5E94\u4E00\u5E27\u3002", + "dap.export.fps_exact": "\u5C06\u4EE5\u5DE5\u7A0B\u8BBE\u7F6E\u7684 {game_fps} FPS \u5BFC\u51FA {frames} \u5E27\u3002", "dap.export.fps_resample": "\u5F53\u524D\u5438\u9644\u5E27\u7387\u4E3A {source_fps} FPS\uFF08{source_frames} \u4E2A\u6E90\u91C7\u6837\uFF09\u3002\u5BFC\u51FA\u65F6\u5C06\u91CD\u91C7\u6837\u4E3A {game_fps} FPS\uFF08{game_frames} \u5E27\uFF09\uFF0C\u5E76\u4FDD\u6301\u52A8\u753B\u65F6\u957F\u3002", - "dap.export.folder_help": "\u5728 macOS \u4E0A\uFF0C\u201C\u6253\u5F00\u201D\u7528\u4E8E\u9009\u62E9\u7236\u76EE\u5F55\uFF0C\u63D2\u4EF6\u968F\u540E\u4F1A\u5728\u5176\u4E2D\u521B\u5EFA <\u5305\u540D>/\u3002
\u9ED8\u8BA4\u6A21\u5F0F\u4F1A\u521B\u5EFA resource-packs/<\u5305\u540D>/ \u548C datapacks/<\u5305\u540D>/\u3002", + "dap.export.folder_help": "\u521B\u5EFA\u6A21\u5F0F\u9009\u62E9\u7236\u76EE\u5F55\u5E76\u5728\u5176\u4E2D\u5EFA\u7ACB <\u5305\u540D>/\uFF1B\u63D2\u5165\u6A21\u5F0F\u9009\u62E9\u5E26\u6709\u6548 pack.mcmeta \u7684\u73B0\u6709\u5DF2\u89E3\u538B\u5305\u6587\u4EF6\u5939\u5185\u3002", "dap.bounds.no_animation": "\u6CA1\u6709\u53EF\u68C0\u67E5\u7684\u52A8\u753B", "dap.bounds.passed_title": "\u6A21\u578B\u8303\u56F4\u68C0\u67E5\u901A\u8FC7", + "dap.bounds.passed_heading": "\u672A\u53D1\u73B0\u8D8A\u754C\u5E27", "dap.bounds.passed_message": "\u5DF2\u6309 {fps} FPS \u68C0\u67E5 {frames} \u5E27\uFF0C\u6240\u6709\u5750\u6807\u5747\u5728 -16 \u5230 32 \u8303\u56F4\u5185\u3002", + "dap.bounds.passed_animation": "\u5DF2\u68C0\u67E5\u52A8\u753B\uFF1A{animation}", + "dap.bounds.passed_animations": "\u5DF2\u68C0\u67E5\u52A8\u753B\uFF1A{animations} \u4E2A", + "dap.bounds.panel_title": "\u6A21\u578B\u8303\u56F4\u68C0\u6D4B", + "dap.bounds.panel_checked_animations": "\u5DF2\u68C0\u67E5 {animations} \u4E2A\u52A8\u753B", + "dap.bounds.panel_failed": "\u53D1\u73B0 {frames} \u4E2A\u95EE\u9898\u5E27", + "dap.bounds.panel_passed": "\u5168\u90E8 {frames} \u5E27\u5747\u901A\u8FC7\u68C0\u6D4B", + "dap.bounds.panel_all_passed": "\u6240\u6709\u52A8\u753B\u5747\u901A\u8FC7\u68C0\u6D4B", + "dap.bounds.animation_failed": "{frames} \u4E2A\u95EE\u9898\u5E27", + "dap.bounds.animation_passed": "\u901A\u8FC7 \xB7 {frames} \u5E27", + "dap.bounds.panel_hint": "\u70B9\u51FB\u95EE\u9898\u5E27\u53EF\u5B9A\u4F4D\u65F6\u95F4\u8F74\u3001\u9AD8\u4EAE\u76F8\u5173\u7EC4\u4EF6\u5E76\u6253\u5F00\u6700\u8FD1\u7684\u5F71\u54CD\u5173\u952E\u5E27\uFF1B\u4FEE\u6539\u540E\u70B9\u51FB\u201C\u91CD\u65B0\u68C0\u6D4B\u201D\u3002", + "dap.bounds.recheck": "\u91CD\u65B0\u68C0\u6D4B", + "dap.bounds.mode.quick": "\u5FEB\u901F\u6570\u5B66\u68C0\u6D4B", + "dap.bounds.mode.exact": "\u7CBE\u786E\u9694\u79BB\u68C0\u6D4B", + "dap.bounds.mode.export_bake": "\u9694\u79BB\u5BFC\u51FA\u70D8\u7119", + "dap.bounds.choose_title": "\u9009\u62E9\u8303\u56F4\u68C0\u6D4B\u65B9\u5F0F", + "dap.bounds.choose_message": "\u5FEB\u901F\u68C0\u6D4B\u4F7F\u7528\u4E0D\u4FEE\u6539\u5DE5\u7A0B\u7684\u77E9\u9635\u6570\u5B66\uFF1B\u7CBE\u786E\u68C0\u6D4B\u521B\u5EFA\u4E00\u6B21\u6027\u5185\u5B58\u5DE5\u7A0B\uFF0C\u5E76\u4F7F\u7528 Blockbench Java \u7F16\u8BD1\u5668\u9010\u5E27\u9A8C\u8BC1\u3002", + "dap.bounds.choose_cancel": "\u53D6\u6D88", + "dap.bounds.progress_preparing": "\u6B63\u5728\u51C6\u5907\u8303\u56F4\u68C0\u6D4B\u2026", + "dap.bounds.progress_title": "{mode}\uFF1A{animation}", + "dap.bounds.progress_frames": "\u5F53\u524D\u52A8\u753B {frame}/{frames} \u5E27 \xB7 \u603B\u8FDB\u5EA6 {completed}/{total}", + "dap.bounds.progress_status": "{mode}\uFF1A{animation} {frame}/{frames}", + "dap.bounds.export_bake_complete": "\u9694\u79BB\u5BFC\u51FA\u70D8\u7119\u5B8C\u6210", + "dap.bounds.export_bake_complete_detail": "\u5DF2\u4ECE {animations} \u4E2A\u52A8\u753B\u751F\u6210 {frames} \u5E27\uFF0C\u53EF\u4EE5\u7EE7\u7EED\u5B8C\u6210\u5BFC\u51FA\u6D41\u7A0B\u3002", + "dap.bounds.cancel": "\u53D6\u6D88\u68C0\u6D4B", + "dap.bounds.cancelling": "\u6B63\u5728\u53D6\u6D88\u2026", + "dap.bounds.cancelled": "\u5DF2\u53D6\u6D88\u8303\u56F4\u68C0\u6D4B\uFF0C\u672A\u7F13\u5B58\u4E0D\u5B8C\u6574\u7ED3\u679C", + "dap.bounds.cache_reused": "\u5DF2\u590D\u7528 {animations} \u4E2A\u52A8\u753B\u7684\u6709\u6548\u68C0\u6D4B\u7ED3\u679C", + "dap.bounds.cache_all_reused": "\u6A21\u578B\u672A\u4FEE\u6539\uFF0C\u5DF2\u590D\u7528\u5168\u90E8\u6709\u6548\u8303\u56F4\u68C0\u6D4B\u7ED3\u679C", + "dap.bounds.status.unchecked": "\u672A\u68C0\u6D4B", + "dap.bounds.status.stale": "\u68C0\u6D4B\u540E\u5DF2\u4FEE\u6539", + "dap.bounds.status.quick_passed": "\u5FEB\u901F\u68C0\u6D4B\u901A\u8FC7", + "dap.bounds.status.quick_failed": "\u5FEB\u901F\u68C0\u6D4B\u8B66\u544A", + "dap.bounds.status.exact_passed": "\u7CBE\u786E\u68C0\u6D4B\u901A\u8FC7", + "dap.bounds.status.exact_failed": "\u7CBE\u786E\u68C0\u6D4B\u8B66\u544A", + "dap.bounds.checking": "\u6B63\u5728\u6309\u5DE5\u7A0B\u52A8\u753B FPS \u68C0\u67E5 {animation}\u2026", + "dap.bounds.checking_animations": "\u6B63\u5728\u6309\u5DE5\u7A0B\u52A8\u753B FPS \u68C0\u67E5 {animations} \u4E2A\u52A8\u753B\u2026", + "dap.bounds.check_in_progress": "\u6A21\u578B\u8303\u56F4\u68C0\u6D4B\u6B63\u5728\u8FDB\u884C\uFF0C\u8BF7\u52FF\u91CD\u590D\u70B9\u51FB", + "dap.bounds.located": "\u5DF2\u5B9A\u4F4D\u5230 {animation} \u7684\u7B2C {frame} \u5E27", + "dap.bounds.edit_in_progress_title": "\u8BF7\u5148\u5B8C\u6210\u5F53\u524D\u7F16\u8F91", + "dap.bounds.edit_in_progress_message": "Blockbench \u4ECD\u6709\u5C1A\u672A\u7ED3\u675F\u7684\u6A21\u578B\u6216\u5173\u952E\u5E27\u7F16\u8F91\u3002\u8BF7\u6309 Enter \u6216\u70B9\u51FB\u9884\u89C8\u533A\u57DF\u63D0\u4EA4\u4FEE\u6539\uFF0C\u7136\u540E\u91CD\u65B0\u68C0\u6D4B\u3002\u672C\u6B21\u672A\u542F\u52A8\u4EFB\u4F55\u70D8\u7119\u64CD\u4F5C\u3002", + "dap.bounds.check_failed_title": "\u6A21\u578B\u8303\u56F4\u68C0\u6D4B\u5931\u8D25", + "dap.bake.active_edit": "Blockbench \u5B58\u5728\u5176\u4ED6\u7F16\u8F91\u4E8B\u52A1\u65F6\uFF0C\u8303\u56F4\u68C0\u6D4B\u548C\u5BFC\u51FA\u4E0D\u80FD\u542F\u52A8\u5E27\u70D8\u7119\u3002", "dap.bounds.frame": "\u7B2C {frame} \u5E27\uFF1A{parts}\uFF0C{field}.{axis} = {value}", "dap.bounds.parts_many": "{names} \u7B49 {count} \u4E2A\u90E8\u4EF6", "dap.bounds.summary": "\u5171\u6709 {frames} \u5E27\u4E2D\u7684\u90E8\u4EF6\u8D85\u51FA Minecraft \u6A21\u578B\u8303\u56F4\uFF08\u6BCF\u4E2A\u5750\u6807\u8F74\u5FC5\u987B\u4F4D\u4E8E -16 \u5230 32 \u4E4B\u95F4\uFF09\u3002", - "dap.bounds.guidance": "\u8D8A\u754C\u5E27\u53EF\u80FD\u51FA\u73B0\u504F\u79FB\u6216\u6D88\u5931\u3002\u53EF\u4F7F\u7528\u6570\u636E\u5305\u7684 next/prev \u51FD\u6570\u9010\u5E27\u68C0\u67E5\uFF0C\u7136\u540E\u5728 Blockbench \u4E2D\u51CF\u5C0F\u5BF9\u5E94\u52A8\u4F5C\u5E45\u5EA6\uFF1A", + "dap.bounds.guidance": "\u8D8A\u754C\u5E27\u53EF\u80FD\u51FA\u73B0\u504F\u79FB\u6216\u6D88\u5931\u3002\u53EF\u4F7F\u7528\u6570\u636E\u5305\u7684 frame \u547D\u4EE4\u68C0\u67E5\u6307\u5B9A\u5E27\uFF0C\u7136\u540E\u5728 Blockbench \u4E2D\u51CF\u5C0F\u5BF9\u5E94\u52A8\u4F5C\u5E45\u5EA6\uFF1A", "dap.bounds.omitted": "\u2026\u2026\u53E6\u6709 {count} \u4E2A\u8D8A\u754C\u5E27\u672A\u663E\u793A\u3002", "dap.rollback.title": "\u56DE\u6EDA\u4E0D\u5B8C\u6574\u2014\u2014\u8BF7\u52FF\u4FDD\u5B58", "dap.rollback.message": "\u70D8\u7119\u524D\u6709 {before} \u4E2A\u5173\u952E\u5E27\uFF0C\u6062\u590D\u540E\u4E3A {after} \u4E2A\uFF0C\u7F3A\u5C11 {lost} \u4E2A\u3002\n\n\u8BF7\u7ACB\u5373\u6309 Ctrl+Z \u64A4\u9500\uFF0C\u6216\u4E0D\u4FDD\u5B58\u5173\u95ED\u6587\u4EF6\u540E\u91CD\u65B0\u6253\u5F00\u3002", @@ -203,17 +612,26 @@ "dap.error.file_verify": "\u5199\u5165\u540E\u7684\u6587\u4EF6\u5185\u5BB9\u6821\u9A8C\u5931\u8D25\uFF1A{path}", "dap.error.manifest_not_written": "\u5BFC\u51FA\u6E05\u5355\u672A\u6210\u529F\u5199\u5165\uFF1A{path}", "dap.error.manifest_verify": "\u5199\u5165\u540E\u7684\u5BFC\u51FA\u6E05\u5355\u6821\u9A8C\u5931\u8D25\uFF1A{path}", + "dap.error.duplicate_target": "\u4E24\u4E2A\u751F\u6210\u5305\u6307\u5411\u4E86\u540C\u4E00\u4E2A\u76EE\u6807\u6587\u4EF6\u5939\uFF1A{path}", + "dap.error.unsafe_path": "\u751F\u6210\u6587\u4EF6\u8DEF\u5F84\u4E0D\u5B89\u5168\uFF0C\u5DF2\u62D2\u7EDD\u5199\u5165\uFF1A{path}", "dap.error.no_models": "\u6CA1\u6709\u53EF\u7528\u4E8E\u7269\u54C1\u5B9A\u4E49\u7684\u6A21\u578B", "dap.error.external_texture": "\u7EB9\u7406\u201C{label}\u201D\u4ECD\u5F15\u7528\u5916\u90E8\u56FE\u96C6\u201C{value}\u201D\u3002Minecraft 26.2 \u7269\u54C1\u6A21\u578B\u4E0D\u80FD\u6DF7\u7528\u7269\u54C1\u4E0E\u65B9\u5757\u56FE\u96C6\u3002", "dap.error.texture_not_generated": "\u6A21\u578B\u5F15\u7528\u4E86\u672A\u751F\u6210\u7684\u7EB9\u7406\uFF1A{value}", - "dap.datapack.loaded": "\u6570\u636E\u5305\u5DF2\u52A0\u8F7D\u3002\u8FD0\u884C /function {namespace}:give \u83B7\u53D6\u52A8\u753B\u7269\u54C1\u3002", - "dap.datapack.item_given": "\u5DF2\u7ED9\u4E88\u52A8\u753B\u7269\u54C1\u3002\u624B\u6301\u7269\u54C1\u65F6\u53EF\u4F7F\u7528 play_loop\u3001play_once \u6216 next/prev\u3002", + "dap.error.manifest_invalid": "JSB \u9879\u76EE\u6E05\u5355\u65E0\u6548\uFF1A{path}", + "dap.error.invalid_pack": "\u63D2\u5165\u6A21\u5F0F\u8981\u6C42\u9009\u62E9\u5E26\u6709\u6548 pack.mcmeta \u7684\u5DF2\u89E3\u538B\u5305\u6587\u4EF6\u5939\uFF1A{path}", + "dap.error.shared_tag_invalid": "\u73B0\u6709\u51FD\u6570\u6807\u7B7E\u6587\u4EF6\u65E0\u6548\uFF0C\u672A\u8FDB\u884C\u4FEE\u6539\uFF1A{path}", + "dap.error.path_conflicts": "\u73B0\u6709\u6587\u4EF6\u4E0D\u5C5E\u4E8E\u5F53\u524D JSB \u9879\u76EE\uFF0C\u4E0D\u80FD\u8986\u76D6\uFF1A\n{paths}", + "dap.error.rollback_partial": "\u5BFC\u51FA\u5931\u8D25\uFF0C\u81EA\u52A8\u56DE\u6EDA\u672A\u80FD\u6062\u590D\u90E8\u5206\u6587\u4EF6\uFF0C\u8BF7\u624B\u52A8\u68C0\u67E5\u4EE5\u4E0B\u8DEF\u5F84\uFF1A\n{paths}", + "dap.datapack.loaded": "\u6570\u636E\u5305\u5DF2\u52A0\u8F7D\u3002\u8FD0\u884C /function {namespace}/give \u83B7\u53D6\u52A8\u753B\u7269\u54C1\u3002", + "dap.datapack.item_given": "\u5DF2\u7ED9\u4E88\u52A8\u753B\u7269\u54C1\uFF08\u9ED8\u8BA4\u52A8\u753B {animation}\uFF0C\u7B2C 0 \u5E27\uFF09\u3002\u624B\u6301\u65F6\u8C03\u7528 /function {namespace}//<\u52A8\u753B\u540D> {frame:12}\uFF1B\u4F7F\u7528 stop \u505C\u6B62\u52A8\u753B\u3002", "dap.datapack.hold_item": "\u8BF7\u5148\u5C06\u52A8\u753B\u7269\u54C1\u62FF\u5728\u4E3B\u624B\u3002", - "dap.datapack.loop_started": "\u5DF2\u5F00\u59CB\u5FAA\u73AF\u64AD\u653E\uFF0820 FPS\uFF0C\u7B2C 0-{last_frame} \u5E27\uFF09\u3002", - "dap.datapack.once_started": "\u5DF2\u5F00\u59CB\u5355\u6B21\u64AD\u653E\uFF0C\u5C06\u5728\u7B2C {last_frame} \u5E27\u505C\u6B62\u3002", + "dap.datapack.loop_started": "\u5DF2\u5F00\u59CB\u5FAA\u73AF\u64AD\u653E {animation}\uFF08{fps} FPS\uFF0C\u7B2C 0-{last_frame} \u5E27\uFF09\u3002", + "dap.datapack.once_started": "\u5DF2\u5F00\u59CB\u5355\u6B21\u64AD\u653E {animation}\uFF1B\u663E\u793A\u7B2C {last_frame} \u5E27\u4E00\u4E2A\u6E38\u620F\u523B\u540E\uFF0C\u5C06\u590D\u4F4D\u5230\u9ED8\u8BA4\u52A8\u753B\u7B2C 0 \u5E27\u3002", "dap.datapack.current_frame": "\u5F53\u524D\u5E27\uFF1A", "dap.datapack.reset": "\u5DF2\u91CD\u7F6E\u5230\u7B2C 0 \u5E27\u3002", - "dap.datapack.stopped": "\u64AD\u653E\u5DF2\u505C\u6B62\u5E76\u505C\u7559\u5728\u5F53\u524D\u5E27\u3002", + "dap.datapack.stopped": "\u64AD\u653E\u5DF2\u505C\u6B62\uFF0C\u5E76\u590D\u4F4D\u5230\u9ED8\u8BA4\u52A8\u753B\u7B2C 0 \u5E27\u3002", + "dap.datapack.invalid_animation": "\u672A\u77E5\u7684\u52A8\u753B key\uFF1A{animation}", + "dap.datapack.invalid_mode": "\u65E0\u6548\u7684\u64AD\u653E\u6A21\u5F0F\uFF1A{mode}\u3002\u8BF7\u4F7F\u7528 once \u6216 loop\u3002", "dap.slot.thirdperson_righthand": "\u7B2C\u4E09\u4EBA\u79F0-\u53F3\u624B", "dap.slot.thirdperson_lefthand": "\u7B2C\u4E09\u4EBA\u79F0-\u5DE6\u624B", "dap.slot.firstperson_righthand": "\u7B2C\u4E00\u4EBA\u79F0-\u53F3\u624B", @@ -242,6 +660,341 @@ return text; } + // src/java-block-codec.ts + function isCompilingCodec(value) { + return Boolean( + value && typeof value.compile === "function" + ); + } + function resolveJavaBlockCodec() { + const candidates = [ + typeof Formats !== "undefined" ? Formats.java_block?.codec : void 0, + typeof Codecs !== "undefined" ? Codecs.java_block : void 0 + ]; + const codec = candidates.find(isCompilingCodec); + if (!codec) { + throw new Error( + "Blockbench's Java block/item model compiler is unavailable. Reload Blockbench and try again." + ); + } + return codec; + } + + // src/format.ts + var FORMAT_ID = "display_animation_sequence"; + var FORMAT_COORDINATE_OPTIONS = { + centered_grid: false + }; + var JAVA_MODEL_COMPATIBILITY_OPTIONS = { + render_sides: "front", + model_identifier: false, + parent_model_id: true, + vertex_color_ambient_occlusion: 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, + texture_folder: true, + animation_controllers: true, + animation_files: true + }; + var ownedFormat = null; + function rebindOpenProjects(format) { + if (typeof ModelProject === "undefined" || !Array.isArray(ModelProject.all)) return; + let reboundCurrentProject = false; + for (const project of ModelProject.all) { + if (project.format?.id !== FORMAT_ID || project.format === format) continue; + project.format = format; + reboundCurrentProject || (reboundCurrentProject = Project === project); + } + if (reboundCurrentProject) { + format.select?.(); + Canvas.updateAll(); + } + } + function createFormat(id) { + const javaBlockCodec = resolveJavaBlockCodec(); + return new ModelFormat(id, { + id, + name: tr("dap.format.name"), + icon: "icon-format_block", + category: "minecraft", + target: "Minecraft: Java Edition", + description: tr("dap.format.description"), + show_in_start_screen: true, + box_uv: false, + optional_box_uv: true, + single_texture: false, + ...JAVA_MODEL_COMPATIBILITY_OPTIONS, + bone_rig: true, + ...FORMAT_COORDINATE_OPTIONS, + rotate_cubes: true, + integer_size: false, + animation_mode: true, + display_mode: true, + codec: javaBlockCodec + }); + } + function registerModelFormat() { + if (!Formats[FORMAT_ID]) { + ownedFormat = createFormat(FORMAT_ID); + rebindOpenProjects(ownedFormat); + console.log(`Registered custom format "${FORMAT_ID}".`); + } + } + function unregisterModelFormat() { + if (ownedFormat && Formats[FORMAT_ID] === ownedFormat) { + ownedFormat.delete(); + console.log(`Unregistered custom format "${FORMAT_ID}".`); + } + ownedFormat = null; + } + + // src/first-person-panel.ts + var FIRST_PERSON_ASPECT = 993 / 556; + function firstPersonFocalLength(aspect) { + return aspect > 1.7 ? 18 / aspect : aspect > 1 ? 16.57 - 3.57 * aspect : 13 * aspect; + } + function fitFirstPersonViewport(width, height, aspect) { + return width / height > aspect ? { width: height * aspect, height } : { width, height: width / aspect }; + } + function applyFirstPersonDisplay(base, entry, side) { + const sign = side === "left" ? -1 : 1; + const rotation = entry?.rotation ?? [0, 0, 0]; + const translation = entry?.translation ?? [0, 0, 0]; + const scale = entry?.scale ?? [1, 1, 1]; + const mirror = entry?.mirror ?? [false, false, false]; + base.rotation.set(rotation[0] * Math.PI / 180, sign * rotation[1] * Math.PI / 180, sign * rotation[2] * Math.PI / 180); + base.position.set(sign * translation[0], translation[1], translation[2]); + base.scale.set((scale[0] || 1e-3) * (mirror[0] ? -1 : 1), (scale[1] || 1e-3) * (mirror[1] ? -1 : 1), (scale[2] || 1e-3) * (mirror[2] ? -1 : 1)); + const pivot = new THREE.Vector3().fromArray(entry?.rotation_pivot ?? [0, 0, 0]).multiplyScalar(16); + const original = new THREE.Vector3().copy(pivot); + base.position.sub(pivot.applyEuler(base.rotation).sub(original)); + pivot.fromArray(entry?.scale_pivot ?? [0, 0, 0]).multiplyScalar(16).applyEuler(base.rotation); + pivot.x *= 1 - scale[0]; + pivot.y *= 1 - scale[1]; + pivot.z *= 1 - scale[2]; + base.position.add(pivot); + } + function applyFirstPersonScale(matrix) { + matrix.elements[0] *= 1.31; + matrix.elements[5] *= 1.31; + } + var panel = null; + var frame = null; + var disposeView = null; + function registerFirstPersonPanel() { + if (panel) return; + let side = "right"; + const wrapper = document.createElement("div"); + wrapper.style.display = "flex"; + wrapper.style.flexDirection = "column"; + wrapper.style.height = "100%"; + wrapper.style.minHeight = "0"; + wrapper.style.minWidth = "0"; + wrapper.style.boxSizing = "border-box"; + wrapper.style.overflow = "hidden"; + wrapper.style.gap = "4px"; + wrapper.style.padding = "4px"; + const select = document.createElement("select"); + select.title = tr("dap.fp.side"); + select.style.width = "100%"; + select.style.minWidth = "0"; + select.style.flex = "0 0 auto"; + for (const value of ["right", "left"]) { + const option = document.createElement("option"); + option.value = value; + option.innerText = tr(`dap.slot.firstperson_${value}hand`); + select.appendChild(option); + } + select.onchange = () => { + side = select.value === "left" ? "left" : "right"; + }; + wrapper.appendChild(select); + const viewport = document.createElement("div"); + viewport.className = "dap_first_person_viewport"; + viewport.style.position = "relative"; + viewport.style.flex = "0 0 auto"; + viewport.style.minHeight = "0"; + viewport.style.overflow = "hidden"; + viewport.style.background = "transparent"; + viewport.style.display = "flex"; + viewport.style.alignItems = "flex-start"; + viewport.style.justifyContent = "center"; + const picture = document.createElement("div"); + picture.className = "dap_first_person_frame"; + picture.style.position = "relative"; + picture.style.background = "var(--color-back)"; + picture.style.outline = "1px solid var(--color-border)"; + picture.style.flex = "0 0 auto"; + viewport.appendChild(picture); + wrapper.appendChild(viewport); + panel = new Panel("display_anim_first_person", { + name: tr("dap.fp.name"), + icon: "visibility", + condition: { modes: ["animate"], formats: [FORMAT_ID, "java_block_sequence"] }, + growable: true, + resizable: true, + min_height: 190, + default_position: { slot: "left_bar", height: 270, width: 340 } + }); + panel.node.appendChild(wrapper); + let renderer = null; + const scene = new THREE.Scene(); + const area = new THREE.Object3D(); + const base = new THREE.Object3D(); + const model = new THREE.Object3D(); + model.position.set(-8, -8, -8); + scene.add(area); + area.add(base); + base.add(model); + const camera = new THREE.PerspectiveCamera(70, 1, 1, 3e4); + camera.position.set(0, 24, 32.4); + camera.aspect = FIRST_PERSON_ASPECT; + camera.setFocalLength(firstPersonFocalLength(FIRST_PERSON_ASPECT)); + const inverseRoot = new THREE.Matrix4(); + const copies = /* @__PURE__ */ new Map(); + let sourceRoot = null; + let width = 0; + let height = 0; + let pixelRatio = 0; + let lights = null; + function updateLayout(force = false) { + const aspect = FIRST_PERSON_ASPECT; + const availableHeight = Math.max(0, wrapper.clientHeight - select.clientHeight - 12); + const { width: nextWidth, height: nextHeight } = fitFirstPersonViewport(viewport.clientWidth, availableHeight, aspect); + const nextPixelRatio = devicePixelRatio; + if (force || nextWidth !== width || nextHeight !== height || pixelRatio !== nextPixelRatio) { + pixelRatio = nextPixelRatio; + renderer?.setPixelRatio(pixelRatio); + width = nextWidth; + height = nextHeight; + viewport.style.height = `${height}px`; + picture.style.width = `${width}px`; + picture.style.height = `${height}px`; + renderer?.setSize(width, height); + if (renderer) { + renderer.domElement.style.width = "100%"; + renderer.domElement.style.height = "100%"; + } + camera.aspect = aspect; + camera.updateProjectionMatrix(); + } + return width > 0 && height > 0; + } + const resizeObserver = new ResizeObserver(() => updateLayout()); + resizeObserver.observe(wrapper); + function render() { + frame = requestAnimationFrame(render); + if (sourceRoot && sourceRoot !== Project?.model_3d) { + model.clear(); + copies.clear(); + sourceRoot = null; + if (lights) scene.remove(lights); + lights = null; + } + if (!panel?.isVisible() || !panel.node.isConnected || Modes.selected.id !== "animate" || !Project || ![FORMAT_ID, "java_block_sequence"].includes(Format.id)) return; + if (!updateLayout()) return; + if (!renderer) { + renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true, preserveDrawingBuffer: true }); + renderer.domElement.style.width = "100%"; + renderer.domElement.style.height = "100%"; + renderer.domElement.style.pointerEvents = "none"; + picture.appendChild(renderer.domElement); + const crosshair = document.createElement("span"); + crosshair.innerText = "+"; + crosshair.style.position = "absolute"; + crosshair.style.left = "50%"; + crosshair.style.top = "50%"; + crosshair.style.transform = "translate(-50%, -50%)"; + crosshair.style.pointerEvents = "none"; + picture.appendChild(crosshair); + updateLayout(true); + } + renderer.toneMapping = Preview.selected.renderer.toneMapping; + if (sourceRoot !== Project.model_3d) { + model.clear(); + copies.clear(); + sourceRoot = Project.model_3d; + if (lights) scene.remove(lights); + lights = Canvas.scene.children.find((child) => child.name === "lights")?.clone(true) ?? null; + if (lights) scene.add(lights); + } + area.position.set(side === "left" ? -9.039 : 9.039, 24 - 8.318, 20.8); + applyFirstPersonDisplay(base, Project.display_settings[`firstperson_${side}hand`], side); + sourceRoot.updateMatrixWorld(true); + inverseRoot.copy(sourceRoot.matrixWorld).invert(); + const active = /* @__PURE__ */ new Set(); + for (const element of Outliner.elements) { + const source = element.mesh; + if (!(source instanceof THREE.Mesh)) continue; + active.add(source); + let copy = copies.get(source); + if (!copy) { + copy = new THREE.Mesh(source.geometry, source.material); + copy.matrixAutoUpdate = false; + copies.set(source, copy); + model.add(copy); + } + copy.geometry = source.geometry; + copy.material = source.material; + copy.visible = element.visibility !== false; + for (let ancestor = source; ancestor && ancestor !== sourceRoot; ancestor = ancestor.parent) { + if (!ancestor.visible) copy.visible = false; + } + copy.matrix.multiplyMatrices(inverseRoot, source.matrixWorld); + } + for (const [source, copy] of copies) { + if (!active.has(source)) { + model.remove(copy); + copies.delete(source); + } + } + const nativePreview = Preview.all.find((preview) => preview.id === "display"); + if (nativePreview && DisplayMode.display_slot.startsWith("firstperson_")) { + camera.copy(nativePreview.camPers, false); + camera.updateProjectionMatrix(); + } + camera.aspect = FIRST_PERSON_ASPECT; + camera.setFocalLength(firstPersonFocalLength(FIRST_PERSON_ASPECT)); + camera.updateProjectionMatrix(); + applyFirstPersonScale(camera.projectionMatrix); + camera.projectionMatrixInverse.copy(camera.projectionMatrix).invert(); + renderer.render(scene, camera); + } + disposeView = () => { + resizeObserver.disconnect(); + copies.clear(); + model.clear(); + scene.clear(); + renderer?.dispose(); + renderer?.forceContextLoss(); + renderer = null; + }; + frame = requestAnimationFrame(render); + } + function disposeFirstPersonPanel() { + if (frame !== null) cancelAnimationFrame(frame); + frame = null; + disposeView?.(); + disposeView = null; + panel?.delete(); + panel = null; + } + function openFirstPersonPanel() { + registerFirstPersonPanel(); + if (!panel) return; + if (Modes.selected.id !== "animate") Modes.options.animate?.select(); + if (panel.slot === "hidden") panel.moveTo("left_bar"); + panel.fold(false); + panel.selectTab(); + panel.moveToFront(); + panel.update(); + } + // src/display-animation-settings.ts var DISPLAY_CONTEXTS = [ { id: "thirdperson_righthand", label: "Third Person - Right Hand", defaultAnimated: false }, @@ -264,6 +1017,7 @@ if (ModelProject.properties?.[PROPERTY_NAME]) return; settingsProperty = new Property(ModelProject, "object", PROPERTY_NAME, { default: {}, + exposed: false, label: tr("dap.property.name"), description: tr("dap.property.description") }); @@ -297,19 +1051,221 @@ })); } + // src/export-layout.ts + var EXPORT_NAMESPACE = "jsb"; + var MAX_EXPORT_FPS = 20; + function frameCountFor(length, fps) { + return Math.floor(length * fps) + 1; + } + var DISPLAY_CONTEXT_PATHS = { + firstperson_righthand: "fp_r", + firstperson_lefthand: "fp_l", + thirdperson_righthand: "tp_r", + thirdperson_lefthand: "tp_l", + gui: "gui", + ground: "ground", + head: "head", + fixed: "fixed", + embedded: "embed", + on_shelf: "shelf" + }; + var SAFE_SEGMENT = /^[a-z0-9_.-]+$/; + var RUNTIME_NAME = /^[A-Za-z0-9._+\-]+$/; + function isValidObjectiveName(value) { + return value.length > 0 && value.length <= 16 && RUNTIME_NAME.test(value); + } + function isValidPlayingTag(value) { + return value.length > 0 && RUNTIME_NAME.test(value); + } + function isSafeProjectName(value) { + return Boolean(value) && value !== "." && value !== ".." && value !== "_generated" && SAFE_SEGMENT.test(value); + } + function sanitizeProjectName(value, fallback = "display_animation") { + const sanitized = value.trim().toLowerCase().replace(/[^a-z0-9_.-]+/g, "_").replace(/^_+|_+$/g, ""); + return isSafeProjectName(sanitized) ? sanitized : fallback; + } + function isReservedAnimationKey(value) { + return value === "_generated"; + } + function projectHash(value) { + let hash2 = 2166136261; + for (let index = 0; index < value.length; index++) { + hash2 ^= value.charCodeAt(index); + hash2 = Math.imul(hash2, 16777619); + } + return (hash2 >>> 0).toString(36).slice(0, 6).padStart(6, "0"); + } + function phaseObjectiveFor(frameObjective) { + return `jsb_${projectHash(frameObjective)}_p`; + } + function defaultRuntimeNames(projectName) { + const hash2 = projectHash(projectName); + return { + frameObjective: `jsb_${hash2}_f`, + modeObjective: `jsb_${hash2}_m`, + maxFrameObjective: `jsb_${hash2}_x`, + playingTag: `jsb_${hash2}_playing` + }; + } + + // src/export-animation-settings.ts + var PROPERTY_NAME2 = "display_anim_export_settings"; + var settingsProperty2 = null; + function isOutputMode(value) { + return ["both_default", "both_separate", "resource_only", "datapack_only"].includes( + String(value) + ); + } + function isWriteMode(value) { + return value === "create" || value === "insert"; + } + function normalizeAnimationFps(value) { + const numeric = typeof value === "number" ? value : Number(value); + return Math.min( + MAX_EXPORT_FPS, + Math.max(1, Math.round(Number.isFinite(numeric) ? numeric : MAX_EXPORT_FPS)) + ); + } + function storedSettings() { + if (!Project) return null; + const value = Project[PROPERTY_NAME2]; + if (!value || value.version !== 2 && value.version !== 3 && value.version !== 4 && value.version !== 5 && value.version !== 6 && value.version !== 7 || !Array.isArray(value.selectedAnimationUuids) || typeof value.defaultAnimationUuid !== "string" || typeof value.packName !== "string" || typeof value.projectName !== "string" || !isOutputMode(value.outputMode) || !isWriteMode(value.writeMode) || typeof value.baseItem !== "string" || typeof value.displayName !== "string" || typeof value.frameObjective !== "string" || typeof value.modeObjective !== "string" || typeof value.maxFrameObjective !== "string" || typeof value.playingTag !== "string") { + return null; + } + return { + version: 7, + selectedAnimationUuids: value.selectedAnimationUuids.filter( + (uuid) => typeof uuid === "string" + ), + defaultAnimationUuid: value.defaultAnimationUuid, + packName: value.packName, + projectName: value.projectName, + outputMode: value.outputMode, + writeMode: value.writeMode, + baseItem: value.baseItem, + displayName: value.displayName, + frameObjective: value.frameObjective, + modeObjective: value.modeObjective, + maxFrameObjective: value.maxFrameObjective, + playingTag: value.playingTag, + debugEnabled: value.debugEnabled === true, + handRenderingEnabled: value.handRenderingEnabled === true, + handRigRootUuid: typeof value.handRigRootUuid === "string" ? value.handRigRootUuid : void 0, + handLeftGroupUuid: typeof value.handLeftGroupUuid === "string" ? value.handLeftGroupUuid : void 0, + handRightGroupUuid: typeof value.handRightGroupUuid === "string" ? value.handRightGroupUuid : void 0, + handPreviewTextureUuid: typeof value.handPreviewTextureUuid === "string" ? value.handPreviewTextureUuid : void 0, + exactBoundsOnExport: value.exactBoundsOnExport !== false, + animationFps: normalizeAnimationFps(value.animationFps), + sharedRoot: typeof value.sharedRoot === "string" ? value.sharedRoot : "", + resourcePackFolder: typeof value.resourcePackFolder === "string" ? value.resourcePackFolder : "", + datapackFolder: typeof value.datapackFolder === "string" ? value.datapackFolder : "" + }; + } + function registerExportAnimationSettingsProperty() { + if (ModelProject.properties?.[PROPERTY_NAME2]) return; + settingsProperty2 = new Property(ModelProject, "object", PROPERTY_NAME2, { + default: {}, + exposed: false, + label: tr("dap.export.property.name"), + description: tr("dap.export.property.description") + }); + } + function unregisterExportAnimationSettingsProperty() { + settingsProperty2?.delete(); + settingsProperty2 = null; + } + function initialExportSettings(animations) { + const projectName = sanitizeProjectName(Project?.name ?? "", "display_animation"); + const runtime = defaultRuntimeNames(projectName); + const stored = storedSettings(); + const available = new Set(animations.map((animation) => animation.uuid)); + const selected = stored?.selectedAnimationUuids.filter((uuid) => available.has(uuid)) ?? []; + const fallback = Animation.selected ?? animations[0] ?? null; + if (!stored && fallback) selected.push(fallback.uuid); + const selectedSet = new Set(selected); + const defaultAnimationUuid = (stored?.defaultAnimationUuid && selectedSet.has(stored.defaultAnimationUuid) ? stored.defaultAnimationUuid : Animation.selected && selectedSet.has(Animation.selected.uuid) ? Animation.selected.uuid : selected[0]) ?? ""; + return { + version: 7, + selectedAnimationUuids: selected, + defaultAnimationUuid, + packName: stored?.packName || projectName, + projectName: stored?.projectName || projectName, + outputMode: stored?.outputMode ?? "both_default", + writeMode: stored?.writeMode ?? "create", + baseItem: stored?.baseItem || "minecraft:potion", + displayName: stored?.displayName || Project?.name?.trim() || projectName, + frameObjective: stored?.frameObjective || runtime.frameObjective, + modeObjective: stored?.modeObjective || runtime.modeObjective, + maxFrameObjective: stored?.maxFrameObjective || runtime.maxFrameObjective, + playingTag: stored?.playingTag || runtime.playingTag, + debugEnabled: stored?.debugEnabled === true, + handRenderingEnabled: stored?.handRenderingEnabled === true, + handRigRootUuid: stored?.handRigRootUuid, + handLeftGroupUuid: stored?.handLeftGroupUuid, + handRightGroupUuid: stored?.handRightGroupUuid, + handPreviewTextureUuid: stored?.handPreviewTextureUuid, + exactBoundsOnExport: stored?.exactBoundsOnExport !== false, + animationFps: stored?.animationFps ?? 20, + sharedRoot: stored?.sharedRoot ?? "", + resourcePackFolder: stored?.resourcePackFolder ?? "", + datapackFolder: stored?.datapackFolder ?? "" + }; + } + function rememberExportSettings(settings) { + if (!Project) return; + Project[PROPERTY_NAME2] = { + version: 7, + ...settings, + selectedAnimationUuids: [...settings.selectedAnimationUuids], + debugEnabled: settings.debugEnabled === true, + handRenderingEnabled: settings.handRenderingEnabled === true + }; + Project.saved = false; + } + function rememberExportSettingsDraft(settings) { + if (!Project) return; + Project[PROPERTY_NAME2] = { + ...settings, + version: 7, + selectedAnimationUuids: [...settings.selectedAnimationUuids], + debugEnabled: settings.debugEnabled === true, + handRenderingEnabled: settings.handRenderingEnabled === true + }; + Project.saved = false; + } + function getProjectAnimationFps() { + return storedSettings()?.animationFps ?? 20; + } + function setProjectAnimationFps(value) { + const fps = normalizeAnimationFps(value); + const settings = initialExportSettings(Animation.all); + settings.animationFps = fps; + rememberExportSettingsDraft(settings); + return fps; + } + // src/playback.ts var lowFpsPreview = false; + var previewLooping = false; var onTick = () => { }; var listenersRegistered = false; var quantizedPreviewInProgress = false; var previewPlaybackTimer = null; + var playbackIntent = false; + var pauseResolutionToken = 0; + var lastPlaybackTime = 0; + var lastPlaybackAnimationUuid = ""; + var originalLoopToggleValue = null; + function firstMinecraftFrameTime(animation) { + return Math.min(1 / getProjectAnimationFps(), animation.length); + } function getAnimation() { return Animation.selected ?? Animation.all[0] ?? null; } function quantize(time) { if (!lowFpsPreview) return time; - const step = Timeline.getStep(); + const step = 1 / getProjectAnimationFps(); if (!step || step <= 0) return time; const epsilon = step * 1e-7; return Math.floor((time + epsilon) / step) * step; @@ -328,6 +1284,17 @@ function previewPlaybackAllowed() { return Modes.selected.id !== "display" || isCurrentDisplayAnimationEnabled(); } + function renderAtTimePreservingClock(time) { + const rawTime = Timeline.time; + quantizedPreviewInProgress = true; + try { + Timeline.time = time; + Animator.preview(true); + } finally { + Timeline.time = rawTime; + quantizedPreviewInProgress = false; + } + } function stopPreviewPlaybackDriver() { if (previewPlaybackTimer !== null) { clearInterval(previewPlaybackTimer); @@ -335,14 +1302,14 @@ } } function drivePreviewPlayback() { - if (!Timeline.playing || !usesPreviewPlaybackDriver() || !previewPlaybackAllowed()) { + if (!Timeline.playing || !usesPreviewPlaybackDriver()) { stopPreviewPlaybackDriver(); return; } Timeline.loop(); } function syncPreviewPlaybackDriver() { - if (Timeline.playing && usesPreviewPlaybackDriver() && previewPlaybackAllowed()) { + if (Timeline.playing && usesPreviewPlaybackDriver()) { if (previewPlaybackTimer === null) { previewPlaybackTimer = setInterval(drivePreviewPlayback, 16); } @@ -352,29 +1319,50 @@ } function enforceCurrentDisplayAnimationPolicy() { if (Modes.selected.id !== "display" || isCurrentDisplayAnimationEnabled()) { + if (Modes.selected.id === "display") Animator.preview(); syncPreviewPlaybackDriver(); report(); return; } - if (Timeline.playing) Timeline.pause(); - seekTo(0); - stopPreviewPlaybackDriver(); + renderAtTimePreservingClock(0); + syncPreviewPlaybackDriver(); + report(); } function handleDisplayFrame() { if (quantizedPreviewInProgress) return; const animation = getAnimation(); if (!animation) return; const rawTime = Timeline.time; + const animationChanged = lastPlaybackAnimationUuid !== animation.uuid; + if (animationChanged) { + lastPlaybackAnimationUuid = animation.uuid; + lastPlaybackTime = rawTime; + } else if (playbackIntent && rawTime + 1e-7 < lastPlaybackTime) { + if (!previewLooping) { + playbackIntent = false; + Timeline.pause(); + Timeline.setTime(animation.length); + Animator.preview(); + lastPlaybackTime = animation.length; + onTick(animation.length, animation.length, false); + return; + } + const restartTime = firstMinecraftFrameTime(animation); + Timeline.setTime(restartTime); + lastPlaybackTime = restartTime; + Animator.preview(); + onTick(restartTime, animation.length, true); + return; + } + lastPlaybackTime = rawTime; + if (Modes.selected.id === "display" && !isCurrentDisplayAnimationEnabled()) { + renderAtTimePreservingClock(0); + report(rawTime); + return; + } const displayTime = Math.min(quantize(rawTime), animation.length); if (lowFpsPreview && Timeline.playing && Math.abs(displayTime - rawTime) > 1e-8) { - quantizedPreviewInProgress = true; - try { - Timeline.time = displayTime; - Animator.preview(true); - } finally { - Timeline.time = rawTime; - quantizedPreviewInProgress = false; - } + renderAtTimePreservingClock(displayTime); } onTick(displayTime, animation.length, Timeline.playing); } @@ -387,16 +1375,48 @@ ); return; } + const animation = getAnimation(); + if (animation && !previewLooping && Timeline.time >= animation.length - 1e-7) { + const restartTime = firstMinecraftFrameTime(animation); + Timeline.setTime(restartTime); + lastPlaybackAnimationUuid = animation.uuid; + lastPlaybackTime = restartTime; + Animator.preview(); + } + playbackIntent = true; + pauseResolutionToken++; syncPreviewPlaybackDriver(); report(); } function handleTimelinePause() { stopPreviewPlaybackDriver(); - if (lowFpsPreview) { - seekTo(Timeline.time); - } else { - report(); - } + const token = ++pauseResolutionToken; + const pausedMode = Modes.selected.id; + const shouldResumeAcrossModeChange = playbackIntent && pausedMode === "animate"; + setTimeout(() => { + if (token !== pauseResolutionToken) return; + if (shouldResumeAcrossModeChange && Modes.selected.id === "display") { + Timeline.start(); + return; + } + playbackIntent = false; + if (lowFpsPreview) { + const animation = getAnimation(); + if (animation && Timeline.time >= animation.length - 1e-7) { + const displayTime = Math.min(quantize(animation.length), animation.length); + renderAtTimePreservingClock(displayTime); + onTick(displayTime, animation.length, false); + } else { + seekTo(Timeline.time); + } + } else report(); + }, 0); + } + function handleAnimationSelect() { + const animation = getAnimation(); + lastPlaybackAnimationUuid = animation?.uuid ?? ""; + lastPlaybackTime = Timeline.time; + report(); } function handleModeSelect() { enforceCurrentDisplayAnimationPolicy(); @@ -407,7 +1427,11 @@ Blockbench.on("timeline_play", handleTimelinePlay); Blockbench.on("timeline_pause", handleTimelinePause); Blockbench.on("select_mode", handleModeSelect); + Blockbench.on("select_animation", handleAnimationSelect); listenersRegistered = true; + previewLooping = false; + originalLoopToggleValue = BarItems.looped_animation_playback.value; + BarItems.looped_animation_playback.set(false); syncPreviewPlaybackDriver(); } function disposePlaybackSync() { @@ -416,15 +1440,21 @@ Blockbench.removeListener("timeline_play", handleTimelinePlay); Blockbench.removeListener("timeline_pause", handleTimelinePause); Blockbench.removeListener("select_mode", handleModeSelect); + Blockbench.removeListener("select_animation", handleAnimationSelect); stopPreviewPlaybackDriver(); listenersRegistered = false; onTick = () => { }; + if (originalLoopToggleValue !== null) { + BarItems.looped_animation_playback.set(originalLoopToggleValue); + originalLoopToggleValue = null; + } } function isLooping() { - return BarItems.looped_animation_playback.value; + return previewLooping; } function setLooping(value) { + previewLooping = value; BarItems.looped_animation_playback.set(value); } function isLowFpsPreview() { @@ -438,6 +1468,17 @@ seekTo(Timeline.time); } } + function getPreviewFps() { + return getProjectAnimationFps(); + } + function setPreviewFps(value) { + const previewFps = setProjectAnimationFps(value); + if (lowFpsPreview) { + if (Timeline.playing) handleDisplayFrame(); + else seekTo(Timeline.time); + } + return previewFps; + } function setTickCallback(cb) { onTick = cb; report(); @@ -451,6 +1492,23 @@ Animator.preview(); onTick(displayTime, animation.length, Timeline.playing); } + function selectPreviewAnimation(uuid) { + const animation = Animation.all.find((item) => item.uuid === uuid) ?? null; + if (!animation) return null; + animation.select(); + const startTime = firstMinecraftFrameTime(animation); + lastPlaybackAnimationUuid = animation.uuid; + lastPlaybackTime = startTime; + Timeline.setTime(startTime); + if (Modes.selected.id === "display" && !isCurrentDisplayAnimationEnabled()) { + renderAtTimePreservingClock(0); + } else { + Animator.preview(); + } + syncPreviewPlaybackDriver(); + onTick(startTime, animation.length, Timeline.playing); + return animation; + } function togglePlay() { if (!getAnimation()) return; if (Modes.selected.id === "display" && !isCurrentDisplayAnimationEnabled()) { @@ -474,7 +1532,7 @@ if (animation) { stop(); animation.select(); - seekTo(0); + seekTo(firstMinecraftFrameTime(animation)); } return animation; } @@ -502,16 +1560,46 @@ return DisplayMode.display_slot; } + // src/ui-dom.ts + function el(tag, text) { + const node = document.createElement(tag); + if (text) node.innerText = text; + return node; + } + function escapeHtml(value) { + return value.replace(/[&<>"']/g, (character) => ({ + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'" + })[character]); + } + function applyFocusHighlight(control) { + const focusable = control; + focusable.onfocus = () => { + control.style.borderColor = "var(--color-accent)"; + }; + focusable.onblur = () => { + control.style.borderColor = "var(--color-border)"; + }; + } + // src/control-panel.ts - var panel = null; + var panel2 = null; var slotSelectEl = null; var sliderEl = null; var timeLabelEl = null; var playButtonEl = null; var animatedCheckboxEl = null; + var animationSelectEl = null; var animationSwitchRowEl = null; var slotMonitorTimer = null; var observedSlot = ""; + var observedAnimationUuid = ""; + var observedAnimationList = ""; + var lastTransportPlaying = null; + var lastTransportLength = Number.NaN; var SLOT_LABELS = { thirdperson_righthand: "dap.slot.thirdperson_righthand", thirdperson_lefthand: "dap.slot.thirdperson_lefthand", @@ -527,17 +1615,31 @@ function formatTime(t) { return t.toFixed(2) + "s"; } + function styleInputControl(input) { + input.style.background = "var(--color-back)"; + input.style.color = "var(--color-text)"; + input.style.border = "1px solid var(--color-border)"; + input.style.borderRadius = "0"; + input.style.padding = "4px 7px"; + input.style.boxSizing = "border-box"; + input.style.outline = "none"; + applyFocusHighlight(input); + } function updateControlsUI(time, length, playing) { syncDisplayControls(); if (sliderEl) { - sliderEl.max = String(length); + if (length !== lastTransportLength) { + sliderEl.max = String(length); + lastTransportLength = length; + } sliderEl.value = String(time); } if (timeLabelEl) { timeLabelEl.innerText = `${formatTime(time)} / ${formatTime(length)}`; } - if (playButtonEl) { + if (playButtonEl && playing !== lastTransportPlaying) { playButtonEl.innerHTML = `${playing ? "pause" : "play_arrow"}`; + lastTransportPlaying = playing; } } function refreshModeVisibility() { @@ -564,9 +1666,63 @@ refreshPlayButtonState(); enforceCurrentDisplayAnimationPolicy(); } + function syncAnimationControl() { + const listSignature = Animation.all.map((animation2) => `${animation2.uuid}\0${animation2.name}`).join(""); + if (animationSelectEl && listSignature !== observedAnimationList) { + animationSelectEl.innerHTML = ""; + for (const animation2 of Animation.all) { + const option = document.createElement("option"); + option.value = animation2.uuid; + option.innerText = animation2.name; + animationSelectEl.appendChild(option); + } + observedAnimationList = listSignature; + } + const selectedUuid = Animation.selected?.uuid ?? ""; + if (selectedUuid === observedAnimationUuid) return; + observedAnimationUuid = selectedUuid; + if (animationSelectEl) animationSelectEl.value = selectedUuid; + const animation = Animation.selected; + if (animation) updateControlsUI(Math.min(Timeline.time, animation.length), animation.length, Timeline.playing); + } function startSlotMonitor() { if (slotMonitorTimer !== null) return; - slotMonitorTimer = setInterval(() => syncDisplayControls(), 150); + slotMonitorTimer = setInterval(() => { + syncDisplayControls(); + syncAnimationControl(); + }, 150); + } + function buildAnimationPicker(container) { + const row = document.createElement("div"); + row.style.display = "flex"; + row.style.alignItems = "center"; + row.style.gap = "6px"; + row.style.padding = "4px 0"; + row.style.flexWrap = "wrap"; + const label = document.createElement("span"); + label.innerText = tr("dap.panel.animation"); + label.style.fontSize = "inherit"; + label.style.whiteSpace = "nowrap"; + const select = document.createElement("select"); + select.style.flex = "1 1 150px"; + select.style.minWidth = "0"; + for (const animation of Animation.all) { + const option = document.createElement("option"); + option.value = animation.uuid; + option.innerText = animation.name; + select.appendChild(option); + } + observedAnimationList = Animation.all.map((animation) => `${animation.uuid}\0${animation.name}`).join(""); + select.value = Animation.selected?.uuid ?? ""; + observedAnimationUuid = select.value; + select.onchange = (event) => { + const animation = selectPreviewAnimation(event.target.value); + observedAnimationUuid = animation?.uuid ?? ""; + }; + animationSelectEl = select; + row.appendChild(label); + row.appendChild(select); + container.appendChild(row); } function stopSlotMonitor() { if (slotMonitorTimer === null) return; @@ -582,7 +1738,7 @@ row.style.flexWrap = "wrap"; const label = document.createElement("span"); label.innerText = tr("dap.panel.slot"); - label.style.fontSize = "11px"; + label.style.fontSize = "inherit"; label.style.whiteSpace = "nowrap"; label.style.flex = "0 0 auto"; const select = document.createElement("select"); @@ -625,7 +1781,7 @@ animatedCheckboxEl = animated; const animatedLabel = document.createElement("span"); animatedLabel.innerText = tr("dap.panel.animate"); - animatedLabel.style.fontSize = "11px"; + animatedLabel.style.fontSize = "inherit"; animatedLabel.style.whiteSpace = "nowrap"; row.appendChild(animated); row.appendChild(animatedLabel); @@ -644,7 +1800,8 @@ buttonGroup.style.display = "flex"; buttonGroup.style.alignItems = "center"; buttonGroup.style.gap = "6px"; - buttonGroup.style.flex = "0 0 auto"; + buttonGroup.style.flex = "1 1 100%"; + buttonGroup.style.flexWrap = "wrap"; const playButton = document.createElement("button"); playButton.innerHTML = 'play_arrow'; playButton.title = tr("dap.panel.play"); @@ -671,6 +1828,27 @@ setLowFpsPreview(!isLowFpsPreview()); lowFpsButton.style.opacity = isLowFpsPreview() ? "1" : "0.4"; }; + const fpsGroup = document.createElement("label"); + fpsGroup.style.display = "flex"; + fpsGroup.style.alignItems = "center"; + fpsGroup.style.gap = "4px"; + fpsGroup.style.fontSize = "inherit"; + fpsGroup.style.whiteSpace = "nowrap"; + fpsGroup.style.flex = "1 1 118px"; + fpsGroup.innerText = tr("dap.panel.preview_fps"); + const fpsInput = document.createElement("input"); + fpsInput.type = "number"; + fpsInput.min = "1"; + fpsInput.max = "20"; + fpsInput.step = "1"; + fpsInput.value = String(getPreviewFps()); + fpsInput.title = tr("dap.panel.preview_fps_hint"); + fpsInput.style.width = "48px"; + styleInputControl(fpsInput); + fpsInput.onchange = () => { + fpsInput.value = String(setPreviewFps(parseFloat(fpsInput.value))); + }; + fpsGroup.appendChild(fpsInput); const scrubGroup = document.createElement("div"); scrubGroup.style.display = "flex"; scrubGroup.style.alignItems = "center"; @@ -692,7 +1870,7 @@ }; sliderEl = slider; const timeLabel = document.createElement("span"); - timeLabel.style.fontSize = "11px"; + timeLabel.style.fontSize = "inherit"; timeLabel.style.textAlign = "right"; timeLabel.style.whiteSpace = "nowrap"; timeLabel.style.flex = "0 0 auto"; @@ -700,6 +1878,7 @@ buttonGroup.appendChild(playButton); buttonGroup.appendChild(loopButton); buttonGroup.appendChild(lowFpsButton); + buttonGroup.appendChild(fpsGroup); scrubGroup.appendChild(slider); scrubGroup.appendChild(timeLabel); bar.appendChild(buttonGroup); @@ -708,7 +1887,8 @@ } function openControlPanel() { setTickCallback(updateControlsUI); - if (panel) { + if (panel2) { + panel2.fold(false); const animation2 = selectAnimationAndReset(); syncDisplayControls(true); startSlotMonitor(); @@ -722,17 +1902,19 @@ wrapper.style.padding = "0 4px 4px"; wrapper.style.minWidth = "0"; buildSlotPicker(wrapper); + buildAnimationPicker(wrapper); buildAnimationSwitch(wrapper); buildTransportControls(wrapper); - panel = new Panel("display_anim_preview_controls", { + panel2 = new Panel("display_anim_preview_controls", { name: tr("dap.panel.name"), icon: "movie", growable: true, resizable: true, // Leave enough height for a third wrapped row. - default_position: { slot: "left_bar", height: 165, width: 320 } + default_position: { slot: "left_bar", height: 205, width: 340 } }); - panel.node.appendChild(wrapper); + panel2.node.appendChild(wrapper); + panel2.fold(false); const animation = selectAnimationAndReset(); syncDisplayControls(true); startSlotMonitor(); @@ -745,49 +1927,620 @@ } function disposeControlPanel() { closeControlPanel(); - panel?.delete(); - panel = null; + panel2?.delete(); + panel2 = null; slotSelectEl = null; sliderEl = null; timeLabelEl = null; playButtonEl = null; animatedCheckboxEl = null; + animationSelectEl = null; animationSwitchRowEl = null; observedSlot = ""; + observedAnimationUuid = ""; + observedAnimationList = ""; + lastTransportPlaying = null; + lastTransportLength = Number.NaN; } - // src/display-snapshot.ts - function cloneCompiledDisplay(display) { - return display ? JSON.parse(JSON.stringify(display)) : void 0; + // src/bounds-cache.ts + var projectCache = /* @__PURE__ */ new WeakMap(); + function stable(value) { + if (value === null || typeof value !== "object") return JSON.stringify(value); + if (Array.isArray(value)) return `[${value.map(stable).join(",")}]`; + const record = value; + return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stable(record[key])}`).join(",")}}`; } - function applyCompiledDisplaySnapshot(json2, display) { - if (!display) return json2; - const compiled = JSON.parse(json2); - compiled.display = display; - return JSON.stringify(compiled); + function hash(value) { + let result = 2166136261; + for (let index = 0; index < value.length; index++) { + result ^= value.charCodeAt(index); + result = Math.imul(result, 16777619); + } + return (result >>> 0).toString(36); } - - // src/java-block-codec.ts - function isCompilingCodec(value) { - return Boolean( - value && typeof value.compile === "function" - ); + function nodeParentUuid(node) { + return node.parent && node.parent !== "root" ? node.parent.uuid : "root"; } - function resolveJavaBlockCodec() { - const candidates = [ - typeof Format !== "undefined" ? Format?.codec : void 0, - typeof Formats !== "undefined" ? Formats.java_block?.codec : void 0, - typeof Codecs !== "undefined" ? Codecs.java_block : void 0 - ]; - const codec = candidates.find(isCompilingCodec); - if (!codec) { - throw new Error( - "Blockbench's Java block/item model compiler is unavailable. Reload Blockbench and try again." - ); - } - return codec; + function modelBoundsFingerprint() { + const elements = Outliner.elements.map((element) => ({ + uuid: element.uuid, + type: element.constructor?.name ?? "element", + parent: nodeParentUuid(element), + from: element.from, + to: element.to, + origin: element.origin, + rotation: element.rotation, + inflate: element.inflate ?? 0, + export: element.export !== false + })).sort((left, right) => left.uuid.localeCompare(right.uuid)); + const groups = Group.all.map((group) => ({ + uuid: group.uuid, + parent: nodeParentUuid(group), + children: group.children.map((child) => child.uuid), + origin: group.origin, + rotation: group.rotation, + export: group.export !== false + })).sort((left, right) => left.uuid.localeCompare(right.uuid)); + return hash(stable({ format: Format.id, elements, groups })); } - + function keyframeSnapshot(keyframe) { + const value = keyframe; + const dataPoints = (keyframe.data_points ?? []).map((point) => Object.fromEntries( + Object.entries(point).filter(([key, item]) => key !== "keyframe" && (item === null || ["string", "number", "boolean"].includes(typeof item) || Array.isArray(item) && item.every((entry) => entry === null || ["string", "number", "boolean"].includes(typeof entry)))) + )); + return { + time: keyframe.time, + channel: keyframe.channel, + interpolation: keyframe.interpolation, + // KeyframeDataPoint refers back to its keyframe; retain serializable user data only. + data_points: dataPoints, + easing: value.easing, + easingArgs: value.easingArgs, + bezier_left_time: keyframe.bezier_left_time, + bezier_left_value: keyframe.bezier_left_value, + bezier_right_time: keyframe.bezier_right_time, + bezier_right_value: keyframe.bezier_right_value + }; + } + function animationBoundsFingerprint(animation, modelFingerprint = modelBoundsFingerprint()) { + const animators = Object.entries(animation.animators ?? {}).map(([uuid, animator]) => ({ + uuid, + keyframes: (animator?.keyframes ?? []).map(keyframeSnapshot) + })).sort((left, right) => left.uuid.localeCompare(right.uuid)); + return hash(stable({ + modelFingerprint, + uuid: animation.uuid, + name: animation.name, + length: animation.length, + snapping: animation.snapping, + blendWeight: animation.blend_weight ?? "", + animators + })); + } + function cacheKey(mode, animationUuid) { + return `${mode}:${animationUuid}`; + } + function rememberBoundsDetection(project, record) { + let records = projectCache.get(project); + if (!records) { + records = /* @__PURE__ */ new Map(); + projectCache.set(project, records); + } + records.set(cacheKey(record.mode, record.animationUuid), record); + } + function validBoundsDetection(project, animation, mode, modelFingerprint = modelBoundsFingerprint()) { + const record = projectCache.get(project)?.get(cacheKey(mode, animation.uuid)) ?? null; + if (!record) return null; + return record.fps === getProjectAnimationFps() && record.fingerprint === animationBoundsFingerprint(animation, modelFingerprint) ? record : null; + } + function detectionStatus(project, animation, modelFingerprint = modelBoundsFingerprint()) { + const records = projectCache.get(project); + const quickStored = records?.get(cacheKey("quick", animation.uuid)) ?? null; + const exactStored = records?.get(cacheKey("exact", animation.uuid)) ?? null; + const fingerprint = animationBoundsFingerprint(animation, modelFingerprint); + const quick = quickStored?.fingerprint === fingerprint ? quickStored : null; + const exact = exactStored?.fingerprint === fingerprint ? exactStored : null; + return { quick, exact, stale: Boolean((quickStored || exactStored) && !quick && !exact) }; + } + + // src/bounds-report.ts + function summarizeOutOfBoundsByFrame(hits) { + const byFrame = /* @__PURE__ */ new Map(); + for (const hit of hits) { + const list = byFrame.get(hit.frame); + if (list) { + list.push(hit); + } else { + byFrame.set(hit.frame, [hit]); + } + } + const frames = [...byFrame.keys()].sort((a, b) => a - b); + return frames.map((frame2) => { + const frameHits = byFrame.get(frame2) ?? []; + const worst = frameHits.reduce( + (acc, hit) => Math.abs(hit.value) > Math.abs(acc.value) ? hit : acc + ); + const names = [...new Set(frameHits.map((hit) => hit.elementName))]; + const nameList = names.length > 2 ? tr("dap.bounds.parts_many", { + names: names.slice(0, 2).join(", "), + count: names.length + }) : names.join(", "); + return { frame: frame2, description: tr("dap.bounds.frame", { + frame: frame2, + parts: nameList, + field: worst.field, + axis: worst.axis, + value: worst.value.toFixed(2) + }) }; + }); + } + function describeOutOfBounds(hits) { + if (!hits.length) return null; + const lines2 = summarizeOutOfBoundsByFrame(hits).map((item) => item.description); + const shown = lines2.slice(0, 12); + const omitted = lines2.length - shown.length; + const parts = [ + tr("dap.bounds.summary", { frames: lines2.length }), + "", + tr("dap.bounds.guidance"), + "", + ...shown + ]; + if (omitted > 0) { + parts.push(tr("dap.bounds.omitted", { count: omitted })); + } + return parts.join("\n"); + } + + // src/bounds-task.ts + var BoundsTaskCancelledError = class extends Error { + constructor() { + super("Bounds check cancelled"); + this.name = "BoundsTaskCancelledError"; + } + }; + function assertBoundsTaskActive(control) { + if (control.isCancelled()) throw new BoundsTaskCancelledError(); + } + function yieldBoundsTask() { + return new Promise((resolve) => setTimeout(resolve, 0)); + } + + // src/display-snapshot.ts + function cloneCompiledDisplay(display) { + return display ? JSON.parse(JSON.stringify(display)) : void 0; + } + function applyCompiledDisplaySnapshot(model, display) { + if (!display) return model; + model.display = display; + return model; + } + + // src/assets/missing.png + var missing_default = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXNSR0IArs4c6QAAAHpJREFUOI21kjEKACEMBNdD/IGV/39abMwL0uSKgIiCyRVnETFsMrJsUlWEDxFlu0opIuJWZn4ABNUiAiADuChaawDGGNYB4BDs6yvhuRNsYJ3/n5DXd611c9khbPt8wtw33YwSps5x6dSdhKSq8Sz13r9niYiYOZ7wF+cqCUwV0Ir/AAAAAElFTkSuQmCC"; + + // src/hand-rig.ts + var ROLE_PROPERTY = "display_anim_hand_role"; + var GENERATED_PROPERTY = "display_anim_hand_generated"; + var PREVIEW_TEXTURE_PROPERTY = "display_anim_hand_preview_texture"; + var ROOT_ROLE = "root"; + var LEFT_ROLE = "left_arm"; + var RIGHT_ROLE = "right_arm"; + var DEFAULT_TEXTURE_NAME = "missing.png"; + var groupRoleProperty = null; + var groupGeneratedProperty = null; + var cubeRoleProperty = null; + var cubeGeneratedProperty = null; + var texturePreviewProperty = null; + function registerHandRigProperties() { + if (!Group.properties?.[ROLE_PROPERTY]) { + groupRoleProperty = new Property(Group, "string", ROLE_PROPERTY, { default: "" }); + } + if (!Group.properties?.[GENERATED_PROPERTY]) { + groupGeneratedProperty = new Property(Group, "boolean", GENERATED_PROPERTY, { default: false }); + } + if (!Cube.properties?.[ROLE_PROPERTY]) { + cubeRoleProperty = new Property(Cube, "string", ROLE_PROPERTY, { default: "" }); + } + if (!Cube.properties?.[GENERATED_PROPERTY]) { + cubeGeneratedProperty = new Property(Cube, "boolean", GENERATED_PROPERTY, { default: false }); + } + if (!Texture.properties?.[PREVIEW_TEXTURE_PROPERTY]) { + texturePreviewProperty = new Property(Texture, "boolean", PREVIEW_TEXTURE_PROPERTY, { default: false }); + } + } + function unregisterHandRigProperties() { + for (const property of [groupRoleProperty, groupGeneratedProperty, cubeRoleProperty, cubeGeneratedProperty, texturePreviewProperty]) { + property?.delete(); + } + groupRoleProperty = groupGeneratedProperty = cubeRoleProperty = cubeGeneratedProperty = null; + texturePreviewProperty = null; + } + function roleOf(node) { + return String(node[ROLE_PROPERTY] ?? ""); + } + function setRole(node, role, generated) { + const record = node; + record[ROLE_PROPERTY] = role; + record[GENERATED_PROPERTY] = generated; + } + function isGenerated(node) { + return node[GENERATED_PROPERTY] === true; + } + function findGroup(uuid, role, compatibleName) { + return (uuid ? Group.all.find((group) => group.uuid === uuid) : void 0) ?? Group.all.find((group) => roleOf(group) === role) ?? Group.all.find((group) => group.name.toLowerCase() === compatibleName) ?? null; + } + function resolveHandRig(settings) { + const left = findGroup(settings.handLeftGroupUuid, LEFT_ROLE, "lefthand"); + const right = findGroup(settings.handRightGroupUuid, RIGHT_ROLE, "righthand"); + if (!left || !right) return null; + const root = findGroup(settings.handRigRootUuid, ROOT_ROLE, "dap_playerhands"); + return { root, left, right }; + } + function resolveProjectHandRig() { + const settings = Project?.display_anim_export_settings; + if (!settings?.handRenderingEnabled) return null; + return resolveHandRig(settings); + } + function assertNoHandScaleKeyframes(animation, rig) { + for (const hand of [rig.left, rig.right]) { + let group = hand; + while (group && group !== "root") { + const animator = animation.animators?.[group.uuid]; + const hasScale = animator?.keyframes?.some((keyframe) => keyframe.channel === "scale") === true; + if (hasScale) { + throw new Error(tr("dap.hand.scale_unsupported", { animation: animation.name, group: group.name })); + } + group = group.parent; + } + } + } + function armPlaceholder(side) { + return side === "right" ? { pivot: [14, 2, 16], from: [12, 0, 4], to: [16, 4, 16] } : { pivot: [2, 2, 16], from: [0, 0, 4], to: [4, 4, 16] }; + } + function armBoxUvOffset() { + return [0, 0, 0]; + } + function applyArmTexture(cube, texture) { + cube.applyTexture?.(texture, true); + cube.box_uv = true; + cube.uv_offset = armBoxUvOffset().slice(); + for (const faceName of Object.keys(cube.faces ?? {})) { + const face = cube.faces?.[faceName]; + if (!face) continue; + face.texture = texture.uuid; + } + } + function createArmCube(group, texture, side) { + const placeholder = armPlaceholder(side); + const cube = new Cube({ + name: `DAP_${side === "right" ? "Right" : "Left"}Arm_Skin`, + from: placeholder.from.slice(), + to: placeholder.to.slice(), + origin: placeholder.pivot.slice(), + inflate: 0, + export: false, + visibility: true, + autouv: 0 + }).init().addTo(group); + setRole(cube, `${side}_skin`, true); + applyArmTexture(cube, texture); + return cube; + } + function armCubes(group, side) { + const descendants = []; + group.forEachChild?.((child) => descendants.push(child)); + const direct = group.children ?? []; + const all = descendants.length ? descendants : direct; + const skin = all.find((node) => roleOf(node) === `${side}_skin`) ?? all.find((node) => node instanceof Cube && (node.inflate ?? 0) < 0.1) ?? null; + const stale = all.filter( + (node) => node !== skin && node instanceof Cube && (roleOf(node) === `${side}_sleeve` || roleOf(node) === "") + ); + return { skin, stale }; + } + function ensureDefaultTexture(settings) { + const existing = Texture.all.find((texture2) => texture2.uuid === settings.handPreviewTextureUuid) ?? Texture.all.find( + (texture2) => texture2[PREVIEW_TEXTURE_PROPERTY] === true + ); + if (existing) { + existing[PREVIEW_TEXTURE_PROPERTY] = true; + return existing; + } + const texture = new Texture({ name: DEFAULT_TEXTURE_NAME }).fromDataURL(missing_default).add(false); + texture[PREVIEW_TEXTURE_PROPERTY] = true; + return texture; + } + function refreshArm(group, texture, side, settings) { + setRole(group, side === "left" ? LEFT_ROLE : RIGHT_ROLE, isGenerated(group)); + if (!isGenerated(group)) return; + group.visibility = settings.handRenderingEnabled; + const cubes = armCubes(group, side); + if (cubes.skin && !isGenerated(cubes.skin)) return; + const skin = cubes.skin ?? createArmCube(group, texture, side); + setRole(skin, `${side}_skin`, true); + skin.export = false; + skin.visibility = settings.handRenderingEnabled; + if (!cubes.skin) applyArmTexture(skin, texture); + } + function ensureHandRig(settings) { + if (!Project) throw new Error(tr("dap.settings.no_project")); + const beforeGroups = Group.all.slice(); + const beforeElements = Outliner.elements.slice(); + const beforeTextures = Texture.all.slice(); + Undo.initEdit({ elements: beforeElements, groups: beforeGroups, textures: beforeTextures, outliner: true, animations: Animation.all.slice() }); + try { + const texture = ensureDefaultTexture(settings); + let root = findGroup(settings.handRigRootUuid, ROOT_ROLE, "dap_playerhands"); + let left = findGroup(settings.handLeftGroupUuid, LEFT_ROLE, "lefthand"); + let right = findGroup(settings.handRightGroupUuid, RIGHT_ROLE, "righthand"); + if (!left || !right) { + root = root ?? new Group({ name: "DAP_PlayerHands", origin: [8, 8, 8] }).init(); + setRole(root, ROOT_ROLE, true); + if (!right) { + right = new Group({ name: "DAP_RightArm", origin: armPlaceholder("right").pivot.slice() }).init().addTo(root); + setRole(right, RIGHT_ROLE, true); + } + if (!left) { + left = new Group({ name: "DAP_LeftArm", origin: armPlaceholder("left").pivot.slice() }).init().addTo(root); + setRole(left, LEFT_ROLE, true); + } + if (!right || !left) throw new Error("Unable to create player hand groups."); + setRole(right, RIGHT_ROLE, isGenerated(right)); + setRole(left, LEFT_ROLE, isGenerated(left)); + } else { + setRole(left, LEFT_ROLE, isGenerated(left)); + setRole(right, RIGHT_ROLE, isGenerated(right)); + } + refreshArm(left, texture, "left", settings); + refreshArm(right, texture, "right", settings); + settings.handRigRootUuid = root?.uuid; + settings.handLeftGroupUuid = left.uuid; + settings.handRightGroupUuid = right.uuid; + settings.handPreviewTextureUuid = texture.uuid; + Undo.finishEdit(tr("dap.hand.undo_create"), { elements: Outliner.elements.slice(), groups: Group.all.slice(), textures: Texture.all.slice(), outliner: true, animations: Animation.all.slice() }); + Canvas.updateAll(); + return { root, left, right }; + } catch (error) { + Undo.cancelEdit(true); + throw error; + } + } + function setHandRigVisibility(settings) { + const rig = resolveHandRig(settings); + if (!rig) return; + for (const [side, group] of [["left", rig.left], ["right", rig.right]]) { + if (!isGenerated(group)) continue; + group.visibility = settings.handRenderingEnabled; + const cubes = armCubes(group, side); + if (cubes.skin) cubes.skin.visibility = settings.handRenderingEnabled; + } + Canvas.updateAll(); + } + function deleteHandRig(settings) { + const rig = resolveHandRig(settings); + if (!rig) return; + Undo.initEdit({ elements: Outliner.elements.slice(), groups: Group.all.slice(), outliner: true, animations: Animation.all.slice() }); + try { + for (const group of [rig.left, rig.right]) { + if (isGenerated(group)) { + const descendants = []; + group.forEachChild?.((node) => descendants.push(node)); + if (descendants.every(isGenerated)) group.remove(); + else setRole(group, "", false); + } else setRole(group, "", false); + } + if (rig.root && isGenerated(rig.root) && rig.root.children.length === 0) rig.root.remove(); + settings.handRigRootUuid = void 0; + settings.handLeftGroupUuid = void 0; + settings.handRightGroupUuid = void 0; + Undo.finishEdit(tr("dap.hand.undo_delete"), { elements: Outliner.elements.slice(), groups: Group.all.slice(), outliner: true, animations: Animation.all.slice() }); + Canvas.updateAll(); + } catch (error) { + Undo.cancelEdit(true); + throw error; + } + } + + // src/hand-pose.ts + var IDENTITY_MATRIX = [ + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1 + ]; + function multiplyMatrices(left, right) { + const result = new Array(16).fill(0); + for (let column = 0; column < 4; column++) { + for (let row = 0; row < 4; row++) { + for (let index = 0; index < 4; index++) { + result[column * 4 + row] += left[index * 4 + row] * right[column * 4 + index]; + } + } + } + return result; + } + function invertRigidMatrix(matrix) { + if (matrix.length !== 16) throw new Error("Expected a 4\xD74 hand matrix."); + const result = IDENTITY_MATRIX.slice(); + for (let column = 0; column < 3; column++) { + for (let row = 0; row < 3; row++) result[column * 4 + row] = matrix[row * 4 + column]; + } + const x = matrix[12], y = matrix[13], z = matrix[14]; + result[12] = -(result[0] * x + result[4] * y + result[8] * z); + result[13] = -(result[1] * x + result[5] * y + result[9] * z); + result[14] = -(result[2] * x + result[6] * y + result[10] * z); + return result; + } + function relativeHandMatrix(current, bind) { + return { matrix: multiplyMatrices(current, invertRigidMatrix(bind)) }; + } + function assertRigidHandPose(pose) { + const matrix = pose?.matrix ?? IDENTITY_MATRIX; + if (matrix.length !== 16 || matrix.some((value) => !Number.isFinite(value))) { + throw new Error("Expected a finite 4\xD74 hand matrix."); + } + for (let column = 0; column < 3; column++) { + for (let other = 0; other < 3; other++) { + const dot = [0, 1, 2].reduce((sum, row) => sum + matrix[column * 4 + row] * matrix[other * 4 + row], 0); + if (Math.abs(dot - (column === other ? 1 : 0)) > 1e-4) { + throw new Error("Hand pose contains scale or shear; only position and rotation are supported."); + } + } + } + const determinant = matrix[0] * (matrix[5] * matrix[10] - matrix[9] * matrix[6]) - matrix[4] * (matrix[1] * matrix[10] - matrix[9] * matrix[2]) + matrix[8] * (matrix[1] * matrix[6] - matrix[5] * matrix[2]); + if (Math.abs(determinant - 1) > 1e-4 || [3, 7, 11].some((i) => Math.abs(matrix[i]) > 1e-4) || Math.abs(matrix[15] - 1) > 1e-4) { + throw new Error("Hand pose must be a proper rigid affine transform."); + } + return matrix; + } + var HAND_CALIBRATION = { + left: { rotation: [0, 180, 0], translation: [-20.3, 5.5, 1.7] }, + right: { rotation: [180, 0, 0], translation: [-1, 1.4, 1.5] } + }; + var HAND_MARKER_SCALE = { + left: [0.471, 0.515, 1.515], + right: [0.46629, 0.50985, 1.49985] + }; + function handSpecialTransformation() { + return { + left_rotation: [1, 0, 0, 0], + right_rotation: [0, 0, 0, 1], + scale: [1, 1, 1], + translation: [0.5, 0, 0.5] + }; + } + function degrees(value) { + return value * Math.PI / 180; + } + function eulerXyzMatrix(rotation, translation = [0, 0, 0], scale = [1, 1, 1]) { + const x = degrees(rotation[0]); + const y = degrees(rotation[1]); + const z = degrees(rotation[2]); + const a = Math.cos(x), b = Math.sin(x); + const c = Math.cos(y), d = Math.sin(y); + const e = Math.cos(z), f = Math.sin(z); + return [ + c * e * scale[0], + (a * f + b * e * d) * scale[0], + (b * f - a * e * d) * scale[0], + 0, + -c * f * scale[1], + (a * e - b * f * d) * scale[1], + (b * e + a * f * d) * scale[1], + 0, + d * scale[2], + -b * c * scale[2], + a * c * scale[2], + 0, + translation[0], + translation[1], + translation[2], + 1 + ]; + } + function radiansToDegrees(value) { + return value * 180 / Math.PI; + } + function cleanNumber(value) { + if (Math.abs(value) < 1e-10) return 0; + return Number(value.toFixed(8)); + } + function matrixEulerXyz(matrix) { + const sx = Math.hypot(matrix[0], matrix[1], matrix[2]) || 1; + const sy = Math.hypot(matrix[4], matrix[5], matrix[6]) || 1; + const sz = Math.hypot(matrix[8], matrix[9], matrix[10]) || 1; + const m00 = matrix[0] / sx; + const m01 = matrix[4] / sy; + const m11 = matrix[5] / sy; + const m21 = matrix[6] / sy; + const m02 = Math.max(-1, Math.min(1, matrix[8] / sz)); + const m12 = matrix[9] / sz; + const m22 = matrix[10] / sz; + let x; + const y = Math.asin(m02); + let z; + if (Math.abs(m02) < 0.9999999) { + x = Math.atan2(-m12, m22); + z = Math.atan2(-m01, m00); + } else { + x = Math.atan2(m21, m11); + z = 0; + } + return [x, y, z].map((value) => cleanNumber(radiansToDegrees(value))); + } + var HAND_REFERENCE_PIVOT = { + left: [2, 2, 16], + right: [14, 2, 16] + }; + function captureHandPose(side, current) { + assertRigidHandPose({ matrix: current }); + return relativeHandMatrix(current, eulerXyzMatrix([0, 0, 0], HAND_REFERENCE_PIVOT[side])); + } + function animatedHandDisplay(side, pose) { + const delta = assertRigidHandPose(pose); + const calibration = HAND_CALIBRATION[side]; + if (delta.every((value, index) => Math.abs(value - IDENTITY_MATRIX[index]) < 1e-9)) { + return { + rotation: calibration.rotation.slice(), + translation: calibration.translation.slice(), + scale: HAND_MARKER_SCALE[side].slice() + }; + } + const base = eulerXyzMatrix(calibration.rotation, calibration.translation); + const centerY = -4 * HAND_MARKER_SCALE[side][1]; + const pivotOffset = [base[4] * centerY, base[5] * centerY, base[6] * centerY + 6]; + const anchorTranslation = calibration.translation.map((value, axis) => value + pivotOffset[axis] - HAND_REFERENCE_PIVOT[side][axis]); + const anchor = eulerXyzMatrix([0, 0, 0], anchorTranslation); + const motion = multiplyMatrices(multiplyMatrices(anchor, delta), invertRigidMatrix(anchor)); + const combined = multiplyMatrices(motion, base); + return { + rotation: matrixEulerXyz(combined), + translation: [combined[12], combined[13], combined[14]].map(cleanNumber), + scale: HAND_MARKER_SCALE[side].slice() + }; + } + function previewAlignedHandDisplay(side, pose, display = {}, leftContext = false) { + const delta = assertRigidHandPose(pose); + const group = multiplyMatrices(delta, eulerXyzMatrix([0, 0, 0], HAND_REFERENCE_PIVOT[side])); + const rotation = (display.rotation ?? [0, 0, 0]).slice(); + const translation = (display.translation ?? [0, 0, 0]).slice(); + if (leftContext) { + translation[0] *= -1; + rotation[1] *= -1; + rotation[2] *= -1; + } + const displayRotation = eulerXyzMatrix(rotation); + const displayMatrix = eulerXyzMatrix(rotation, translation, display.scale ?? [1, 1, 1]); + const center = pose.center ? pose.center.map((value) => value - 8) : [0, 1, 2].map((axis) => group[12 + axis] - 6 * group[8 + axis] - 8); + const target = [0, 1, 2].map((axis) => displayMatrix[12 + axis] + center.reduce((sum, value, column) => sum + displayMatrix[column * 4 + axis] * value, 0)); + const orientation = multiplyMatrices( + multiplyMatrices(displayRotation, group), + eulerXyzMatrix(HAND_CALIBRATION[side].rotation) + ); + const offset = -4 * HAND_MARKER_SCALE[side][1]; + const resultTranslation = target.map((value, axis) => cleanNumber(value - orientation[4 + axis] * offset)); + const resultRotation = matrixEulerXyz(orientation); + if (leftContext) { + resultTranslation[0] *= -1; + resultRotation[1] *= -1; + resultRotation[2] *= -1; + } + return { rotation: resultRotation, translation: resultTranslation, scale: HAND_MARKER_SCALE[side].slice() }; + } + // src/bake.ts function snapshotCompiledDisplay() { try { @@ -803,26 +2556,22 @@ var COORDINATE_MIN = -16; var COORDINATE_MAX = 32; var AXIS_NAMES = ["x", "y", "z"]; - function applyAnimatedOffsets(node) { + function applyAnimatedOffsets(node, animation) { const offsetRotation = [0, 0, 0]; const offsetPosition = [0, 0, 0]; - for (const animation of Animator.animations) { - if (!animation.playing) continue; - const animator = animation.getBoneAnimator(node); - if (!animator) continue; - if (!(node instanceof Group)) continue; - const multiplier = animation.blend_weight ? Math.max(Animator.MolangParser.parse(animation.blend_weight), 0) : 1; - if (animator.channels.rotation) { - const rotation = animator.interpolate("rotation"); - if (rotation instanceof Array) { - offsetRotation.V3_add(rotation.map((v) => v * multiplier)); - } + const animator = animation.getBoneAnimator(node); + if (!animator || !(node instanceof Group)) return; + const multiplier = animation.blend_weight ? Math.max(Animator.MolangParser.parse(animation.blend_weight), 0) : 1; + if (animator.channels.rotation) { + const rotation = animator.interpolate("rotation"); + if (rotation instanceof Array) { + offsetRotation.V3_add(rotation.map((v) => v * multiplier)); } - if (animator.channels.position) { - const position = animator.interpolate("position"); - if (position instanceof Array) { - offsetPosition.V3_add(position.map((v) => v * multiplier)); - } + } + if (animator.channels.position) { + const position = animator.interpolate("position"); + if (position instanceof Array) { + offsetPosition.V3_add(position.map((v) => v * multiplier)); } } if (node.getTypeBehavior("rotatable") && node.rotation) { @@ -872,16 +2621,9 @@ } } } - function collectOutOfBounds(frame, json2) { + function collectOutOfBounds(frame2, model, sources) { const hits = []; - let parsed; - try { - parsed = JSON.parse(json2); - } catch (err) { - console.error(`Frame ${frame}: compiled model is not valid JSON`, err); - return hits; - } - const elements = parsed.elements ?? []; + const elements = model.elements ?? []; elements.forEach((element, elementIndex) => { const fields = [ ["from", element.from], @@ -892,12 +2634,14 @@ values.forEach((value, axis) => { if (value >= COORDINATE_MIN && value <= COORDINATE_MAX) return; hits.push({ - frame, + frame: frame2, elementIndex, elementName: element.name ?? `element ${elementIndex}`, axis: AXIS_NAMES[axis] ?? "x", field, - value + value, + sourceElementUuid: sources[elementIndex]?.elementUuid, + sourceGroupUuids: sources[elementIndex]?.groupUuids }); }); } @@ -914,25 +2658,142 @@ } return total; } - function bakeFrames(frameCount, fps, rootGroupUuid) { + function modelStructureSignature() { + const elements = Outliner.elements.map((element) => element.uuid).sort(); + const groups = Group.all.map((group) => group.uuid).sort(); + return `${elements.join(",")}|${groups.join(",")}`; + } + function safelyCancelBakeEdit(token) { + if (Undo.current_save !== token) { + throw new Error("Blockbench changed the active edit while restoring a baked frame."); + } + const previewDescriptor = Object.getOwnPropertyDescriptor(Animator, "preview"); + if (!previewDescriptor?.configurable) { + throw new Error("Blockbench does not allow a safe animation-preview restore in this version."); + } + try { + Object.defineProperty(Animator, "preview", { + configurable: true, + value: () => { + } + }); + Undo.cancelEdit(true); + } finally { + Object.defineProperty(Animator, "preview", previewDescriptor); + } + if (Undo.current_save) { + throw new Error("Blockbench did not finish restoring the baked frame."); + } + } + function readModelMatrix(group) { + const chain = []; + let current = group; + while (current && current !== "root") { + chain.unshift(current); + current = current.parent; + } + let matrix = IDENTITY_MATRIX.slice(); + for (const item of chain) { + item.mesh.updateMatrixWorld(true); + const local = item.mesh.matrix.toArray(); + if (local.length !== 16 || local.some((value) => !Number.isFinite(value))) { + throw new Error(`Invalid Blockbench model matrix for hand group ${item.name}.`); + } + matrix = multiplyMatrices(matrix, local); + } + return matrix; + } + function readHandMatrices(rig) { + const read = (side) => { + const group = rig[side]; + const pose = captureHandPose(side, readModelMatrix(group)); + const cubes = []; + group.forEachChild?.((node) => { + if (node instanceof Cube) cubes.push(node); + }); + if (!cubes.length) return pose; + const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity]; + const inverse = invertRigidMatrix(readModelMatrix(group)); + for (const cube of cubes) { + const { from, to, origin } = cube; + if (!from || !to || !origin) continue; + const local = multiplyMatrices(inverse, readModelMatrix(cube)); + for (let corner = 0; corner < 8; corner++) { + const point = [0, 1, 2].map((axis) => (corner & 1 << axis ? to[axis] : from[axis]) - origin[axis]); + for (let axis = 0; axis < 3; axis++) { + const value = local[12 + axis] + point.reduce((sum, v, column) => sum + local[column * 4 + axis] * v, 0); + min[axis] = Math.min(min[axis], value); + max[axis] = Math.max(max[axis], value); + } + } + } + const center = min.map((v, axis) => (v + max[axis]) / 2); + if (center.every(Number.isFinite)) { + const matrix = readModelMatrix(group); + pose.center = [0, 1, 2].map((axis) => matrix[12 + axis] + center.reduce((sum, v, column) => sum + matrix[column * 4 + axis] * v, 0)); + } + return pose; + }; + return { left: read("left"), right: read("right") }; + } + function* bakeFrameSteps(animation, frameCount, fps, rootGroupUuid, collectBounds = true, captureHands = false) { + if (Undo.current_save) { + throw new Error(tr("dap.bake.active_edit")); + } const frames = []; const outOfBounds = []; const originalTime = Timeline.time; - const originalAnimation = Animation.selected; - const playingStates = Animation.all.map((animation) => ({ - animation, - playing: animation.playing + const sourceAnimationUuid = animation.uuid; + const originalAnimationUuid = Animation.selected?.uuid; + const playingStates = Animation.all.map((animation2) => ({ + uuid: animation2.uuid, + playing: animation2.playing })); const originalSaved = Project?.saved; const keyframesBefore = countKeyframes(); + const structureBefore = modelStructureSignature(); const originalModeId = Modes.selected.id; const displaySnapshot = snapshotCompiledDisplay(); + const sourceGroups = /* @__PURE__ */ new Map(); + let handRig = null; + if (collectBounds) { + for (const element of Outliner.elements) { + const groupUuids = []; + let parent = element.parent; + while (parent && parent !== "root") { + if (parent instanceof Group) groupUuids.push(parent.uuid); + parent = parent.parent; + } + sourceGroups.set(element.uuid, groupUuids); + } + } try { Modes.options.animate?.select(); - originalAnimation?.select(); - for (let frame = 0; frame < frameCount; frame++) { - Timeline.setTime(frame / fps); + for (const state of playingStates) { + const current = Animation.all.find((item) => item.uuid === state.uuid); + if (current) current.playing = false; + } + const initialTarget = Animation.all.find((item) => item.uuid === sourceAnimationUuid); + if (!initialTarget) throw new Error(`Animation ${sourceAnimationUuid} is no longer available.`); + initialTarget.select(); + if (captureHands) { + handRig = resolveProjectHandRig(); + if (!handRig) throw new Error(tr("dap.hand.rig_missing")); + assertNoHandScaleKeyframes(initialTarget, handRig); + } + initialTarget.playing = true; + for (let frame2 = 0; frame2 < frameCount; frame2++) { + const targetAnimation = Animation.all.find((item) => item.uuid === sourceAnimationUuid); + if (!targetAnimation) throw new Error(`Animation ${sourceAnimationUuid} disappeared during baking.`); + if (handRig) { + handRig = resolveProjectHandRig(); + if (!handRig) throw new Error(tr("dap.hand.rig_missing")); + Canvas.updateAll(); + } + Timeline.setTime(frame2 / fps); Animator.preview(); + const currentHands = handRig ? readHandMatrices(handRig) : null; + const hands = currentHands ?? void 0; const token = Undo.initEdit({ elements: Outliner.elements.slice(), groups: Group.all.slice(), @@ -942,32 +2803,51 @@ }); try { restrictExportToRoot(rootGroupUuid); + if (handRig) { + for (const group of [handRig.left, handRig.right]) { + group.forEachChild?.((node) => { + node.export = false; + }); + } + } const animatableElements = Outliner.elements.filter( (element) => element.constructor.animator ); for (const node of [...Group.all, ...animatableElements]) { - applyAnimatedOffsets(node); + applyAnimatedOffsets(node, targetAnimation); } flattenHierarchy(); - const json2 = applyCompiledDisplaySnapshot( - resolveJavaBlockCodec().compile({ prevent_dialog: true }), - displaySnapshot + const compiledSources = collectBounds ? Outliner.elements.filter((element) => element.export !== false).map((element) => ({ + elementUuid: element.uuid, + groupUuids: sourceGroups.get(element.uuid) ?? [] + })) : []; + const compiled = JSON.parse( + resolveJavaBlockCodec().compile({ prevent_dialog: true }) ); - frames.push({ frame, json: json2 }); - outOfBounds.push(...collectOutOfBounds(frame, json2)); + const model = applyCompiledDisplaySnapshot(compiled, displaySnapshot); + frames.push({ frame: frame2, model, hands }); + if (collectBounds) { + outOfBounds.push(...collectOutOfBounds(frame2, model, compiledSources)); + } } finally { - if (Undo.current_save === token) { - Undo.cancelEdit(true); + safelyCancelBakeEdit(token); + if (modelStructureSignature() !== structureBefore) { + throw new Error("Blockbench did not restore the model hierarchy after checking a frame."); } } + yield { frame: frame2 + 1, total: frameCount }; } } finally { - Timeline.setTime(originalTime); + for (const item of Animation.all) item.selected = false; + const originalAnimation = originalAnimationUuid ? Animation.all.find((item) => item.uuid === originalAnimationUuid) ?? null : null; + Animation.selected = originalAnimation; + if (originalAnimation) originalAnimation.selected = true; for (const state of playingStates) { - state.animation.playing = state.playing; + const current = Animation.all.find((item) => item.uuid === state.uuid); + if (current) current.playing = state.playing; } - originalAnimation?.select(); Modes.options[originalModeId]?.select(); + Timeline.setTime(originalTime); Animator.preview(); if (Project && originalSaved !== void 0) { Project.saved = originalSaved; @@ -988,289 +2868,838 @@ icon: "error" }); } + if (modelStructureSignature() !== structureBefore) { + console.error("Bake rollback incomplete: model hierarchy changed during baking"); + } } return { frames, outOfBounds }; } - function frameCountFor(length, fps) { - return Math.floor(length * fps) + 1; + async function bakeFramesAsync(animation, frameCount, fps, control, onFrame, rootGroupUuid, collectBounds = true, captureHands = false) { + const generator = bakeFrameSteps(animation, frameCount, fps, rootGroupUuid, collectBounds, captureHands); + let completed = false; + try { + while (true) { + assertBoundsTaskActive(control); + const step = generator.next(); + if (step.done) { + completed = true; + return step.value; + } + onFrame?.(step.value.frame, step.value.total); + await yieldBoundsTask(); + } + } finally { + if (!completed) generator.return({ frames: [], outOfBounds: [] }); + } } - // src/bounds-report.ts - function summarizeByFrame(hits) { - const byFrame = /* @__PURE__ */ new Map(); - for (const hit of hits) { - const list = byFrame.get(hit.frame); - if (list) { - list.push(hit); - } else { - byFrame.set(hit.frame, [hit]); - } + // src/isolated-project.ts + function cloneProjectModel() { + const compiled = Codecs.project.compile({ raw: true, absolute_paths: true, editor_state: false }); + return JSON.parse(JSON.stringify(compiled)); + } + function captureSourceState() { + if (!Project) throw new Error("No active Blockbench project."); + return { + project: Project, + projectUuid: String(Project.uuid), + mode: Modes.selected.id, + timelineTime: Timeline.time, + animationUuid: Animation.selected?.uuid ?? null, + playing: Animation.all.map((animation) => ({ uuid: animation.uuid, playing: animation.playing })), + saved: Project.saved + }; + } + function restoreSourceState(state) { + if (!ModelProject.all.includes(state.project)) { + throw new Error("The source Blockbench project was closed during isolated checking."); } - const frames = [...byFrame.keys()].sort((a, b) => a - b); - return frames.map((frame) => { - const frameHits = byFrame.get(frame) ?? []; - const worst = frameHits.reduce( - (acc, hit) => Math.abs(hit.value) > Math.abs(acc.value) ? hit : acc - ); - const names = [...new Set(frameHits.map((hit) => hit.elementName))]; - const nameList = names.length > 2 ? tr("dap.bounds.parts_many", { - names: names.slice(0, 2).join(", "), - count: names.length - }) : names.join(", "); - return tr("dap.bounds.frame", { - frame, - parts: nameList, - field: worst.field, - axis: worst.axis, - value: worst.value.toFixed(2) - }); - }); + state.project.select(); + if (Project?.uuid !== state.projectUuid) { + throw new Error("Blockbench did not restore the source project after isolated checking."); + } + Modes.options[state.mode]?.select(); + for (const animation of Animation.all) animation.selected = false; + Animation.selected = state.animationUuid ? Animation.all.find((animation) => animation.uuid === state.animationUuid) ?? null : null; + if (Animation.selected) Animation.selected.selected = true; + for (const item of state.playing) { + const animation = Animation.all.find((candidate) => candidate.uuid === item.uuid); + if (animation) animation.playing = item.playing; + } + Timeline.setTime(state.timelineTime); + Animator.preview(); + Project.saved = state.saved; } - function describeOutOfBounds(hits) { - if (!hits.length) return null; - const lines2 = summarizeByFrame(hits); - const shown = lines2.slice(0, 12); - const omitted = lines2.length - shown.length; - const parts = [ - tr("dap.bounds.summary", { frames: lines2.length }), - "", - tr("dap.bounds.guidance"), - "", - ...shown - ]; - if (omitted > 0) { - parts.push(tr("dap.bounds.omitted", { count: omitted })); + async function withIsolatedProject(operation) { + const sourceState = captureSourceState(); + const snapshot = cloneProjectModel(); + const formatId = snapshot.meta?.model_format; + const format = (formatId ? Formats[formatId] : void 0) ?? sourceState.project.format ?? Formats.free; + if (!format) throw new Error(`The source project format "${formatId ?? "unknown"}" is not available.`); + const scratch = new ModelProject({ format }); + let result; + let operationError; + try { + if (!scratch.select()) throw new Error("Blockbench refused to select the isolated check project."); + Codecs.project.parse(snapshot); + scratch.name = `[JDA Check] ${snapshot.name ?? "Project"}`; + scratch.save_path = ""; + scratch.export_path = ""; + scratch.saved = true; + result = await operation(scratch, sourceState); + } catch (error) { + operationError = error; + } finally { + try { + if (ModelProject.all.includes(scratch)) await scratch.close(true); + } catch (closeError) { + console.error("Failed to close the isolated check project", closeError); + } + try { + restoreSourceState(sourceState); + } catch (restoreError) { + if (!operationError) operationError = restoreError; + else console.error("Failed to restore the source project after an isolated check", restoreError); + } } - return parts.join("\n"); + if (operationError) throw operationError; + return result; } - // src/resource-pack.ts - var RESOURCE_PACK_FORMAT = [88, 0]; - function sanitizeTextureName(name, fallbackIndex) { - const safe = name.replace(/\.png$/i, "").toLowerCase().replace(/[^a-z0-9_-]+/g, "_").replace(/^_+|_+$/g, ""); - return safe || `texture_${fallbackIndex}`; - } - function collectTextures() { - const used = /* @__PURE__ */ new Set(); - return Texture.all.map((texture, index) => { - const base = sanitizeTextureName(texture.name, index); - let name = base; - let suffix = 2; - while (used.has(name)) { - name = `${base}_${suffix++}`; + // src/exact-bounds.ts + async function runExactBoundsScan(sourceAnimations, control, keysByUuid = /* @__PURE__ */ new Map(), framesByUuid = /* @__PURE__ */ new Map(), samplingFps = 20, collectBounds = true, captureHands = false) { + const modelFingerprint = modelBoundsFingerprint(); + const requested = sourceAnimations.map((animation) => ({ + uuid: animation.uuid, + name: animation.name, + frames: framesByUuid.get(animation.uuid) ?? frameCountFor(animation.length, samplingFps), + fingerprint: animationBoundsFingerprint(animation, modelFingerprint) + })); + const totalFrames = requested.reduce((sum, animation) => sum + animation.frames, 0); + let completedFrames = 0; + return withIsolatedProject(async () => { + const records = []; + const sequences = []; + for (const item of requested) { + const animation = Animation.all.find((candidate) => candidate.uuid === item.uuid); + if (!animation) throw new Error(`Animation "${item.name}" is missing from the isolated project.`); + const result = await bakeFramesAsync(animation, item.frames, samplingFps, control, (frame2, frames) => { + completedFrames++; + control.onProgress?.({ + mode: "exact", + animationUuid: item.uuid, + animationName: item.name, + animationFrame: frame2, + animationFrames: frames, + completedFrames, + totalFrames + }); + }, void 0, collectBounds, captureHands); + records.push({ + mode: "exact", + animationUuid: item.uuid, + animationName: item.name, + fingerprint: item.fingerprint, + frames: result.frames.length, + hits: result.outOfBounds, + checkedAt: Date.now(), + fps: samplingFps + }); + sequences.push({ + sourceUuid: item.uuid, + sourceName: item.name, + key: keysByUuid.get(item.uuid) ?? item.name, + frames: result.frames, + outOfBounds: result.outOfBounds + }); } - used.add(name); - return { - id: String(texture.id), - name, - link: texture.javaTextureLink(), - dataUrl: texture.getDataURL() - }; + return { records, sequences }; }); } - function rewriteTextureRefs(json2, options, textures) { - const model = JSON.parse(json2); - if (!model.textures) return JSON.stringify(model); - for (const key of Object.keys(model.textures)) { - const value = model.textures[key]; - if (value.startsWith("#")) continue; - const valueStem = value.split("/").pop()?.replace(/\.png$/i, ""); - const texture = textures.find( - (candidate) => candidate.id === key || candidate.link === value || candidate.name === key || candidate.name === valueStem - ); - if (texture) { - model.textures[key] = `${options.namespace}:item/${options.itemModel}/${texture.name}`; - } - } - if (!model.textures.particle) { - const firstTextureKey = Object.keys(model.textures).find( - (key) => key !== "particle" && !model.textures[key].startsWith("#") - ); - if (firstTextureKey) model.textures.particle = `#${firstTextureKey}`; - } - return JSON.stringify(model); + + // src/math-bounds.ts + var MIN = -16; + var MAX = 32; + var DEG = Math.PI / 180; + function vector(value, fallback = [0, 0, 0]) { + return value ? [value[0] ?? 0, value[1] ?? 0, value[2] ?? 0] : [...fallback]; } - function animatedModel(modelPaths) { - const uniquePaths = [...new Set(modelPaths)]; - if (uniquePaths.length === 1) { - return { type: "minecraft:model", model: uniquePaths[0] }; + function rotateZYX(point, rotation, origin) { + let x = point[0] - origin[0]; + let y = point[1] - origin[1]; + let z = point[2] - origin[2]; + const [rx, ry, rz] = rotation.map((value) => value * DEG); + let cosine = Math.cos(rx); + let sine = Math.sin(rx); + [y, z] = [y * cosine - z * sine, y * sine + z * cosine]; + cosine = Math.cos(ry); + sine = Math.sin(ry); + [x, z] = [x * cosine + z * sine, -x * sine + z * cosine]; + cosine = Math.cos(rz); + sine = Math.sin(rz); + [x, y] = [x * cosine - y * sine, x * sine + y * cosine]; + return [x + origin[0], y + origin[1], z + origin[2]]; + } + function add(left, right) { + return [left[0] + right[0], left[1] + right[1], left[2] + right[2]]; + } + function groupChain(element) { + const result = []; + let parent = element.parent; + while (parent && parent !== "root") { + if (parent instanceof Group) result.push(parent); + parent = parent.parent; } - const entries = modelPaths.map((model, frame) => ({ - threshold: frame, - model: { type: "minecraft:model", model } - })); + return result; + } + function animationOffsets(animation, group) { + const animator = animation.getBoneAnimator(group); + if (!animator) return { position: [0, 0, 0], rotation: [0, 0, 0] }; + const multiplier = animation.blend_weight ? Math.max(Animator.MolangParser.parse(animation.blend_weight), 0) : 1; + const position = animator.channels.position ? animator.interpolate("position") : null; + const rotation = animator.channels.rotation ? animator.interpolate("rotation") : null; return { - type: "minecraft:range_dispatch", - property: "minecraft:custom_model_data", - index: 0, - fallback: { type: "minecraft:model", model: modelPaths[0] }, - entries + position: vector(position instanceof Array ? position.map((value) => value * multiplier) : void 0), + rotation: vector(rotation instanceof Array ? rotation.map((value) => value * multiplier) : void 0) }; } - function buildItemDefinition(options, modelPaths) { - if (!modelPaths.length) throw new Error(tr("dap.error.no_models")); - let model = animatedModel(modelPaths); - if (options.displayContexts?.length) { - const cases = options.displayContexts.map((route) => ({ - when: route.context, - model: route.animated ? animatedModel(modelPaths) : { type: "minecraft:model", model: modelPaths[0] } - })); - model = { - type: "minecraft:select", - property: "minecraft:display_context", - cases, - fallback: { type: "minecraft:model", model: modelPaths[0] } - }; + function transformedCorners(element, animation) { + const from = vector(element.from); + const to = vector(element.to); + const inflate = element.inflate ?? 0; + const lows = [from[0] - inflate, from[1] - inflate, from[2] - inflate]; + const highs = [to[0] + inflate, to[1] + inflate, to[2] + inflate]; + const elementOrigin = vector(element.origin, from); + const elementRotation = vector(element.rotation); + const groups = groupChain(element); + const offsets = new Map(groups.map((group) => [group.uuid, animationOffsets(animation, group)])); + const corners = []; + for (const x of [lows[0], highs[0]]) for (const y of [lows[1], highs[1]]) for (const z of [lows[2], highs[2]]) { + let point = rotateZYX([x, y, z], elementRotation, elementOrigin); + for (const group of groups) { + const offset = offsets.get(group.uuid); + point = rotateZYX(point, add(vector(group.rotation), offset.rotation), vector(group.origin)); + point = add(point, offset.position); + } + corners.push(point); } - return `${JSON.stringify( - { - model, - // Verified in 26.2: this field prevents equip bobbing on each custom_model_data update. - swap_animation_scale: 0 - }, - null, - 2 - )} -`; + return corners; } - function buildPackMcmeta(options) { - return `${JSON.stringify( - { - pack: { - description: options.description, - min_format: RESOURCE_PACK_FORMAT, - max_format: RESOURCE_PACK_FORMAT - } - }, - null, - 2 - )} -`; - } - function sanitizeTextureRefs(frameJson, options, textureNames) { - const model = JSON.parse(frameJson); - const prefix2 = `${options.namespace}:item/${options.itemModel}/`; - const textures = model.textures ?? {}; - let omittedFaces = 0; - let omittedElements = 0; - const validateResolvedTexture = (value, label) => { - if (!value.startsWith(prefix2)) { - throw new Error(tr("dap.error.external_texture", { label, value })); + function collectFrameHits(frame2, animation) { + const hits = []; + for (let index = 0; index < Outliner.elements.length; index++) { + const element = Outliner.elements[index]; + if (!element.from || !element.to || element.export === false) continue; + const corners = transformedCorners(element, animation); + const chain = groupChain(element).map((group) => group.uuid); + for (let axis = 0; axis < 3; axis++) { + const values = corners.map((corner) => corner[axis]); + const low = Math.min(...values); + const high = Math.max(...values); + if (low < MIN) hits.push({ + frame: frame2, + elementIndex: index, + elementName: element.name, + axis: ["x", "y", "z"][axis], + field: "from", + value: low, + sourceElementUuid: element.uuid, + sourceGroupUuids: chain + }); + if (high > MAX) hits.push({ + frame: frame2, + elementIndex: index, + elementName: element.name, + axis: ["x", "y", "z"][axis], + field: "to", + value: high, + sourceElementUuid: element.uuid, + sourceGroupUuids: chain + }); } - const stem = value.slice(prefix2.length); - if (!textureNames.has(stem)) { - throw new Error(tr("dap.error.texture_not_generated", { value })); + } + return hits; + } + async function runQuickBoundsScan(animations, control) { + const originalTime = Timeline.time; + const originalMode = Modes.selected.id; + const originalAnimationUuid = Animation.selected?.uuid; + const originalPlaying = Animation.all.map((animation) => ({ uuid: animation.uuid, playing: animation.playing })); + const originalSaved = Project?.saved; + const fps = getProjectAnimationFps(); + const modelFingerprint = modelBoundsFingerprint(); + const totalFrames = animations.reduce((sum, animation) => sum + frameCountFor(animation.length, fps), 0); + let completedFrames = 0; + const records = []; + try { + Modes.options.animate?.select(); + for (const state of originalPlaying) { + const current = Animation.all.find((item) => item.uuid === state.uuid); + if (current) current.playing = false; } - }; - const resolveTexture = (reference) => { - let value = reference; - const visited = /* @__PURE__ */ new Set(); - while (value.startsWith("#")) { - const key = value.slice(1); - if (!key || key === "missing" || visited.has(key) || !textures[key]) { - return null; + for (const requested of animations) { + const animation = Animation.all.find((item) => item.uuid === requested.uuid); + if (!animation) continue; + animation.select(); + animation.playing = true; + const frames = frameCountFor(animation.length, fps); + const hits = []; + for (let frame2 = 0; frame2 < frames; frame2++) { + assertBoundsTaskActive(control); + Timeline.setTime(frame2 / fps); + hits.push(...collectFrameHits(frame2, animation)); + completedFrames++; + control.onProgress?.({ + mode: "quick", + animationUuid: animation.uuid, + animationName: animation.name, + animationFrame: frame2 + 1, + animationFrames: frames, + completedFrames, + totalFrames + }); + if (frame2 % 5 === 0) await yieldBoundsTask(); } - visited.add(key); - value = textures[key]; + animation.playing = false; + records.push({ + mode: "quick", + animationUuid: animation.uuid, + animationName: animation.name, + fingerprint: animationBoundsFingerprint(animation, modelFingerprint), + frames, + hits, + checkedAt: Date.now(), + fps + }); } - return value; - }; - for (const [key, rawValue] of Object.entries(textures)) { - const value = resolveTexture(rawValue); - if (!value) continue; - validateResolvedTexture(value, `#${key}`); + return records; + } finally { + for (const item of Animation.all) item.selected = false; + Animation.selected = originalAnimationUuid ? Animation.all.find((item) => item.uuid === originalAnimationUuid) ?? null : null; + if (Animation.selected) Animation.selected.selected = true; + for (const state of originalPlaying) { + const current = Animation.all.find((item) => item.uuid === state.uuid); + if (current) current.playing = state.playing; + } + Modes.options[originalMode]?.select(); + Timeline.setTime(originalTime); + Animator.preview(); + if (Project && originalSaved !== void 0) Project.saved = originalSaved; } - for (const element of model.elements ?? []) { - for (const [faceName, face] of Object.entries(element.faces ?? {})) { - const value = face.texture ? resolveTexture(face.texture) : null; - if (!value) { - delete element.faces?.[faceName]; - omittedFaces++; - } else { - validateResolvedTexture(value, face.texture ?? faceName); + } + + // src/undo-idle.ts + function waitForUndoIdle(options = {}) { + const initialDelayMs = options.initialDelayMs ?? 50; + const pollIntervalMs = options.pollIntervalMs ?? 50; + const timeoutMs = options.timeoutMs ?? 5e3; + const deadline = Date.now() + timeoutMs; + return new Promise((resolve) => { + const check = () => { + if (!Undo.current_save) { + resolve(true); + return; + } + if (Date.now() >= deadline) { + resolve(false); + return; } + setTimeout(check, pollIntervalMs); + }; + setTimeout(check, initialDelayMs); + }); + } + + // src/bounds-check-panel.ts + var panel3 = null; + var content = null; + var checkedMode = "quick"; + var boundsCheckBusy = false; + var recheckButton = null; + var activeCancellation = null; + var progressModeLabelOverride = null; + var exportBakeOnly = false; + function captureBoundsSourceState() { + if (!Project) throw new Error("No active Blockbench project."); + return { + project: Project, + mode: Modes.selected.id, + timelineTime: Timeline.time, + animationUuid: Animation.selected?.uuid ?? null, + playing: Animation.all.map((animation) => ({ uuid: animation.uuid, playing: animation.playing })), + saved: Project.saved + }; + } + function restoreBoundsSourceState(state, restoreMode) { + if (Project !== state.project && ModelProject.all.includes(state.project)) { + state.project.select(); + } + if (!Project) throw new Error("Blockbench did not restore the source project after range checking."); + if (restoreMode) Modes.options[state.mode]?.select(); + for (const animation of Animation.all) animation.selected = false; + Animation.selected = state.animationUuid ? Animation.all.find((animation) => animation.uuid === state.animationUuid) ?? null : null; + if (Animation.selected) Animation.selected.selected = true; + for (const item of state.playing) { + const animation = Animation.all.find((candidate) => candidate.uuid === item.uuid); + if (animation) animation.playing = item.playing; + } + Timeline.setTime(state.timelineTime); + Animator.preview(); + Project.saved = state.saved; + } + function modeLabel(mode) { + return tr(mode === "quick" ? "dap.bounds.mode.quick" : "dap.bounds.mode.exact"); + } + function highlightProblemElements(hits) { + const uuids = new Set(hits.map((hit) => hit.sourceElementUuid).filter(Boolean)); + const elements = Outliner.elements.filter((element) => uuids.has(element.uuid)); + if (!elements.length) return; + unselectAllElements(); + for (const element of elements) { + element.selected = true; + Outliner.selected.push(element); + } + updateSelection(); + } + function openNearestKeyframe(animation, hits, time) { + const groupUuids = [...new Set(hits.flatMap((hit) => hit.sourceGroupUuids ?? []))]; + for (const groupUuid of groupUuids) { + const animator = animation.animators?.[groupUuid]; + if (!animator) continue; + const nearest = [...animator.keyframes ?? []].sort((left, right) => { + const distance = Math.abs(left.time - time) - Math.abs(right.time - time); + if (Math.abs(distance) > 1e-6) return distance; + if (left.channel === right.channel) return 0; + return left.channel === "position" ? -1 : 1; + })[0]; + if (!nearest) continue; + animator.select(); + nearest.select(); + return; + } + } + function locateFrame(animation, frame2, hits) { + Timeline.pause(); + animation.select(); + const time = frame2 / getProjectAnimationFps(); + Timeline.setTime(time); + Animator.preview(); + highlightProblemElements(hits); + openNearestKeyframe(animation, hits, time); + Blockbench.showQuickMessage(tr("dap.bounds.located", { animation: animation.name, frame: frame2 }), 2200); + } + function passedMessage(animations, frames, mode) { + Blockbench.showMessageBox({ + title: tr("dap.bounds.passed_title"), + message: `
${escapeHtml(tr("dap.bounds.passed_heading"))}
${escapeHtml(tr("dap.bounds.passed_message", { frames, fps: getProjectAnimationFps() }))}
${escapeHtml(modeLabel(mode))} \xB7 ${escapeHtml(tr("dap.bounds.passed_animations", { animations }))}
`, + icon: "check_circle" + }); + } + var progressView = null; + function renderProgress(progress) { + if (!content) return; + if (!progressView || progressView.shell.parentElement !== content) { + content.innerHTML = ""; + progressView = buildProgressView(); + content.appendChild(progressView.shell); + } + const view = progressView; + view.title.innerText = progress ? tr("dap.bounds.progress_title", { mode: progressModeLabelOverride ?? modeLabel(progress.mode), animation: progress.animationName }) : tr("dap.bounds.progress_preparing"); + view.detail.innerText = progress ? tr("dap.bounds.progress_frames", { + frame: progress.animationFrame, + frames: progress.animationFrames, + completed: progress.completedFrames, + total: progress.totalFrames + }) : ""; + view.bar.style.width = progress && progress.totalFrames ? `${Math.min(100, progress.completedFrames / progress.totalFrames * 100).toFixed(1)}%` : "0%"; + } + function buildProgressView() { + const shell = el("div"); + shell.style.padding = "14px 10px"; + const title = el("div"); + title.style.fontWeight = "700"; + const detail = el("div", ""); + detail.style.marginTop = "7px"; + detail.style.color = "var(--color-subtle_text)"; + const track = el("div"); + track.style.height = "8px"; + track.style.marginTop = "12px"; + track.style.background = "var(--color-back)"; + const bar = el("div"); + bar.style.height = "100%"; + bar.style.width = "0%"; + bar.style.background = "var(--color-accent)"; + track.appendChild(bar); + const cancel = el("button", tr("dap.bounds.cancel")); + cancel.style.width = "100%"; + cancel.style.marginTop = "14px"; + cancel.style.borderRadius = "0"; + cancel.onclick = () => { + if (activeCancellation) activeCancellation.cancelled = true; + cancel.innerText = tr("dap.bounds.cancelling"); + cancel.disabled = true; + }; + shell.appendChild(title); + shell.appendChild(detail); + shell.appendChild(track); + shell.appendChild(cancel); + return { shell, title, detail, bar }; + } + function renderExportBakeComplete(animations, frames) { + if (!content) return; + content.innerHTML = ""; + progressView = null; + const shell = el("div"); + shell.style.padding = "14px 10px"; + shell.style.borderLeft = "3px solid #59c36a"; + shell.style.background = "var(--color-back)"; + const title = el("div", tr("dap.bounds.export_bake_complete")); + title.style.fontWeight = "700"; + title.style.color = "#59c36a"; + const detail = el("div", tr("dap.bounds.export_bake_complete_detail", { animations, frames })); + detail.style.marginTop = "7px"; + detail.style.color = "var(--color-subtle_text)"; + shell.appendChild(title); + shell.appendChild(detail); + content.appendChild(shell); + } + function renderResults(results) { + if (!content) return; + content.innerHTML = ""; + progressView = null; + const problemFrames = results.reduce((sum, result) => sum + new Set(result.hits.map((hit) => hit.frame)).size, 0); + const mode = results[0]?.mode ?? checkedMode; + const cachedCount = results.filter((result) => result.cached).length; + const header = el("div"); + header.style.padding = "9px 10px"; + header.style.borderLeft = `3px solid ${problemFrames ? "#e25d68" : "#59c36a"}`; + header.style.background = "var(--color-back)"; + const title = el("div", `${modeLabel(mode)} \xB7 ${tr("dap.bounds.panel_checked_animations", { animations: results.length })}`); + title.style.fontWeight = "700"; + title.style.fontSize = "14px"; + const summary = el("div", problemFrames ? tr("dap.bounds.panel_failed", { frames: problemFrames }) : tr("dap.bounds.panel_all_passed")); + summary.style.color = problemFrames ? "#e25d68" : "#59c36a"; + summary.style.marginTop = "3px"; + header.appendChild(title); + header.appendChild(summary); + if (cachedCount) { + const reused = el("div", tr("dap.bounds.cache_reused", { animations: cachedCount })); + reused.style.fontSize = "inherit"; + reused.style.marginTop = "3px"; + reused.style.color = "var(--color-subtle_text)"; + header.appendChild(reused); + } + content.appendChild(header); + if (problemFrames) { + const hint = el("div", tr("dap.bounds.panel_hint")); + hint.style.color = "var(--color-subtle_text)"; + hint.style.fontSize = "inherit"; + hint.style.padding = "8px 2px 6px"; + content.appendChild(hint); + } + for (const result of results) { + const animationHeader = el("div"); + animationHeader.style.display = "flex"; + animationHeader.style.justifyContent = "space-between"; + animationHeader.style.alignItems = "center"; + animationHeader.style.gap = "8px"; + animationHeader.style.padding = "8px 3px 5px"; + animationHeader.style.borderBottom = "1px solid var(--color-border)"; + const animationName = el("b", result.animation.name); + const animationStatus = el("span", result.hits.length ? tr("dap.bounds.animation_failed", { frames: new Set(result.hits.map((hit) => hit.frame)).size }) : tr("dap.bounds.animation_passed", { frames: result.frames })); + animationStatus.style.color = result.hits.length ? "#e25d68" : "#59c36a"; + animationStatus.style.fontSize = "inherit"; + animationHeader.appendChild(animationName); + animationHeader.appendChild(animationStatus); + content.appendChild(animationHeader); + for (const item of summarizeOutOfBoundsByFrame(result.hits)) { + const row = el("div"); + row.style.display = "block"; + row.style.width = "100%"; + row.style.textAlign = "left"; + row.style.padding = "8px 9px"; + row.style.margin = "0 0 5px"; + row.style.borderRadius = "0"; + row.style.borderLeft = "3px solid #e25d68"; + row.style.background = "var(--color-back)"; + row.style.boxSizing = "border-box"; + row.style.cursor = "pointer"; + row.style.lineHeight = "1.35"; + row.style.minHeight = "58px"; + row.innerHTML = `${escapeHtml(tr("dap.export.locate_frame", { frame: item.frame }))}
${escapeHtml(item.description)}
`; + const frameHits = result.hits.filter((hit) => hit.frame === item.frame); + row.onclick = () => locateFrame(result.animation, item.frame, frameHits); + content.appendChild(row); } } - if (model.elements) { - model.elements = model.elements.filter((element) => { - if (Object.keys(element.faces ?? {}).length) return true; - omittedElements++; - return false; - }); + } + function resolveBoundsCheckAnimations(preferredAnimations) { + const source = preferredAnimations?.length ? preferredAnimations : Animation.all; + const seen = /* @__PURE__ */ new Set(); + return source.filter((animation) => { + if (seen.has(animation.uuid)) return false; + seen.add(animation.uuid); + return true; + }); + } + function setBoundsCheckBusy(busy) { + boundsCheckBusy = busy; + if (recheckButton) recheckButton.disabled = busy; + if (!busy) { + Blockbench.setProgress(0); + Blockbench.setStatusBarText(); } + } + function recordsToResults(animations, records, mode, cached) { + return animations.map((animation) => { + const record = records.find((candidate) => candidate.animationUuid === animation.uuid); + if (!record) throw new Error(`No ${mode} range result was produced for "${animation.name}".`); + return { animation, frames: record.frames, hits: record.hits, mode, cached: cached.has(animation.uuid) }; + }); + } + function createBoundsProgressControl() { return { - json: JSON.stringify(model), - omittedFaces, - omittedElements + isCancelled: () => activeCancellation?.cancelled === true, + onProgress: (progress) => { + renderProgress(progress); + Blockbench.setProgress(progress.totalFrames ? progress.completedFrames / progress.totalFrames : 0); + Blockbench.setStatusBarText(tr("dap.bounds.progress_status", { + mode: progressModeLabelOverride ?? modeLabel(progress.mode), + animation: progress.animationName, + frame: progress.animationFrame, + frames: progress.animationFrames + })); + } }; } - function buildResourcePack(frames, options) { - const files = []; - const assetRoot = `assets/${options.namespace}`; - const textures = collectTextures(); - const textureNames = new Set(textures.map((texture) => texture.name)); - const modelPaths = []; - const uniqueModels = /* @__PURE__ */ new Map(); - let sampledFrames = 0; - let modelBytesBefore = 0; - let modelBytesAfter = 0; - let omittedUntexturedFaces = 0; - let omittedEmptyElements = 0; - files.push({ path: "pack.mcmeta", content: buildPackMcmeta(options) }); - for (const frame of frames) { - const rewritten = rewriteTextureRefs(frame.json, options, textures); - const sanitized = sanitizeTextureRefs(rewritten, options, textureNames); - omittedUntexturedFaces += sanitized.omittedFaces; - omittedEmptyElements += sanitized.omittedElements; - sampledFrames++; - modelBytesBefore += sanitized.json.length; - let modelPath = uniqueModels.get(sanitized.json); - if (!modelPath) { - const uniqueIndex = uniqueModels.size; - modelPath = `${options.namespace}:item/${options.itemModel}/generated/model_${uniqueIndex}`; - uniqueModels.set(sanitized.json, modelPath); - modelBytesAfter += sanitized.json.length; - files.push({ - path: `${assetRoot}/models/item/${options.itemModel}/generated/model_${uniqueIndex}.json`, - content: `${sanitized.json} -` - }); + async function performBoundsCheck(animationUuids, mode, project, sourceState) { + try { + const animations = animationUuids.map((uuid) => Animation.all.find((animation) => animation.uuid === uuid)).filter((animation) => Boolean(animation)); + if (!animations.length) { + Blockbench.showQuickMessage(tr("dap.bounds.no_animation"), 2e3); + return; + } + checkedMode = mode; + openBoundsCheckPanel(); + const modelFingerprint = modelBoundsFingerprint(); + const cached = /* @__PURE__ */ new Map(); + for (const animation of animations) { + const record = validBoundsDetection(project, animation, mode, modelFingerprint); + if (record) cached.set(animation.uuid, record); + } + const pending = animations.filter((animation) => !cached.has(animation.uuid)); + if (!pending.length) { + renderResults(recordsToResults(animations, [...cached.values()], mode, new Set(cached.keys()))); + Blockbench.showQuickMessage(tr("dap.bounds.cache_all_reused"), 2200); + return; } - modelPaths.push(modelPath); + activeCancellation = { cancelled: false }; + renderProgress(); + const control = createBoundsProgressControl(); + const scanned = mode === "quick" ? await runQuickBoundsScan(pending, control) : (await runExactBoundsScan(pending, control)).records; + for (const record of scanned) rememberBoundsDetection(project, record); + const results = recordsToResults(animations, [...cached.values(), ...scanned], mode, new Set(cached.keys())); + renderResults(results); + if (results.every((result) => !result.hits.length)) { + passedMessage(animations.length, results.reduce((sum, result) => sum + result.frames, 0), mode); + } + } finally { + restoreBoundsSourceState(sourceState, false); } - files.push({ - path: `${assetRoot}/items/${options.itemModel}.json`, - content: buildItemDefinition(options, modelPaths) + } + function chooseBoundsMode(animations) { + Blockbench.showMessageBox({ + title: tr("dap.bounds.choose_title"), + message: tr("dap.bounds.choose_message"), + icon: "settings_overscan", + buttons: [tr("dap.bounds.choose_cancel"), tr("dap.bounds.mode.quick"), tr("dap.bounds.mode.exact")], + confirmIndex: 2, + cancelIndex: 0 + }, (button) => { + if (button === 1) runBoundsCheck(animations, "quick"); + if (button === 2) runBoundsCheck(animations, "exact"); }); - for (const texture of textures) { - files.push({ - path: `${assetRoot}/textures/item/${options.itemModel}/${texture.name}.png`, - content: texture.dataUrl, - isImage: true - }); + } + function runBoundsCheck(preferredAnimations, mode) { + const animations = resolveBoundsCheckAnimations(preferredAnimations); + if (!animations.length) { + Blockbench.showQuickMessage(tr("dap.bounds.no_animation"), 2e3); + return; } - return { - files, - report: { - sampledFrames, - uniqueModels: uniqueModels.size, - duplicateFrames: sampledFrames - uniqueModels.size, - modelBytesBefore, - modelBytesAfter, - omittedUntexturedFaces, - omittedEmptyElements + if (!mode) { + chooseBoundsMode(animations); + return; + } + if (boundsCheckBusy) { + Blockbench.showQuickMessage(tr("dap.bounds.check_in_progress"), 1800); + return; + } + setBoundsCheckBusy(true); + const sourceState = captureBoundsSourceState(); + const animationUuids = animations.map((animation) => animation.uuid); + const project = Project; + const projectUuid = Project?.uuid; + document.activeElement?.blur(); + void waitForUndoIdle().then(async (idle) => { + if (!idle || Project?.uuid !== projectUuid) { + if (!idle) Blockbench.showMessageBox({ + title: tr("dap.bounds.edit_in_progress_title"), + message: tr("dap.bounds.edit_in_progress_message"), + icon: "error" + }); + return; + } + await performBoundsCheck(animationUuids, mode, project, sourceState); + }).catch((error) => { + if (error instanceof BoundsTaskCancelledError) { + Blockbench.showQuickMessage(tr("dap.bounds.cancelled"), 2200); + return; + } + Blockbench.showMessageBox({ + title: tr("dap.bounds.check_failed_title"), + message: escapeHtml(error instanceof Error ? error.message : String(error)), + icon: "error" + }); + }).finally(() => { + activeCancellation = null; + setBoundsCheckBusy(false); + }); + } + async function runExactBoundsForExport(animations, keysByUuid, framesByUuid, samplingFps, rememberResults, showResults, captureHands = false) { + if (boundsCheckBusy) throw new Error(tr("dap.bounds.check_in_progress")); + const sourceProject = Project; + const sourceState = captureBoundsSourceState(); + setBoundsCheckBusy(true); + activeCancellation = { cancelled: false }; + progressModeLabelOverride = rememberResults || showResults ? null : tr("dap.bounds.mode.export_bake"); + const bakeOnly = !rememberResults && !showResults; + exportBakeOnly = bakeOnly; + try { + openBoundsCheckPanel(); + checkedMode = "exact"; + renderProgress(); + const control = createBoundsProgressControl(); + const collectBounds = rememberResults || showResults; + const result = await runExactBoundsScan(animations, control, keysByUuid, framesByUuid, samplingFps, collectBounds, captureHands); + if (rememberResults) { + for (const record of result.records) rememberBoundsDetection(sourceProject, record); + } + if (showResults) { + const restoredAnimations = result.records.map((record) => Animation.all.find((animation) => animation.uuid === record.animationUuid)).filter((animation) => Boolean(animation)); + renderResults(recordsToResults(restoredAnimations, result.records, "exact", /* @__PURE__ */ new Set())); + } else if (bakeOnly) { + renderExportBakeComplete( + result.sequences.length, + result.sequences.reduce((sum, sequence) => sum + sequence.frames.length, 0) + ); } + return result; + } catch (error) { + if (error instanceof BoundsTaskCancelledError) { + Blockbench.showQuickMessage(tr("dap.bounds.cancelled"), 2200); + return null; + } + throw error; + } finally { + activeCancellation = null; + progressModeLabelOverride = null; + exportBakeOnly = false; + if (recheckButton) recheckButton.style.display = bakeOnly ? "none" : ""; + setBoundsCheckBusy(false); + restoreBoundsSourceState(sourceState, true); + } + } + function openBoundsCheckPanel() { + Modes.options.animate?.select(); + if (panel3) { + if (recheckButton) recheckButton.style.display = exportBakeOnly ? "none" : ""; + panel3.update(); + return; + } + const shell = el("div"); + shell.style.display = "flex"; + shell.style.flexDirection = "column"; + shell.style.height = "100%"; + shell.style.minHeight = "0"; + shell.style.minWidth = "0"; + const results = el("div"); + results.style.flex = "1 1 auto"; + results.style.minHeight = "0"; + results.style.overflowY = "auto"; + results.style.overflowX = "hidden"; + results.style.padding = "4px 6px 8px 4px"; + results.style.boxSizing = "border-box"; + content = results; + const footer = el("div"); + footer.style.flex = "0 0 auto"; + footer.style.padding = "7px 6px 5px 4px"; + footer.style.borderTop = "1px solid var(--color-border)"; + footer.style.background = "var(--color-ui)"; + const recheck = el("button", tr("dap.bounds.recheck")); + recheckButton = recheck; + recheck.style.display = exportBakeOnly ? "none" : ""; + recheck.style.width = "100%"; + recheck.style.borderRadius = "0"; + recheck.onclick = () => { + runBoundsCheck(void 0, checkedMode); }; + footer.appendChild(recheck); + shell.appendChild(results); + shell.appendChild(footer); + panel3 = new Panel("display_anim_preview_bounds", { + name: tr("dap.bounds.panel_title"), + icon: "settings_overscan", + growable: true, + resizable: true, + condition: { modes: ["animate"] }, + default_position: { slot: "left_bar", height: 420, width: 350 } + }); + panel3.node.style.minHeight = "140px"; + panel3.node.appendChild(shell); + } + function disposeBoundsCheckPanel() { + if (activeCancellation) activeCancellation.cancelled = true; + activeCancellation = null; + progressModeLabelOverride = null; + progressView = null; + exportBakeOnly = false; + panel3?.delete(); + panel3 = null; + content = null; + recheckButton = null; + boundsCheckBusy = false; } - // src/datapack.ts - var DATA_PACK_FORMAT = [107, 1]; - function prefix(options) { - return `{"text":"[${options.packName}] ","color":"gold"}`; + // src/animation-export-plan.ts + function animationKeyFromName(name) { + return name.trim().toLowerCase().replace(/[^a-z0-9_.-]+/g, "_").replace(/^_+|_+$/g, ""); + } + function isValidAnimationKey(key) { + return Boolean(key) && key !== "." && key !== ".." && !isReservedAnimationKey(key) && /^[a-z0-9_.-]+$/.test(key); + } + function findAnimationKeyConflicts(animations) { + const groups = /* @__PURE__ */ new Map(); + for (const animation of animations) { + const key = animationKeyFromName(animation.name); + const names = groups.get(key) ?? []; + names.push(animation.name); + groups.set(key, names); + } + return [...groups.entries()].filter(([key, names]) => !isValidAnimationKey(key) || names.length > 1).map(([key, animationNames]) => ({ key, animationNames })); } - function tellraw(options, text, color, target = "@s") { - return `tellraw ${target} [${prefix(options)},{"text":${JSON.stringify(text)},"color":"${color}"}]`; + function createExportAnimationSpecs(animations, fps) { + return animations.map((animation) => ({ + animation, + sourceUuid: animation.uuid, + sourceName: animation.name, + key: animationKeyFromName(animation.name), + sourceFps: animation.snapping || fps, + frameCount: frameCountFor(animation.length, fps) + })); } + + // src/datapack.ts + var DATA_PACK_FORMAT = [107, 1]; function json(value) { return `${JSON.stringify(value, null, 2)} `; @@ -1279,26 +3708,113 @@ return `${commands.join("\n")} `; } + function prefix(options) { + return JSON.stringify({ text: `[${options.packName}] `, color: "gold" }); + } + function tellraw(options, message, color, target = "@s") { + return `tellraw ${target} [${prefix(options)},{"text":${JSON.stringify(message)},"color":"${color}"}]`; + } + function dynamicErrorTellraw(options, translationKey, placeholder, storagePath, runtimeStorage) { + const marker = "__JSB_DYNAMIC_ARGUMENT__"; + const translated = tr(translationKey, { [placeholder]: marker }); + const parts = translated.includes(marker) ? translated.split(marker) : [`${translated}: `, ""]; + const components = [ + { text: `[${options.packName}] `, color: "gold" } + ]; + parts.forEach((part, index) => { + if (part) components.push({ text: part, color: "red" }); + if (index < parts.length - 1) { + components.push({ nbt: storagePath, storage: runtimeStorage, color: "yellow" }); + } + }); + return `tellraw @s ${JSON.stringify(components)}`; + } function customNameComponent(name) { - const component = JSON.stringify({ text: name, color: "gold", italic: false }).replace(/\\/g, "\\\\").replace(/'/g, "\\'"); - return `minecraft:custom_name='${component}'`; + const component = JSON.stringify({ text: name, color: "gold", italic: false }); + return `minecraft:custom_name=${component}`; + } + function customModelData(animationKey, frame2) { + return `{strings:[${JSON.stringify(animationKey)}],floats:[${frame2.toFixed(1)}]}`; + } + function itemAnimationState(projectName, animationKey, frame2, mode, max, phase) { + const state = itemAnimationStateValue(projectName, animationKey, frame2, mode, max, phase); + return `{jsb:{project:${JSON.stringify(state.jsb.project)},animation:${JSON.stringify(state.jsb.animation)},frame:${state.jsb.frame},mode:${state.jsb.mode},max:${state.jsb.max},phase:${state.jsb.phase}}}`; + } + function itemAnimationStateValue(projectName, animationKey, frame2, mode, max, phase) { + return { jsb: { project: projectName, animation: animationKey, frame: frame2, mode, max, phase } }; + } + function frameModifier(frameObjective, animationKey) { + const modifier = { + function: "minecraft:set_custom_model_data", + floats: { + values: [{ type: "minecraft:score", target: "this", score: frameObjective }], + mode: "replace_all" + } + }; + if (animationKey !== void 0) { + modifier.strings = { values: [animationKey], mode: "replace_all" }; + } + return modifier; + } + function fixedFrameModifier(animationKey, frame2) { + return { + function: "minecraft:set_custom_model_data", + strings: { values: [animationKey], mode: "replace_all" }, + floats: { values: [frame2], mode: "replace_all" } + }; + } + function validateAnimations(options) { + if (!options.animations.length) throw new Error("At least one animation is required"); + const animations = /* @__PURE__ */ new Map(); + for (const animation of options.animations) { + if (!isValidAnimationKey(animation.key)) { + throw new Error(`Unsafe animation key: ${JSON.stringify(animation.key)}`); + } + if (!Number.isInteger(animation.frameCount) || animation.frameCount < 1) { + throw new Error(`Animation ${JSON.stringify(animation.key)} has no frames`); + } + if (animations.has(animation.key)) { + throw new Error(`Duplicate animation key: ${JSON.stringify(animation.key)}`); + } + animations.set(animation.key, animation); + } + if (!animations.has(options.defaultAnimationKey)) { + throw new Error(`Default animation key was not exported: ${options.defaultAnimationKey}`); + } + return animations; } function buildDatapack(options) { - const { - dataNamespace: ns, - frameObjective: frameScore, - modeObjective: modeScore, - playingTag: tag, - frameCount - } = options; - const lastFrame = Math.max(frameCount - 1, 0); - const itemModelId = `${options.assetNamespace}:${options.itemModel}`; + if (!Number.isInteger(options.playbackFps) || options.playbackFps < 1 || options.playbackFps > MAX_EXPORT_FPS) { + throw new Error(`Playback FPS must be an integer from 1 to ${MAX_EXPORT_FPS}: ${options.playbackFps}`); + } + const animations = validateAnimations(options); + const ns = EXPORT_NAMESPACE; + const root = options.projectName; + const id = (path) => `${ns}:${root}/${path}`; + const runtimeStorage = `${ns}:${root}/runtime`; + const itemModelId = `${ns}:${root}`; const heldItem = `*[minecraft:item_model="${itemModelId}"]`; const ifHeld = `execute if items entity @s weapon.mainhand ${heldItem} run`; - const applyFrame = `${ifHeld} item modify entity @s weapon.mainhand ${ns}:set_frame`; + const unlessHeld = `execute unless items entity @s weapon.mainhand ${heldItem} run`; + const frameScore = options.frameObjective; + const modeScore = options.modeObjective; + const maxFrameScore = options.maxFrameObjective; + const phaseScore = phaseObjectiveFor(frameScore); + const playbackFps = options.playbackFps; + const tag = options.playingTag; + const tips = options.debugEnabled === true; + const holdItemLines = tips ? [`${unlessHeld} ${tellraw(options, tr("dap.datapack.hold_item"), "red")}`] : []; + const defaultAnimation = animations.get(options.defaultAnimationKey); + const defaultLastFrame = defaultAnimation.frameCount - 1; + const inventorySlots = [ + ...Array.from({ length: 9 }, (_, slot) => `hotbar.${slot}`), + ...Array.from({ length: 27 }, (_, slot) => `inventory.${slot}`), + "weapon.offhand" + ]; + const onceItem = `*[minecraft:item_model="${itemModelId}",minecraft:custom_data~{jsb:{project:${JSON.stringify(root)},mode:2}}]`; const files = []; - const fn = (name, content) => { - files.push({ path: `data/${ns}/function/${name}.mcfunction`, content }); + const fn = (path, content2) => { + files.push({ path: `data/${ns}/function/${root}/${path}.mcfunction`, content: content2 }); }; files.push({ path: "pack.mcmeta", @@ -1315,223 +3831,1481 @@ lines( `scoreboard objectives add ${frameScore} dummy`, `scoreboard objectives add ${modeScore} dummy`, - tellraw(options, tr("dap.datapack.loaded", { namespace: ns }), "green", "@a") + `scoreboard objectives add ${maxFrameScore} dummy`, + `scoreboard objectives add ${phaseScore} dummy`, + ...options.debugEnabled ? [tellraw(options, tr("dap.datapack.loaded", { namespace: `${ns}:${root}` }), "green", "@a")] : [] ) ); fn( "tick", lines( - `execute as @a[tag=${tag}] unless items entity @s weapon.mainhand ${heldItem} run scoreboard players set @s ${modeScore} 0`, - `execute as @a[tag=${tag}] unless items entity @s weapon.mainhand ${heldItem} run tag @s remove ${tag}`, - `execute as @a[tag=${tag}] if items entity @s weapon.mainhand ${heldItem} run function ${ns}:_tick_player` + `execute as @a if items entity @s weapon.mainhand ${heldItem} run function ${id("_internal/sync_held")}`, + `execute as @a[tag=${tag}] unless items entity @s weapon.mainhand ${heldItem} run function ${id("_internal/leave_held")}` ) ); fn( - "_tick_player", + "_internal/load_held_state", lines( - `scoreboard players add @s ${frameScore} 1`, - `execute if score @s ${modeScore} matches 1 if score @s ${frameScore} matches ${frameCount}.. run scoreboard players set @s ${frameScore} 0`, - `execute if score @s ${modeScore} matches 2 if score @s ${frameScore} matches ${frameCount}.. run scoreboard players set @s ${frameScore} ${lastFrame}`, - applyFrame, - `execute if score @s ${modeScore} matches 2 if score @s ${frameScore} matches ${lastFrame} run tag @s remove ${tag}` + `scoreboard players set @s ${frameScore} 0`, + `scoreboard players set @s ${modeScore} 0`, + `scoreboard players set @s ${maxFrameScore} ${defaultLastFrame}`, + `scoreboard players set @s ${phaseScore} 0`, + `data modify storage ${runtimeStorage} held set value ${itemAnimationState(root, defaultAnimation.key, 0, 0, defaultLastFrame, 0).slice(5, -1)}`, + `execute store result score @s ${frameScore} run data get entity @s SelectedItem.components."minecraft:custom_data".jsb.frame 1`, + `execute store result score @s ${modeScore} run data get entity @s SelectedItem.components."minecraft:custom_data".jsb.mode 1`, + `execute store result score @s ${maxFrameScore} run data get entity @s SelectedItem.components."minecraft:custom_data".jsb.max 1`, + `execute store result score @s ${phaseScore} run data get entity @s SelectedItem.components."minecraft:custom_data".jsb.phase 1`, + ...Array.from( + animations.values(), + (animation) => `execute if items entity @s weapon.mainhand *[minecraft:item_model="${itemModelId}",minecraft:custom_data~{jsb:{project:${JSON.stringify(root)},animation:${JSON.stringify(animation.key)}}}] run data modify storage ${runtimeStorage} held.animation set value ${JSON.stringify(animation.key)}` + ) ) ); fn( - "give", + "_internal/save_scores_to_storage", + lines( + `execute store result storage ${runtimeStorage} held.frame int 1 run scoreboard players get @s ${frameScore}`, + `execute store result storage ${runtimeStorage} held.mode int 1 run scoreboard players get @s ${modeScore}`, + `execute store result storage ${runtimeStorage} held.max int 1 run scoreboard players get @s ${maxFrameScore}`, + `execute store result storage ${runtimeStorage} held.phase int 1 run scoreboard players get @s ${phaseScore}` + ) + ); + fn( + "_internal/apply_animation_from_storage", + lines(...Array.from( + animations.values(), + (animation) => `execute if data storage ${runtimeStorage} {held:{animation:${JSON.stringify(animation.key)}}} run item modify entity @s weapon.mainhand ${id(`set_frame/${animation.key}`)}` + )) + ); + fn( + "_internal/save_held_state", lines( - `give @s ${options.baseItem}[minecraft:item_model="${itemModelId}",minecraft:custom_model_data={floats:[0.0]},${customNameComponent(options.itemDisplayName)}]`, + `function ${id("_internal/save_scores_to_storage")}`, + `item modify entity @s weapon.mainhand ${id("state/copy_from_storage")}`, + `function ${id("_internal/apply_animation_from_storage")}` + ) + ); + fn( + "_internal/reset_inactive_once", + lines(...inventorySlots.map( + (slot) => `execute if items entity @s ${slot} ${onceItem} run item modify entity @s ${slot} ${id("state/reset_default")}` + )) + ); + fn( + "_internal/sync_held", + lines( + `function ${id("_internal/load_held_state")}`, + `execute if entity @s[tag=${tag}] run function ${id("_internal/reset_inactive_once")}`, + `function ${id("_internal/save_held_state")}`, + `execute if score @s ${modeScore} matches 1..2 run tag @s add ${tag}`, + `execute unless score @s ${modeScore} matches 1..2 run tag @s remove ${tag}`, + `execute if score @s ${modeScore} matches 1..2 run function ${id("_internal/tick_player")}` + ) + ); + fn( + "_internal/leave_held", + lines( + `function ${id("_internal/reset_inactive_once")}`, + `function ${id("_internal/cancel")}` + ) + ); + fn( + "_internal/tick_player", + lines( + `scoreboard players add @s ${phaseScore} ${playbackFps}`, + `execute if score @s ${phaseScore} matches 20.. if score @s ${modeScore} matches 2 if score @s ${frameScore} = @s ${maxFrameScore} run function ${id("_internal/reset_default")}`, + `execute if entity @s[tag=${tag}] if score @s ${phaseScore} matches 20.. run scoreboard players add @s ${frameScore} 1`, + `execute if entity @s[tag=${tag}] if score @s ${phaseScore} matches 20.. run scoreboard players remove @s ${phaseScore} 20`, + `execute if entity @s[tag=${tag}] if score @s ${maxFrameScore} matches 0 if score @s ${frameScore} > @s ${maxFrameScore} run scoreboard players set @s ${frameScore} 0`, + `execute if entity @s[tag=${tag}] if score @s ${maxFrameScore} matches 1.. if score @s ${frameScore} > @s ${maxFrameScore} run scoreboard players set @s ${frameScore} 1`, + `execute if entity @s[tag=${tag}] run function ${id("_internal/save_held_state")}` + ) + ); + fn( + "_internal/cancel", + lines( + `tag @s remove ${tag}`, `scoreboard players set @s ${frameScore} 0`, `scoreboard players set @s ${modeScore} 0`, - `tag @s remove ${tag}`, - tellraw(options, tr("dap.datapack.item_given"), "green") + `scoreboard players set @s ${maxFrameScore} ${defaultLastFrame}`, + `scoreboard players set @s ${phaseScore} 0` + ) + ); + fn( + "_internal/reset_default", + lines( + `function ${id("_internal/cancel")}`, + `${ifHeld} item modify entity @s weapon.mainhand ${id("state/reset_default")}` + ) + ); + fn( + "give", + lines( + options.handRenderingEnabled ? `loot give @s loot ${id("give")}` : `give @s ${options.baseItem}[minecraft:item_model="${itemModelId}",minecraft:custom_model_data=${customModelData(defaultAnimation.key, 0)},minecraft:custom_data=${itemAnimationState(root, defaultAnimation.key, 0, 0, defaultLastFrame, 0)},minecraft:max_stack_size=1,${customNameComponent(options.itemDisplayName)}]`, + ...options.debugEnabled ? [tellraw(options, tr("dap.datapack.item_given", { + namespace: `${ns}:${root}`, + animation: defaultAnimation.key + }), "green")] : [] + ) + ); + if (options.handRenderingEnabled) { + files.push({ + path: `data/${ns}/loot_table/${root}/give.json`, + content: json({ + type: "minecraft:command", + pools: [{ + rolls: 1, + entries: [{ + type: "minecraft:item", + name: "minecraft:player_head", + functions: [ + { + function: "minecraft:set_components", + components: { + "minecraft:item_model": itemModelId, + "minecraft:custom_model_data": { strings: [defaultAnimation.key], floats: [0] }, + "minecraft:custom_data": itemAnimationStateValue(root, defaultAnimation.key, 0, 0, defaultLastFrame, 0), + "minecraft:max_stack_size": 1, + "minecraft:custom_name": { text: options.itemDisplayName, color: "gold", italic: false } + } + }, + { function: "minecraft:fill_player_head", entity: "this" } + ] + }] + }] + }) + }); + } + fn( + "_internal/validate_animation", + lines( + `data modify storage ${runtimeStorage} request.valid_animation set value 0b`, + ...Array.from( + animations.values(), + (animation) => `execute if data storage ${runtimeStorage} {request:{animation:${JSON.stringify(animation.key)}}} run data modify storage ${runtimeStorage} request.valid_animation set value 1b` + ) ) ); - const startGuard = `execute unless items entity @s weapon.mainhand ${heldItem} run`; fn( - "play_loop", + "_internal/validate_mode", lines( - `${startGuard} ${tellraw(options, tr("dap.datapack.hold_item"), "red")}`, - `${ifHeld} scoreboard players set @s ${frameScore} 0`, - `${ifHeld} scoreboard players set @s ${modeScore} 1`, - applyFrame, - `${ifHeld} tag @s add ${tag}`, - `${ifHeld} ${tellraw(options, tr("dap.datapack.loop_started", { last_frame: lastFrame }), "green")}` + `data modify storage ${runtimeStorage} request.valid_mode set value 0b`, + `execute if data storage ${runtimeStorage} {request:{mode:"loop"}} run data modify storage ${runtimeStorage} request.valid_mode set value 1b`, + `execute if data storage ${runtimeStorage} {request:{mode:"once"}} run data modify storage ${runtimeStorage} request.valid_mode set value 1b` ) ); fn( - "play_once", + "_internal/error/invalid_animation", lines( - `${startGuard} ${tellraw(options, tr("dap.datapack.hold_item"), "red")}`, - `${ifHeld} scoreboard players set @s ${frameScore} 0`, - `${ifHeld} scoreboard players set @s ${modeScore} 2`, - applyFrame, - `${ifHeld} tag @s add ${tag}`, - `${ifHeld} ${tellraw(options, tr("dap.datapack.once_started", { last_frame: lastFrame }), "green")}` + ...tips ? [ + dynamicErrorTellraw( + options, + "dap.datapack.invalid_animation", + "animation", + "request.animation", + runtimeStorage + ) + ] : ["return 0"] ) ); - const frameReadout = `tellraw @s [${prefix(options)},{"text":${JSON.stringify(tr("dap.datapack.current_frame"))},"color":"gold"},{"score":{"name":"@s","objective":"${frameScore}"},"color":"aqua"}]`; fn( - "next", + "_internal/error/invalid_mode", lines( - `${ifHeld} tag @s remove ${tag}`, - `${ifHeld} scoreboard players add @s ${frameScore} 1`, - `${ifHeld} execute if score @s ${frameScore} matches ${frameCount}.. run scoreboard players set @s ${frameScore} 0`, - applyFrame, - `${ifHeld} ${frameReadout}` + ...tips ? [ + dynamicErrorTellraw( + options, + "dap.datapack.invalid_mode", + "mode", + "request.mode", + runtimeStorage + ) + ] : ["return 0"] ) ); fn( - "prev", + "play", lines( - `${ifHeld} tag @s remove ${tag}`, - `${ifHeld} scoreboard players remove @s ${frameScore} 1`, - `${ifHeld} execute if score @s ${frameScore} matches ..-1 run scoreboard players set @s ${frameScore} ${lastFrame}`, - applyFrame, - `${ifHeld} ${frameReadout}` + ...holdItemLines, + `${unlessHeld} return 0`, + `data remove storage ${runtimeStorage} request`, + `$data modify storage ${runtimeStorage} request.animation set value "$(animation)"`, + `$data modify storage ${runtimeStorage} request.mode set value "$(mode)"`, + `function ${id("_internal/validate_animation")}`, + `execute unless data storage ${runtimeStorage} {request:{valid_animation:1b}} run function ${id("_internal/error/invalid_animation")}`, + `execute unless data storage ${runtimeStorage} {request:{valid_animation:1b}} run return 0`, + `function ${id("_internal/validate_mode")}`, + `execute unless data storage ${runtimeStorage} {request:{valid_mode:1b}} run function ${id("_internal/error/invalid_mode")}`, + `execute unless data storage ${runtimeStorage} {request:{valid_mode:1b}} run return 0`, + `$function ${id("_internal/play/$(animation)/$(mode)")}` ) ); fn( - "reset", + "frame", lines( - `${ifHeld} tag @s remove ${tag}`, - `${ifHeld} scoreboard players set @s ${frameScore} 0`, - `${ifHeld} scoreboard players set @s ${modeScore} 0`, - applyFrame, - `${ifHeld} ${tellraw(options, tr("dap.datapack.reset"), "green")}` + ...holdItemLines, + `${unlessHeld} return 0`, + `data remove storage ${runtimeStorage} request`, + `$data modify storage ${runtimeStorage} request.animation set value "$(animation)"`, + `function ${id("_internal/validate_animation")}`, + `execute unless data storage ${runtimeStorage} {request:{valid_animation:1b}} run function ${id("_internal/error/invalid_animation")}`, + `execute unless data storage ${runtimeStorage} {request:{valid_animation:1b}} run return 0`, + `$function ${id("_internal/frame/$(animation)")} {frame:$(frame)}` ) ); fn( "stop", lines( - `tag @s remove ${tag}`, - `scoreboard players set @s ${modeScore} 0`, - tellraw(options, tr("dap.datapack.stopped"), "yellow") + ...holdItemLines, + `${unlessHeld} return 0`, + `function ${id("_internal/reset_default")}`, + ...tips ? [tellraw(options, tr("dap.datapack.stopped"), "yellow")] : [] ) ); + for (const animation of animations.values()) { + const lastFrame = animation.frameCount - 1; + const firstMotionFrame = lastFrame >= 1 ? 1 : 0; + const start = [ + `scoreboard players set @s ${frameScore} ${firstMotionFrame}`, + `scoreboard players set @s ${maxFrameScore} ${lastFrame}`, + `scoreboard players set @s ${phaseScore} 0`, + `data modify storage ${runtimeStorage} held set value {project:${JSON.stringify(root)},animation:${JSON.stringify(animation.key)},frame:${firstMotionFrame},mode:0,max:${lastFrame},phase:0}` + ]; + const finish = [ + `function ${id("_internal/save_held_state")}`, + `tag @s add ${tag}` + ]; + fn( + `_internal/play/${animation.key}/loop`, + lines( + ...start, + `scoreboard players set @s ${modeScore} 1`, + ...finish, + ...tips ? [tellraw(options, tr("dap.datapack.loop_started", { + animation: animation.displayName, + fps: options.playbackFps, + last_frame: lastFrame + }), "green")] : [] + ) + ); + fn( + `_internal/play/${animation.key}/once`, + lines( + ...start, + `scoreboard players set @s ${modeScore} 2`, + ...finish, + ...tips ? [tellraw(options, tr("dap.datapack.once_started", { + animation: animation.displayName, + last_frame: lastFrame + }), "green")] : [] + ) + ); + fn( + `_internal/frame/${animation.key}`, + lines( + `tag @s remove ${tag}`, + `scoreboard players set @s ${modeScore} 0`, + `scoreboard players set @s ${maxFrameScore} ${lastFrame}`, + `scoreboard players set @s ${frameScore} 0`, + `scoreboard players set @s ${phaseScore} 0`, + `data modify storage ${runtimeStorage} held set value {project:${JSON.stringify(root)},animation:${JSON.stringify(animation.key)},frame:0,mode:0,max:${lastFrame},phase:0}`, + `$scoreboard players set @s ${frameScore} $(frame)`, + `execute if score @s ${frameScore} matches ..-1 run scoreboard players set @s ${frameScore} 0`, + `execute if score @s ${frameScore} > @s ${maxFrameScore} run scoreboard players operation @s ${frameScore} = @s ${maxFrameScore}`, + `function ${id("_internal/save_held_state")}` + ) + ); + fn( + `play/${animation.key}`, + lines(`function ${id("play")} {animation:${JSON.stringify(animation.key)},mode:"once"}`) + ); + fn( + `loop/${animation.key}`, + lines(`function ${id("play")} {animation:${JSON.stringify(animation.key)},mode:"loop"}`) + ); + fn( + `frame/${animation.key}`, + lines(`$function ${id("frame")} {animation:${JSON.stringify(animation.key)},frame:$(frame)}`) + ); + files.push({ + path: `data/${ns}/item_modifier/${root}/set_frame/${animation.key}.json`, + content: json(frameModifier(frameScore, animation.key)) + }); + } + files.push({ + path: `data/${ns}/item_modifier/${root}/set_frame.json`, + content: json(frameModifier(frameScore)) + }); files.push({ - path: `data/${ns}/item_modifier/set_frame.json`, + path: `data/${ns}/item_modifier/${root}/state/copy_from_storage.json`, content: json({ - function: "minecraft:set_custom_model_data", - floats: { - values: [{ type: "minecraft:score", target: "this", score: frameScore }], - mode: "replace_all" - } + function: "minecraft:copy_custom_data", + source: { type: "minecraft:storage", source: runtimeStorage }, + ops: [{ source: "held", target: "jsb", op: "replace" }] }) }); + files.push({ + path: `data/${ns}/item_modifier/${root}/state/reset_default.json`, + content: json([ + { + function: "minecraft:set_custom_data", + tag: itemAnimationState(root, defaultAnimation.key, 0, 0, defaultLastFrame, 0) + }, + fixedFrameModifier(defaultAnimation.key, 0) + ]) + }); files.push({ path: "data/minecraft/tags/function/load.json", - content: json({ values: [`${ns}:load`] }) + content: json({ values: [id("load")] }) }); files.push({ path: "data/minecraft/tags/function/tick.json", - content: json({ values: [`${ns}:tick`] }) + content: json({ values: [id("tick")] }) }); return files; } + // src/shaders/entity.fsh + var entity_default = "#version 330\n\n#moj_import \n#moj_import \n\nuniform sampler2D Sampler0;\n\n#ifdef DISSOLVE\nuniform sampler2D DissolveMaskSampler;\n#endif\n\nin float sphericalVertexDistance;\nin float cylindricalVertexDistance;\n#ifdef PER_FACE_LIGHTING\nin vec4 vertexPerFaceColorBack;\nin vec4 vertexPerFaceColorFront;\n#else\nin vec4 vertexColor;\n#endif\n\n#ifndef EMISSIVE\nin vec4 lightMapColor;\n#endif\n\n#ifndef NO_OVERLAY\nin vec4 overlayColor;\n#endif\n\nin vec2 texCoord0;\nin vec2 modelTexCoord;\nin vec3 modelPosition;\n\nout vec4 fragColor;\n\nconst float SKIN_SIZE = 64.0;\nconst float HEAD_FACE_SIZE = 8.0;\nconst float RIGHT_HAND_TAG_SCALE = 0.99;\nconst vec3 HAND_MODEL_SCALE = vec3(0.471, 0.515, 1.515);\nconst vec3 modelScaleF = 0.5 * HAND_MODEL_SCALE;\nconst vec3 modelScaleS = modelScaleF + (0.25 / 8.0) * HAND_MODEL_SCALE;\nconst vec3 hRefF = vec3(length(modelScaleF.xz), length(modelScaleF.yz), length(modelScaleF.xy));\nconst vec3 hRefS = vec3(length(modelScaleS.xz), length(modelScaleS.yz), length(modelScaleS.xy));\n\nconst ivec4 armUV[] = ivec4[](\n ivec4(40, 52, 36, 64),\n ivec4(44, 64, 48, 52),\n ivec4(36, 64, 32, 52),\n ivec4(44, 52, 40, 48),\n ivec4(40, 52, 44, 64),\n ivec4(36, 52, 40, 48)\n);\n\nconst ivec4 slimArmUV[] = ivec4[](\n ivec4(39, 52, 36, 64),\n ivec4(43, 64, 46, 52),\n ivec4(36, 64, 32, 52),\n ivec4(42, 52, 39, 48),\n ivec4(39, 52, 43, 64),\n ivec4(36, 52, 39, 48)\n);\n\nconst bool armRotateUV[] = bool[](\n false, false, true, false, true, false\n);\n\nbool testDim(float h, float hRef) {\n return abs(h - hRef) < 0.001;\n}\n\nbool testDims(float h, vec3 hRef) {\n return testDim(h, hRef.x) || testDim(h, hRef.y) || testDim(h, hRef.z);\n}\n\nbool isSlimSkin() {\n vec4 samp1 = texture(Sampler0, vec2(54.0 / SKIN_SIZE, 20.0 / SKIN_SIZE));\n vec4 samp2 = texture(Sampler0, vec2(55.0 / SKIN_SIZE, 20.0 / SKIN_SIZE));\n return samp1.a == 0.0 || (((samp1.r + samp1.g + samp1.b) == 0.0)\n && ((samp2.r + samp2.g + samp2.b) == 0.0)\n && samp1.a == 1.0 && samp2.a == 1.0);\n}\n\nfloat faceDiagonal() {\n vec3 dpdx = dFdx(modelPosition);\n vec3 dpdy = dFdy(modelPosition);\n vec2 duvdx = dFdx(modelTexCoord);\n vec2 duvdy = dFdy(modelTexCoord);\n float determinant = duvdx.x * duvdy.y - duvdx.y * duvdy.x;\n if (abs(determinant) < 1e-10) {\n return 0.0;\n }\n\n vec3 dpdu = (dpdx * duvdy.y - dpdy * duvdx.y) / determinant;\n vec3 dpdv = (dpdy * duvdx.x - dpdx * duvdy.x) / determinant;\n return length((dpdv - dpdu) * (HEAD_FACE_SIZE / SKIN_SIZE));\n}\n\nbool decodeHeadUV(vec2 sourceCoord, out int face, out bool overlay, out vec2 faceCoord) {\n vec2 pixel = sourceCoord * SKIN_SIZE;\n overlay = pixel.x >= 32.0;\n if (overlay) {\n pixel.x -= 32.0;\n }\n\n vec2 faceOrigin;\n if (pixel.y >= 0.0 && pixel.y <= 8.0 && pixel.x >= 8.0 && pixel.x <= 24.0) {\n if (pixel.x < 16.0) {\n face = 0;\n faceOrigin = vec2(8.0, 0.0);\n } else {\n face = 1;\n faceOrigin = vec2(16.0, 0.0);\n }\n } else if (pixel.y >= 8.0 && pixel.y <= 16.0 && pixel.x >= 0.0 && pixel.x <= 32.0) {\n if (pixel.x < 8.0) {\n face = 2;\n faceOrigin = vec2(0.0, 8.0);\n } else if (pixel.x < 16.0) {\n face = 3;\n faceOrigin = vec2(8.0, 8.0);\n } else if (pixel.x < 24.0) {\n face = 4;\n faceOrigin = vec2(16.0, 8.0);\n } else {\n face = 5;\n faceOrigin = vec2(24.0, 8.0);\n }\n } else {\n return false;\n }\n\n faceCoord = clamp((pixel - faceOrigin) / HEAD_FACE_SIZE, vec2(0.0), vec2(1.0));\n return true;\n}\n\nvec2 armTexCoord(int face, bool overlay, bool rightHand, bool slim, vec2 sourceFaceCoord) {\n ivec4 uvData = slim ? slimArmUV[face] : armUV[face];\n if (rightHand) {\n uvData += ivec4(8, -32, 8, -32);\n if (overlay) {\n uvData.yw += 16;\n }\n } else if (overlay) {\n uvData.xz += 16;\n }\n\n vec2 result;\n if (armRotateUV[face]) {\n result.x = mix(float(uvData.x), float(uvData.z), sourceFaceCoord.y);\n result.y = mix(float(uvData.w), float(uvData.y), sourceFaceCoord.x);\n } else {\n result.x = mix(float(uvData.z), float(uvData.x), sourceFaceCoord.x);\n result.y = mix(float(uvData.y), float(uvData.w), sourceFaceCoord.y);\n }\n return result / SKIN_SIZE;\n}\n\nvoid main() {\n vec2 texCoord = texCoord0;\n\n if (textureSize(Sampler0, 0) == ivec2(64, 64)) {\n float h = faceDiagonal();\n bool leftHand = testDims(h, hRefF) || testDims(h, hRefS);\n bool rightHand = testDims(h, hRefF * RIGHT_HAND_TAG_SCALE)\n || testDims(h, hRefS * RIGHT_HAND_TAG_SCALE);\n\n int face;\n bool overlay;\n vec2 sourceFaceCoord;\n if ((leftHand || rightHand) && decodeHeadUV(modelTexCoord, face, overlay, sourceFaceCoord)) {\n // The player-head bottom face is 180 degrees opposite to the arm UV orientation.\n if (face == 1) {\n sourceFaceCoord = vec2(1.0) - sourceFaceCoord;\n }\n texCoord = armTexCoord(face, overlay, rightHand, isSlimSkin(), sourceFaceCoord);\n }\n }\n\n vec4 color = texture(Sampler0, texCoord);\n#ifdef ALPHA_CUTOUT\n if (color.a < ALPHA_CUTOUT) {\n discard;\n }\n#endif\n\n#ifdef PER_FACE_LIGHTING\n vec4 faceVertexColor = gl_FrontFacing ? vertexPerFaceColorFront : vertexPerFaceColorBack;\n#else\n vec4 faceVertexColor = vertexColor;\n#endif\n\n#ifdef DISSOLVE\n if (faceVertexColor.a < texture(DissolveMaskSampler, texCoord).a) {\n discard;\n }\n faceVertexColor.a = 1.0;\n#endif\n\n color *= faceVertexColor * ColorModulator;\n#ifndef NO_OVERLAY\n color.rgb = mix(overlayColor.rgb, color.rgb, overlayColor.a);\n#endif\n#ifndef EMISSIVE\n color *= lightMapColor;\n#endif\n\n fragColor = apply_fog(color, sphericalVertexDistance, cylindricalVertexDistance, FogEnvironmentalStart, FogEnvironmentalEnd, FogRenderDistanceStart, FogRenderDistanceEnd, FogColor);\n}\n"; + + // src/shaders/entity.vsh + var entity_default2 = "#version 330\n\n#if defined(PER_FACE_LIGHTING) || !defined(NO_CARDINAL_LIGHTING)\n#moj_import \n#endif\n#moj_import \n#moj_import \n#moj_import \n#moj_import \n\nin vec3 Position;\nin vec4 Color;\nin vec2 UV0;\nin ivec2 UV1;\nin ivec2 UV2;\nin vec3 Normal;\n\n#ifndef NO_OVERLAY\nuniform sampler2D Sampler1;\n#endif\n\n#ifndef EMISSIVE\nuniform sampler2D Sampler2;\n#endif\n\nout float sphericalVertexDistance;\nout float cylindricalVertexDistance;\n\n#ifdef PER_FACE_LIGHTING\nout vec4 vertexPerFaceColorBack;\nout vec4 vertexPerFaceColorFront;\n#else\nout vec4 vertexColor;\n#endif\n\n#ifndef EMISSIVE\nout vec4 lightMapColor;\n#endif\n\n#ifndef NO_OVERLAY\nout vec4 overlayColor;\n#endif\n\nout vec2 texCoord0;\nout vec2 modelTexCoord;\nout vec3 modelPosition;\n\nvoid main() {\n gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0);\n\n sphericalVertexDistance = fog_spherical_distance(Position);\n cylindricalVertexDistance = fog_cylindrical_distance(Position);\n\n#ifdef PER_FACE_LIGHTING\n vec2 light = minecraft_compute_light(Light0_Direction, Light1_Direction, Normal);\n vertexPerFaceColorBack = minecraft_mix_light_separate(-light, Color);\n vertexPerFaceColorFront = minecraft_mix_light_separate(light, Color);\n#elif defined(NO_CARDINAL_LIGHTING)\n vertexColor = Color;\n#else\n vertexColor = minecraft_mix_light(Light0_Direction, Light1_Direction, Normal, Color);\n#endif\n\n#ifndef EMISSIVE\n lightMapColor = sample_lightmap(Sampler2, UV2);\n#endif\n\n#ifndef NO_OVERLAY\n overlayColor = texelFetch(Sampler1, UV1, 0);\n#endif\n\n modelTexCoord = UV0;\n modelPosition = Position;\n texCoord0 = UV0;\n\n#ifdef APPLY_TEXTURE_MATRIX\n texCoord0 = (TextureMat * vec4(UV0, 0.0, 1.0)).xy;\n#endif\n}\n"; + + // src/hand-rendering.ts + var HAND_SHADER_PATHS = /* @__PURE__ */ new Set([ + "assets/minecraft/shaders/core/entity.vsh", + "assets/minecraft/shaders/core/entity.fsh" + ]); + var HIDDEN_CONTEXTS = [ + "gui", + "fixed", + "ground", + "thirdperson_righthand", + "thirdperson_lefthand", + "head", + "on_shelf" + ]; + function hiddenDisplays() { + return Object.fromEntries(HIDDEN_CONTEXTS.map((context) => [context, { scale: [0, 0, 0] }])); + } + function handBaseModel(side, particleTexture, pose, display) { + const animated = animatedHandDisplay(side, pose); + return { + textures: { particle: particleTexture }, + display: { + firstperson_righthand: pose && display ? previewAlignedHandDisplay(side, pose, display.firstperson_righthand) : animated, + firstperson_lefthand: pose && display ? previewAlignedHandDisplay(side, pose, display.firstperson_lefthand ?? display.firstperson_righthand, true) : animated, + ...hiddenDisplays() + } + }; + } + function playerSkinHands(model, projectName, bases) { + const special = (side) => ({ + type: "minecraft:special", + base: bases?.[side] ?? `jsb:${projectName}/_hand/${side}`, + model: { type: "minecraft:player_head" }, + transformation: handSpecialTransformation() + }); + return { + type: "minecraft:composite", + models: [model, special("left"), special("right")] + }; + } + function handRenderingFiles(projectName, particleTexture) { + const json3 = (value) => `${JSON.stringify(value, null, 2)} +`; + return [ + { + path: `assets/jsb/models/${projectName}/_hand/left.json`, + content: json3(handBaseModel("left", particleTexture)) + }, + { + path: `assets/jsb/models/${projectName}/_hand/right.json`, + content: json3(handBaseModel("right", particleTexture)) + }, + { path: "assets/minecraft/shaders/core/entity.vsh", content: entity_default2 }, + { path: "assets/minecraft/shaders/core/entity.fsh", content: entity_default } + ]; + } + // src/file-writer.ts - var MANIFEST_NAME = ".display-anim-preview-manifest.json"; - function getScopedFs(scopeRoot, prompt) { + var SHARED_TAGS = /* @__PURE__ */ new Set([ + "data/minecraft/tags/function/load.json", + "data/minecraft/tags/function/tick.json" + ]); + function getScopedFs(scopeRoot) { const fs = requireNativeModule("fs", { scope: scopeRoot, message: tr("dap.permission.export"), - show_permission_dialog: prompt + show_permission_dialog: true }); - return fs ?? null; + if (!fs) throw new Error(tr("dap.error.write_permission")); + return fs; } function getPathModule() { return requireNativeModule("path"); } - function isSafeGeneratedPath(relativePath) { - if (!relativePath || relativePath.startsWith("/") || relativePath.includes("..")) { - return false; + function manifestRelativePath(kind) { + return kind === "resource" ? "assets.jsbmeta" : "data.jsbmeta"; + } + function isSafeRelativePath(relativePath) { + return Boolean(relativePath) && !relativePath.startsWith("/") && !relativePath.includes("\\") && !relativePath.includes("\0") && !relativePath.split("/").some((segment) => !segment || segment === "." || segment === ".."); + } + function isOwnedProjectPath(path, project) { + const ns = EXPORT_NAMESPACE; + return path === `assets/${ns}/items/${project}.json` || path.startsWith(`assets/${ns}/models/${project}/`) || path.startsWith(`assets/${ns}/textures/item/${project}/`) || path.startsWith(`data/${ns}/function/${project}/`) || path.startsWith(`data/${ns}/item_modifier/${project}/`) || path.startsWith(`data/${ns}/loot_table/${project}/`); + } + function isManagedProjectPath(path, project, kind) { + return isOwnedProjectPath(path, project) || kind === "resource" && HAND_SHADER_PATHS.has(path); + } + function validateGeneratedPath(path, target) { + if (!isSafeRelativePath(path)) { + throw new Error(tr("dap.error.unsafe_path", { path })); + } + if (path !== "pack.mcmeta" && !SHARED_TAGS.has(path) && !isManagedProjectPath(path, target.projectName, target.kind)) { + throw new Error(tr("dap.error.unsafe_path", { path })); } - return relativePath === "pack.mcmeta" || relativePath.startsWith("assets/") || relativePath.startsWith("data/"); } - function cleanPreviousGeneratedFiles(fs, pathModule, target) { - const manifestPath = pathModule.join(target.root, MANIFEST_NAME); - if (!fs.existsSync(manifestPath)) return; - let previous = []; + function readManifest(fs, pathModule, target) { + const fullPath = pathModule.join(target.root, manifestRelativePath(target.kind)); + if (!fs.existsSync(fullPath)) return { version: 1, projects: {} }; + let value; try { - const parsed = JSON.parse(fs.readFileSync(manifestPath, "utf8")); - if (Array.isArray(parsed.files)) { - previous = parsed.files.filter((value) => typeof value === "string"); + value = JSON.parse(fs.readFileSync(fullPath, "utf8")); + } catch (error) { + throw new Error(tr("dap.error.manifest_invalid", { path: fullPath })); + } + if (value.version !== 1 || !value.projects || typeof value.projects !== "object") { + throw new Error(tr("dap.error.manifest_invalid", { path: fullPath })); + } + for (const [project, entry] of Object.entries(value.projects)) { + if (!entry || entry.kind !== "resource" && entry.kind !== "datapack" || !Array.isArray(entry.files) || entry.files.some((path) => typeof path !== "string" || !isManagedProjectPath(path, project, entry.kind))) { + throw new Error(tr("dap.error.manifest_invalid", { path: fullPath })); } - } catch (err) { - console.warn("Could not read the previous export manifest; skipping stale-file cleanup", err); - return; } - const current = new Set(target.files.map((file) => file.path)); - for (const relativePath of previous) { - if (current.has(relativePath) || !isSafeGeneratedPath(relativePath)) continue; - const fullPath = pathModule.join(target.root, relativePath); - if (fs.existsSync(fullPath)) fs.unlinkSync(fullPath); + return value; + } + function validateExistingPack(fs, pathModule, target) { + const packPath = pathModule.join(target.root, "pack.mcmeta"); + if (!target.insert) return; + if (!fs.existsSync(packPath)) { + throw new Error(tr("dap.error.invalid_pack", { path: target.root })); + } + try { + const parsed = JSON.parse(fs.readFileSync(packPath, "utf8")); + if (!parsed || typeof parsed.pack !== "object") throw new Error("missing pack object"); + } catch (error) { + throw new Error(tr("dap.error.invalid_pack", { path: target.root })); } } - function writePacks(scopeRoot, targets) { - const fs = getScopedFs(scopeRoot, true); - if (!fs) { - throw new Error(tr("dap.error.write_permission")); + function mergeFunctionTag(existing, generated, path) { + let generatedValue; + try { + const parsed = JSON.parse(generated); + generatedValue = parsed.values?.[0]; + if (typeof generatedValue !== "string") throw new Error("missing generated value"); + } catch (error) { + throw new Error(tr("dap.error.shared_tag_invalid", { path })); } - const pathModule = getPathModule(); - let written = 0; - for (const target of targets) { - cleanPreviousGeneratedFiles(fs, pathModule, target); - for (const file of target.files) { + if (!existing) return `${JSON.stringify({ values: [generatedValue] }, null, 2)} +`; + try { + const parsed = JSON.parse(existing); + if (!Array.isArray(parsed.values)) throw new Error("missing values"); + if (!parsed.values.some((value) => value === generatedValue)) parsed.values.push(generatedValue); + return `${JSON.stringify(parsed, null, 2)} +`; + } catch (error) { + throw new Error(tr("dap.error.shared_tag_invalid", { path })); + } + } + function prepareTarget(target, fs, pathModule) { + for (const file of target.files) validateGeneratedPath(file.path, target); + validateExistingPack(fs, pathModule, target); + const manifest = readManifest(fs, pathModule, target); + const previousEntry = manifest.projects[target.projectName]; + if (previousEntry && previousEntry.kind !== target.kind) { + throw new Error(tr("dap.error.manifest_invalid", { path: pathModule.join(target.root, manifestRelativePath(target.kind)) })); + } + const owned = new Set(previousEntry?.files ?? []); + const desired = /* @__PURE__ */ new Map(); + let merged = 0; + for (const file of target.files) { + if (file.path === "pack.mcmeta") { const fullPath = pathModule.join(target.root, file.path); - fs.mkdirSync(pathModule.dirname(fullPath), { recursive: true }); - Blockbench.writeFile(fullPath, { - content: file.content, - savetype: file.isImage ? "image" : "text" - }); - if (!fs.existsSync(fullPath)) { - throw new Error(tr("dap.error.file_not_written", { path: fullPath })); - } - if (!file.isImage && fs.readFileSync(fullPath, "utf8") !== file.content) { - throw new Error(tr("dap.error.file_verify", { path: fullPath })); - } - written++; + if (target.insert) continue; + if (fs.existsSync(fullPath)) continue; + } + if (SHARED_TAGS.has(file.path)) { + const fullPath = pathModule.join(target.root, file.path); + const existing = fs.existsSync(fullPath) ? fs.readFileSync(fullPath, "utf8") : null; + const content2 = mergeFunctionTag(existing, file.content, file.path); + desired.set(file.path, { ...file, content: content2 }); + if (existing !== content2) merged++; + continue; + } + desired.set(file.path, file); + } + const currentOwned = new Set( + [...desired.keys()].filter((path) => isManagedProjectPath(path, target.projectName, target.kind)) + ); + const sharedOwnedByAnotherProject = (path) => Object.entries(manifest.projects).some( + ([project, entry]) => project !== target.projectName && entry.kind === "resource" && entry.files.includes(path) + ); + const stale = [...owned].filter( + (path) => !currentOwned.has(path) && (!HAND_SHADER_PATHS.has(path) || !sharedOwnedByAnotherProject(path)) + ); + const conflicts = []; + for (const relativePath of currentOwned) { + const fullPath = pathModule.join(target.root, relativePath); + if (fs.existsSync(fullPath) && !owned.has(relativePath)) { + const generated = desired.get(relativePath); + const identicalSharedShader = HAND_SHADER_PATHS.has(relativePath) && generated && fs.readFileSync(fullPath, "utf8") === generated.content; + if (!identicalSharedShader) conflicts.push(fullPath); } - const manifestPath = pathModule.join(target.root, MANIFEST_NAME); - const manifestContent = `${JSON.stringify( - { version: 1, files: target.files.map((file) => file.path) }, + } + for (const relativePath of stale) { + if (!isManagedProjectPath(relativePath, target.projectName, target.kind)) { + throw new Error(tr("dap.error.unsafe_path", { path: relativePath })); + } + } + const manifestPath = pathModule.join(target.root, manifestRelativePath(target.kind)); + const nextProjects = { ...manifest.projects, [target.projectName]: { kind: target.kind, files: [...currentOwned].sort() } }; + desired.set(manifestRelativePath(target.kind), { + path: manifestRelativePath(target.kind), + content: `${JSON.stringify( + { + version: 1, + projects: nextProjects + }, null, 2 )} +` + }); + let added = 0; + let updated = 0; + for (const relativePath of desired.keys()) { + if (SHARED_TAGS.has(relativePath)) continue; + if (fs.existsSync(pathModule.join(target.root, relativePath))) updated++; + else added++; + } + return { + target, + fs, + path: pathModule, + manifestPath, + desired, + owned, + stale, + preview: { root: target.root, added, updated, removed: stale.length, merged }, + conflicts + }; + } + function prepareTargets(targets) { + const roots = /* @__PURE__ */ new Set(); + const filesystems = /* @__PURE__ */ new Map(); + const pathModule = getPathModule(); + return targets.map((target) => { + if (roots.has(target.root)) { + throw new Error(tr("dap.error.duplicate_target", { path: target.root })); + } + roots.add(target.root); + let fs = filesystems.get(target.scopeRoot); + if (!fs) { + fs = getScopedFs(target.scopeRoot); + filesystems.set(target.scopeRoot, fs); + } + return prepareTarget(target, fs, pathModule); + }); + } + function previewPacks(targets) { + const prepared = prepareTargets(targets); + const previews = prepared.map((entry) => entry.preview); + return { + added: previews.reduce((sum, value) => sum + value.added, 0), + updated: previews.reduce((sum, value) => sum + value.updated, 0), + removed: previews.reduce((sum, value) => sum + value.removed, 0), + merged: previews.reduce((sum, value) => sum + value.merged, 0), + conflicts: prepared.flatMap((entry) => entry.conflicts), + targets: previews + }; + } + function writeStagedFile(fs, pathModule, stagingRoot, file) { + const staged = pathModule.join(stagingRoot, "files", file.path); + fs.mkdirSync(pathModule.dirname(staged), { recursive: true }); + Blockbench.writeFile(staged, { + content: file.content, + savetype: file.isImage ? "image" : "text" + }); + if (!fs.existsSync(staged)) throw new Error(tr("dap.error.file_not_written", { path: staged })); + if (!file.isImage && fs.readFileSync(staged, "utf8") !== file.content) { + throw new Error(tr("dap.error.file_verify", { path: staged })); + } + return staged; + } + function pruneEmptyParents(entry, relativePaths) { + const root = entry.target.root; + for (const relativePath of relativePaths) { + let directory = entry.path.dirname(entry.path.join(root, relativePath)); + while (directory !== root) { + if (!entry.fs.existsSync(directory)) { + directory = entry.path.dirname(directory); + continue; + } + if (entry.fs.readdirSync(directory, { withFileTypes: true }).length) break; + entry.fs.rmdirSync(directory); + directory = entry.path.dirname(directory); + } + } + } + function removeTransactionRoot(entry, root) { + entry.fs.rmSync(root, { recursive: true, force: true }); + const parent = entry.path.dirname(root); + if (entry.fs.existsSync(parent) && entry.fs.readdirSync(parent, { withFileTypes: true }).length === 0) { + entry.fs.rmdirSync(parent); + } + } + function writePacks(targets) { + const prepared = prepareTargets(targets); + const conflicts = prepared.flatMap((entry) => entry.conflicts); + if (conflicts.length) { + throw new Error(tr("dap.error.path_conflicts", { paths: conflicts.join("\n") })); + } + const staged = /* @__PURE__ */ new Map(); + const mutations = []; + try { + prepared.forEach((entry, targetIndex) => { + const stagingRoot = entry.path.join( + entry.target.root, + `.jsb-transaction-${entry.target.projectName}-${Date.now()}-${targetIndex}` + ); + const stagedFiles = /* @__PURE__ */ new Map(); + for (const file of entry.desired.values()) { + stagedFiles.set( + file.path, + writeStagedFile(entry.fs, entry.path, stagingRoot, file) + ); + } + staged.set(entry, { root: stagingRoot, files: stagedFiles }); + }); + for (const entry of prepared) { + const stagedTarget = staged.get(entry); + let backupIndex = 0; + const replaceOrRemove = [...entry.desired.keys(), ...entry.stale]; + for (const relativePath of replaceOrRemove) { + const finalPath = entry.path.join(entry.target.root, relativePath); + let backupPath = null; + if (entry.fs.existsSync(finalPath)) { + backupPath = entry.path.join(stagedTarget.root, "backups", String(backupIndex++)); + entry.fs.mkdirSync(entry.path.dirname(backupPath), { recursive: true }); + entry.fs.renameSync(finalPath, backupPath); + } + mutations.push({ prepared: entry, finalPath, backupPath }); + const stagedPath = stagedTarget.files.get(relativePath); + if (stagedPath) { + entry.fs.mkdirSync(entry.path.dirname(finalPath), { recursive: true }); + entry.fs.renameSync(stagedPath, finalPath); + } + } + } + } catch (error) { + const rollbackFailures = []; + const rollbackErrors = []; + for (const mutation of [...mutations].reverse()) { + const { fs } = mutation.prepared; + try { + if (fs.existsSync(mutation.finalPath)) fs.unlinkSync(mutation.finalPath); + if (mutation.backupPath && fs.existsSync(mutation.backupPath)) { + fs.mkdirSync(mutation.prepared.path.dirname(mutation.finalPath), { recursive: true }); + fs.renameSync(mutation.backupPath, mutation.finalPath); + } + } catch (rollbackError) { + rollbackErrors.push(rollbackError); + rollbackFailures.push(mutation.finalPath); + } + } + for (const [entry, value] of staged) { + try { + removeTransactionRoot(entry, value.root); + } catch (cleanupError) { + console.warn("Could not clean failed JSB transaction", cleanupError); + } + } + if (rollbackFailures.length) { + console.error("JSB transaction rollback could not restore some files", rollbackErrors); + throw new Error(`${tr("dap.error.rollback_partial", { paths: rollbackFailures.join("\n") })} +${error instanceof Error ? error.message : String(error)}`); + } + throw error; + } + for (const [entry, value] of staged) { + try { + removeTransactionRoot(entry, value.root); + pruneEmptyParents(entry, entry.stale); + const transactionRoot = entry.path.dirname(value.root); + if (entry.fs.existsSync(transactionRoot) && entry.fs.readdirSync(transactionRoot, { withFileTypes: true }).length === 0) { + entry.fs.rmdirSync(transactionRoot); + } + const legacyTransactionRoot = entry.path.join(entry.target.root, ".jsb-transactions"); + if (entry.fs.existsSync(legacyTransactionRoot) && entry.fs.readdirSync(legacyTransactionRoot, { withFileTypes: true }).length === 0) { + entry.fs.rmdirSync(legacyTransactionRoot); + } + } catch (error) { + console.warn("Could not fully clean completed JSB transaction", error); + } + } + return prepared.reduce((sum, entry) => sum + entry.desired.size, 0); + } + + // src/resource-pack.ts + var RESOURCE_PACK_FORMAT = [88, 0]; + function json2(value) { + return `${JSON.stringify(value, null, 2)} `; - Blockbench.writeFile(manifestPath, { - content: manifestContent, - savetype: "text" + } + function sanitizeTextureName(name, fallbackIndex) { + const safe = name.replace(/\.png$/i, "").toLowerCase().replace(/[^a-z0-9_-]+/g, "_").replace(/^_+|_+$/g, ""); + return safe || `texture_${fallbackIndex}`; + } + function collectTextures() { + const used = /* @__PURE__ */ new Set(); + const previewTextureUuid = typeof Project === "undefined" ? void 0 : Project?.display_anim_export_settings?.handPreviewTextureUuid; + return Texture.all.filter( + (texture) => (!previewTextureUuid || texture.uuid !== previewTextureUuid) && texture[PREVIEW_TEXTURE_PROPERTY] !== true && texture.name !== "DAP_Default_Player_Skin.png" && texture.name !== "missing.png" + ).map((texture, index) => { + const base = sanitizeTextureName(texture.name, index); + let name = base; + let suffix = 2; + while (used.has(name)) name = `${base}_${suffix++}`; + used.add(name); + return { + id: String(texture.id), + name, + link: texture.javaTextureLink(), + dataUrl: texture.getDataURL() + }; + }); + } + function rewriteTextureRefs(model, projectName, textures) { + if (!model.textures) return; + for (const key of Object.keys(model.textures)) { + const value = model.textures[key]; + if (value.startsWith("#")) continue; + const valueStem = value.split("/").pop()?.replace(/\.png$/i, ""); + const texture = textures.find( + (candidate) => candidate.id === key || candidate.link === value || candidate.name === key || candidate.name === valueStem + ); + if (texture) model.textures[key] = `${EXPORT_NAMESPACE}:item/${projectName}/${texture.name}`; + } + if (!model.textures.particle) { + const firstTextureKey = Object.keys(model.textures).find( + (key) => key !== "particle" && !model.textures[key].startsWith("#") + ); + if (firstTextureKey) model.textures.particle = `#${firstTextureKey}`; + } + } + function sanitizeTextureRefs(model, projectName, textureNames) { + const prefix2 = `${EXPORT_NAMESPACE}:item/${projectName}/`; + const textures = model.textures ?? {}; + let omittedFaces = 0; + let omittedElements = 0; + const validateResolvedTexture = (value, label) => { + if (!value.startsWith(prefix2)) { + throw new Error(tr("dap.error.external_texture", { label, value })); + } + const stem = value.slice(prefix2.length); + if (!textureNames.has(stem)) { + throw new Error(tr("dap.error.texture_not_generated", { value })); + } + }; + const resolveTexture = (reference) => { + let value = reference; + const visited = /* @__PURE__ */ new Set(); + while (value.startsWith("#")) { + const key = value.slice(1); + if (!key || key === "missing" || visited.has(key) || !textures[key]) return null; + visited.add(key); + value = textures[key]; + } + return value; + }; + for (const [key, rawValue] of Object.entries(textures)) { + const value = resolveTexture(rawValue); + if (value) validateResolvedTexture(value, `#${key}`); + } + for (const element of model.elements ?? []) { + for (const [faceName, face] of Object.entries(element.faces ?? {})) { + const value = face.texture ? resolveTexture(face.texture) : null; + if (!value) { + delete element.faces?.[faceName]; + omittedFaces++; + } else { + validateResolvedTexture(value, face.texture ?? faceName); + } + } + } + if (model.elements) { + model.elements = model.elements.filter((element) => { + if (Object.keys(element.faces ?? {}).length) return true; + omittedElements++; + return false; }); - if (!fs.existsSync(manifestPath)) { - throw new Error(tr("dap.error.manifest_not_written", { path: manifestPath })); + } + return { omittedFaces, omittedElements }; + } + function animatedModel(sequence, projectName, includeHands) { + const frameModel = (frame2) => { + const model = { type: "minecraft:model", model: sequence.modelPaths[frame2] }; + return includeHands ? playerSkinHands(model, projectName, sequence.handBasePaths[frame2]) : model; + }; + if ([...new Set(sequence.modelPaths)].length === 1 && (!includeHands || sequence.handBasePaths.every((paths) => JSON.stringify(paths) === JSON.stringify(sequence.handBasePaths[0])))) { + return frameModel(0); + } + return { + type: "minecraft:range_dispatch", + property: "minecraft:custom_model_data", + index: 0, + fallback: frameModel(0), + entries: sequence.modelPaths.map((_model, frame2) => ({ + threshold: frame2, + model: frameModel(frame2) + })) + }; + } + function selectableAnimationModel(sequences, defaultAnimationKey, projectName, includeHands) { + const fallback = sequences.find((sequence) => sequence.key === defaultAnimationKey); + if (!fallback) throw new Error(`Unknown default animation key: ${defaultAnimationKey}`); + return { + type: "minecraft:select", + property: "minecraft:custom_model_data", + index: 0, + cases: sequences.map((sequence) => ({ + when: sequence.key, + model: animatedModel(sequence, projectName, includeHands) + })), + fallback: animatedModel(fallback, projectName, includeHands) + }; + } + function buildItemDefinition(options, staticModelPath, contextSequences, defaultHandBases) { + const staticModel = { type: "minecraft:model", model: staticModelPath }; + const cases = options.displayContexts.map((route) => { + const includeHands = options.handRenderingEnabled === true && (route.context === "firstperson_righthand" || route.context === "firstperson_lefthand"); + const routedModel = route.animated ? selectableAnimationModel( + contextSequences.get(route.context) ?? [], + options.defaultAnimationKey, + options.projectName, + includeHands + ) : includeHands ? playerSkinHands(staticModel, options.projectName, defaultHandBases) : staticModel; + return { + when: route.context, + model: routedModel + }; + }); + return json2({ + model: { + type: "minecraft:select", + property: "minecraft:display_context", + cases, + fallback: staticModel + }, + swap_animation_scale: 0 + }); + } + function buildResourcePack(sequences, options) { + if (!sequences.length || sequences.some((sequence) => !sequence.frames.length)) { + throw new Error(tr("dap.error.no_models")); + } + const keys = /* @__PURE__ */ new Set(); + for (const sequence of sequences) { + if (!sequence.key || keys.has(sequence.key)) { + throw new Error(`Duplicate or empty animation key: ${sequence.key || ""}`); + } + keys.add(sequence.key); + } + if (!keys.has(options.defaultAnimationKey)) { + throw new Error(`Unknown default animation key: ${options.defaultAnimationKey}`); + } + const files = []; + const assetRoot = `assets/${EXPORT_NAMESPACE}`; + const modelRoot = `${assetRoot}/models/${options.projectName}`; + const textures = collectTextures(); + const textureNames = new Set(textures.map((texture) => texture.name)); + const uniqueModels = /* @__PURE__ */ new Map(); + const basePaths = /* @__PURE__ */ new Map(); + let sampledFrames = 0; + let modelBytesBefore = 0; + let modelBytesAfter = 0; + let omittedUntexturedFaces = 0; + let omittedEmptyElements = 0; + let particleTexture = "minecraft:block/white_concrete"; + files.push({ + path: "pack.mcmeta", + content: json2({ + pack: { + description: options.description, + min_format: RESOURCE_PACK_FORMAT, + max_format: RESOURCE_PACK_FORMAT + } + }) + }); + if (options.handRenderingEnabled) { + particleTexture = textures[0] ? `${EXPORT_NAMESPACE}:item/${options.projectName}/${textures[0].name}` : "minecraft:block/white_concrete"; + files.push(...handRenderingFiles(options.projectName, particleTexture)); + } + const handModelPaths = /* @__PURE__ */ new Map(); + const resolveHandBasePaths = (frame2) => { + if (!options.handRenderingEnabled || !frame2?.hands) return void 0; + const key = JSON.stringify([frame2.hands, frame2.model.display]); + const cached = handModelPaths.get(key); + if (cached) return cached; + const index = handModelPaths.size; + const paths = { + left: `${EXPORT_NAMESPACE}:${options.projectName}/_hand/_generated/left_${index}`, + right: `${EXPORT_NAMESPACE}:${options.projectName}/_hand/_generated/right_${index}` + }; + for (const side of ["left", "right"]) { + files.push({ + path: `${modelRoot}/_hand/_generated/${side}_${index}.json`, + content: json2(handBaseModel(side, particleTexture, frame2.hands[side], frame2.model.display ?? {})) + }); + } + handModelPaths.set(key, paths); + return paths; + }; + for (const sequence of sequences) { + const paths = []; + for (const frame2 of sequence.frames) { + rewriteTextureRefs(frame2.model, options.projectName, textures); + const sanitized = sanitizeTextureRefs(frame2.model, options.projectName, textureNames); + omittedUntexturedFaces += sanitized.omittedFaces; + omittedEmptyElements += sanitized.omittedElements; + const finalJson = JSON.stringify(frame2.model); + sampledFrames++; + modelBytesBefore += finalJson.length; + let modelPath = uniqueModels.get(finalJson); + if (!modelPath) { + const index = uniqueModels.size; + modelPath = `${EXPORT_NAMESPACE}:${options.projectName}/_generated/model_${index}`; + uniqueModels.set(finalJson, modelPath); + modelBytesAfter += finalJson.length; + files.push({ + path: `${modelRoot}/_generated/model_${index}.json`, + content: `${finalJson} +` + }); + } + paths.push(modelPath); } - if (fs.readFileSync(manifestPath, "utf8") !== manifestContent) { - throw new Error(tr("dap.error.manifest_verify", { path: manifestPath })); + basePaths.set(sequence.key, paths); + } + const contextSequences = /* @__PURE__ */ new Map(); + const animatedContextFolders = []; + for (const route of options.displayContexts) { + if (!route.animated) continue; + const shortName = DISPLAY_CONTEXT_PATHS[route.context]; + if (!shortName) throw new Error(`Unknown display context: ${route.context}`); + animatedContextFolders.push(shortName); + const routedSequences = []; + for (const sequence of sequences) { + const paths = basePaths.get(sequence.key); + const aliases = paths.map((parent, frame2) => { + const alias = `${EXPORT_NAMESPACE}:${options.projectName}/${sequence.key}/${shortName}/${frame2}`; + const content2 = json2({ parent }); + modelBytesAfter += content2.length; + files.push({ + path: `${modelRoot}/${sequence.key}/${shortName}/${frame2}.json`, + content: content2 + }); + return alias; + }); + routedSequences.push({ + key: sequence.key, + sourceName: sequence.sourceName, + modelPaths: aliases, + handBasePaths: sequence.frames.map((frame2) => resolveHandBasePaths(frame2)) + }); + } + contextSequences.set(route.context, routedSequences); + } + const defaultPath = basePaths.get(options.defaultAnimationKey)?.[0]; + if (!defaultPath) throw new Error(tr("dap.error.no_models")); + files.push({ + path: `${assetRoot}/items/${options.projectName}.json`, + content: buildItemDefinition( + options, + defaultPath, + contextSequences, + resolveHandBasePaths( + sequences.find((sequence) => sequence.key === options.defaultAnimationKey)?.frames[0] + ) + ) + }); + for (const texture of textures) { + files.push({ + path: `${assetRoot}/textures/item/${options.projectName}/${texture.name}.png`, + content: texture.dataUrl, + isImage: true + }); + } + return { + files, + report: { + sampledFrames, + uniqueModels: uniqueModels.size, + duplicateFrames: sampledFrames - uniqueModels.size, + modelBytesBefore, + modelBytesAfter, + omittedUntexturedFaces, + omittedEmptyElements, + animatedContextFolders, + handRenderingEnabled: options.handRenderingEnabled === true, + animations: sequences.map((sequence) => ({ + key: sequence.key, + sourceName: sequence.sourceName, + sampledFrames: sequence.frames.length + })) } + }; + } + + // src/export-dialog.ts + var exportInProgress = false; + function includesResource(mode) { + return mode !== "datapack_only"; + } + function includesDatapack(mode) { + return mode !== "resource_only"; + } + function normalizeItemId(value) { + const trimmed = value.trim().toLowerCase(); + if (!trimmed) return "minecraft:potion"; + return trimmed.includes(":") ? trimmed : `minecraft:${trimmed}`; + } + function animationIdentity(spec) { + return `${spec.sourceName} (${spec.key})`; + } + function formatBytes(bytes) { + return bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`; + } + function escapeHtml2(value) { + return value.replace(/[&<>"']/g, (character) => ({ + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'" + })[character]); + } + function pickDirectory(resourceId, title) { + return Blockbench.pickDirectory({ resource_id: resourceId, title }) ?? null; + } + function chooseDestinations(outputMode, writeMode, packName, settings) { + if (writeMode === "insert") { + if (outputMode === "both_default") { + const root = settings.sharedRoot || pickDirectory("display_anim_insert_shared", tr("dap.export.pick_shared", { pack: packName })); + if (!root) return null; + return [ + { label: tr("dap.export.resource_pack"), scopeRoot: root, targetRoot: `${root}/resource-packs/${packName}`, kind: "resource", insert: true }, + { label: tr("dap.export.datapack"), scopeRoot: root, targetRoot: `${root}/datapacks/${packName}`, kind: "datapack", insert: true } + ]; + } + const destinations = []; + if (includesResource(outputMode)) { + const root = settings.resourcePackFolder || pickDirectory("display_anim_insert_resource", tr("dap.export.pick_existing_resource")); + if (!root) return null; + destinations.push({ + label: tr("dap.export.resource_pack"), + scopeRoot: root, + targetRoot: root, + kind: "resource", + insert: true + }); + } + if (includesDatapack(outputMode)) { + const root = settings.datapackFolder || pickDirectory("display_anim_insert_datapack", tr("dap.export.pick_existing_datapack")); + if (!root) return null; + destinations.push({ + label: tr("dap.export.datapack"), + scopeRoot: root, + targetRoot: root, + kind: "datapack", + insert: true + }); + } + return destinations; + } + if (outputMode === "both_default") { + const root = settings.sharedRoot || pickDirectory("display_anim_export", tr("dap.export.pick_shared", { pack: packName })); + if (!root) return null; + return [ + { label: tr("dap.export.resource_pack"), scopeRoot: root, targetRoot: `${root}/resource-packs/${packName}`, kind: "resource", insert: false }, + { label: tr("dap.export.datapack"), scopeRoot: root, targetRoot: `${root}/datapacks/${packName}`, kind: "datapack", insert: false } + ]; + } + if (outputMode === "both_separate") { + const resourceParent = settings.resourcePackFolder || pickDirectory("display_anim_export_resource_parent", tr("dap.export.pick_resource", { pack: packName })); + if (!resourceParent) return null; + const datapackParent = settings.datapackFolder || pickDirectory("display_anim_export_datapack_parent", tr("dap.export.pick_datapack", { pack: packName })); + if (!datapackParent) return null; + return [ + { label: tr("dap.export.resource_pack"), scopeRoot: resourceParent, targetRoot: `${resourceParent}/${packName}`, kind: "resource", insert: false }, + { label: tr("dap.export.datapack"), scopeRoot: datapackParent, targetRoot: `${datapackParent}/${packName}`, kind: "datapack", insert: false } + ]; + } + const kind = outputMode === "resource_only" ? "resource" : "datapack"; + const configured = kind === "resource" ? settings.resourcePackFolder : settings.datapackFolder; + const parent = configured || pickDirectory( + kind === "resource" ? "display_anim_export_resource_parent" : "display_anim_export_datapack_parent", + kind === "resource" ? tr("dap.export.pick_resource", { pack: packName }) : tr("dap.export.pick_datapack", { pack: packName }) + ); + return parent ? [{ + label: kind === "resource" ? tr("dap.export.resource_pack") : tr("dap.export.datapack"), + scopeRoot: parent, + targetRoot: `${parent}/${packName}`, + kind, + insert: false + }] : null; + } + function describeTextureSizeMismatch() { + if (!Project) return null; + const previewTextureUuid = Project.display_anim_export_settings?.handPreviewTextureUuid; + const mismatched = Texture.all.filter( + (texture) => (!previewTextureUuid || texture.uuid !== previewTextureUuid) && texture[PREVIEW_TEXTURE_PROPERTY] !== true && texture.name !== "DAP_Default_Player_Skin.png" && texture.name !== "missing.png" && (texture.width !== Project.texture_width || texture.height !== Project.texture_height) + ); + if (!mismatched.length) return null; + return tr("dap.export.texture_mismatch", { + project_width: Project.texture_width, + project_height: Project.texture_height, + textures: mismatched.map((texture) => ` ${texture.name}: ${texture.width}\xD7${texture.height}`).join("\n") + }); + } + function confirmWarnings(warnings) { + if (!warnings.length) return Promise.resolve("continue"); + const boundsAnimationUuid = warnings.find((warning) => warning.boundsAnimationUuid)?.boundsAnimationUuid; + const message = warnings.map((warning, index) => { + const details = escapeHtml2(warning.message).replace(/\n/g, "
"); + return `
\u26A0 ${index + 1}. ${escapeHtml2(warning.title)}
${details}
`; + }).join(`
`); + return new Promise((resolve) => { + const buttons = boundsAnimationUuid ? [tr("dap.export.cancel_export"), tr("dap.export.open_bounds_check"), tr("dap.export.export_anyway")] : [tr("dap.export.cancel_export"), tr("dap.export.export_anyway")]; + Blockbench.showMessageBox( + { + title: tr("dap.export.warnings_title"), + message, + icon: "warning", + buttons, + confirmIndex: buttons.length - 1, + cancelIndex: 0 + }, + (button) => { + if (boundsAnimationUuid && button === 1) { + resolve("bounds"); + } else { + resolve(button === buttons.length - 1 ? "continue" : "cancel"); + } + } + ); + }); + } + function confirmPreflight(targets) { + const preview = previewPacks(targets); + if (preview.conflicts.length) { + Blockbench.showMessageBox({ + title: tr("dap.export.conflict_title"), + message: tr("dap.export.conflict_message", { paths: preview.conflicts.join("\n") }), + icon: "error" + }); + return Promise.resolve(false); + } + const targetsSummary = preview.targets.map((target) => `${escapeHtml2(target.root)}
\u25CF ${escapeHtml2(tr("dap.export.preflight_added"))}: ${target.added}
\u25CF ${escapeHtml2(tr("dap.export.preflight_updated"))}: ${target.updated}
${target.removed ? "\u26A0" : "\u25CF"} ${escapeHtml2(tr("dap.export.preflight_removed"))}: ${target.removed}
\u25CF ${escapeHtml2(tr("dap.export.preflight_merged"))}: ${target.merged}`).join("

"); + return new Promise((resolve) => { + Blockbench.showMessageBox( + { + title: tr("dap.export.preflight_title"), + message: tr("dap.export.preflight_message", { summary: targetsSummary }), + icon: "rule", + buttons: [tr("dap.export.cancel"), tr("dap.export.confirm_write")], + confirmIndex: 1, + cancelIndex: 0 + }, + (button) => resolve(button === 1) + ); + }); + } + async function runExport(form, specs, settings) { + const defaultSpec = specs.find((spec) => spec.sourceUuid === form.default_animation); + if (!defaultSpec) throw new Error(tr("dap.export.default_missing")); + const packName = form.pack_name.trim(); + const projectName = form.project_name.trim(); + const displayName = form.display_name.trim() || projectName; + const configuredBaseItem = normalizeItemId(form.base_item); + const baseItem = settings.handRenderingEnabled ? "minecraft:player_head" : configuredBaseItem; + const displayContexts = configuredDisplayAnimations().map( + ({ context, animated }) => ({ context: context.id, animated }) + ); + const hasAnimatedContext = displayContexts.some((route) => route.animated); + const sequences = []; + const warnings = []; + if (includesResource(form.output_mode)) { + if (settings.handRenderingEnabled) { + rememberExportSettingsDraft(settings); + } + if (!hasAnimatedContext && specs.length > 1) { + warnings.push({ + title: tr("dap.export.no_animated_context_title"), + message: tr("dap.export.no_animated_context_message", { + default_animation: animationIdentity(defaultSpec) + }) + }); + } + const bakedSpecs = hasAnimatedContext ? specs : [defaultSpec]; + const frameCounts = new Map(bakedSpecs.map((spec) => [spec.sourceUuid, hasAnimatedContext ? spec.frameCount : 1])); + const animationKeys = new Map(bakedSpecs.map((spec) => [spec.sourceUuid, spec.key])); + const isolated = await runExactBoundsForExport( + bakedSpecs.map((spec) => spec.animation), + animationKeys, + frameCounts, + settings.animationFps, + settings.exactBoundsOnExport && hasAnimatedContext, + settings.exactBoundsOnExport, + settings.handRenderingEnabled + ); + if (!isolated) { + Blockbench.showQuickMessage(tr("dap.export.cancelled"), 2500); + return; + } + for (const result of isolated.sequences) { + sequences.push({ key: result.key, sourceName: result.sourceName, frames: result.frames }); + } + for (const spec of bakedSpecs) { + if (spec.sourceFps !== settings.animationFps) { + warnings.push({ + title: `${tr("dap.export.resampled_title")} \u2014 ${animationIdentity(spec)}`, + message: tr("dap.export.resampled_message", { + source_fps: spec.sourceFps, + game_fps: settings.animationFps, + frames: spec.frameCount + }) + }); + } + if (settings.exactBoundsOnExport) { + const record = isolated.records.find((item) => item.animationUuid === spec.sourceUuid); + const bounds = describeOutOfBounds(record?.hits ?? []); + if (bounds) warnings.push({ + title: `${tr("dap.export.bounds_title")} \u2014 ${animationIdentity(spec)}`, + message: bounds, + boundsAnimationUuid: spec.sourceUuid + }); + } + } + const textureWarning = describeTextureSizeMismatch(); + if (textureWarning) warnings.push({ title: tr("dap.export.texture_mismatch_title"), message: textureWarning }); + } else { + warnings.push({ title: tr("dap.export.datapack_only_title"), message: tr("dap.export.datapack_only_message") }); + } + if (settings.handRenderingEnabled && includesResource(form.output_mode)) { + warnings.push({ + title: tr("dap.export.hand_rendering_warning_title"), + message: tr("dap.export.hand_rendering_warning_message") + }); + if (!includesDatapack(form.output_mode)) { + warnings.push({ + title: tr("dap.export.hand_rendering_resource_only_title"), + message: tr("dap.export.hand_rendering_resource_only_message") + }); + } + } + const warningDecision = await confirmWarnings(warnings); + if (warningDecision === "bounds") { + runBoundsCheck(specs.map((spec) => spec.animation)); + return; + } + if (warningDecision === "cancel") { + Blockbench.showQuickMessage(tr("dap.export.cancelled"), 2500); + return; + } + const destinations = chooseDestinations(form.output_mode, form.write_mode, packName, settings); + if (!destinations) { + Blockbench.showQuickMessage(tr("dap.export.cancelled"), 2500); + return; + } + const totalFrames = specs.reduce((sum, spec) => sum + spec.frameCount, 0); + const resourceBuild = includesResource(form.output_mode) ? buildResourcePack(sequences, { + packName, + projectName, + defaultAnimationKey: defaultSpec.key, + displayContexts, + handRenderingEnabled: settings.handRenderingEnabled, + description: tr("dap.export.resource_description_multi", { + name: displayName, + animations: specs.length, + frames: sequences.reduce((sum, sequence) => sum + sequence.frames.length, 0), + fps: settings.animationFps + }) + }) : null; + const datapackOptions = { + packName, + projectName, + baseItem, + itemDisplayName: displayName, + frameObjective: form.frame_objective, + modeObjective: form.mode_objective, + maxFrameObjective: form.max_frame_objective, + playingTag: form.playing_tag, + playbackFps: settings.animationFps, + debugEnabled: settings.debugEnabled === true, + handRenderingEnabled: settings.handRenderingEnabled, + animations: specs.map((spec) => ({ key: spec.key, displayName: spec.sourceName, frameCount: spec.frameCount })), + defaultAnimationKey: defaultSpec.key, + description: tr("dap.export.datapack_description_multi", { + name: displayName, + animations: specs.length, + frames: totalFrames + }) + }; + const targets = destinations.map((destination) => ({ + scopeRoot: destination.scopeRoot, + root: destination.targetRoot, + kind: destination.kind, + projectName, + insert: destination.insert, + files: [ + ...destination.kind === "resource" ? resourceBuild.files : buildDatapack(datapackOptions) + ] + })); + if (!await confirmPreflight(targets)) { + Blockbench.showQuickMessage(tr("dap.export.cancelled"), 2500); + return; + } + const count = writePacks(targets); + rememberExportSettings({ + selectedAnimationUuids: specs.map((spec) => spec.sourceUuid), + defaultAnimationUuid: defaultSpec.sourceUuid, + packName, + projectName, + outputMode: form.output_mode, + writeMode: form.write_mode, + baseItem: configuredBaseItem, + displayName, + frameObjective: form.frame_objective, + modeObjective: form.mode_objective, + maxFrameObjective: form.max_frame_objective, + playingTag: form.playing_tag, + debugEnabled: settings.debugEnabled === true, + handRenderingEnabled: settings.handRenderingEnabled, + handRigRootUuid: settings.handRigRootUuid, + handLeftGroupUuid: settings.handLeftGroupUuid, + handRightGroupUuid: settings.handRightGroupUuid, + handPreviewTextureUuid: settings.handPreviewTextureUuid, + exactBoundsOnExport: settings.exactBoundsOnExport, + animationFps: settings.animationFps, + sharedRoot: settings.sharedRoot, + resourcePackFolder: settings.resourcePackFolder, + datapackFolder: settings.datapackFolder + }); + const report2 = resourceBuild?.report ?? null; + const locations = destinations.map( + (destination) => `
${escapeHtml2(destination.label)}\uFF1A
${escapeHtml2(`${destination.targetRoot}/`)}
` + ).join(""); + const optimization = report2 ? describeOptimization(report2) : ""; + const itemModel = includesResource(form.output_mode) || includesDatapack(form.output_mode) ? tr("dap.export.item_model_id", { id: `${EXPORT_NAMESPACE}:${projectName}` }) : ""; + const tipsStatus = tr(settings.debugEnabled === true ? "dap.export.developer_tips_enabled" : "dap.export.developer_tips_disabled"); + const handStatus = tr(settings.handRenderingEnabled ? "dap.export.developer_tips_enabled" : "dap.export.developer_tips_disabled"); + Blockbench.showMessageBox({ + title: tr("dap.export.complete"), + message: `
\u2713 ${escapeHtml2(tr("dap.export.complete_heading"))}
${escapeHtml2(tr("dap.export.write_success", { count }))}
${escapeHtml2(tr("dap.export.output_locations"))}
${locations}
` + (optimization ? `
${escapeHtml2(tr("dap.export.summary"))}
${optimization}
` : "") + `
${escapeHtml2(tr("dap.export.developer_info"))}
` + (itemModel ? `
${escapeHtml2(itemModel)}
` : "") + `
${escapeHtml2(tr("dap.export.developer_tips_status", { status: tipsStatus }))}
${escapeHtml2(tr("dap.export.hand_rendering_status", { status: handStatus }))}
`, + icon: "check_circle" + }); + } + function describeOptimization(report2) { + const statistics = tr("dap.export.optimization_statistics", { + sampled: report2.sampledFrames, + unique: report2.uniqueModels, + duplicates: report2.duplicateFrames + }); + const modelJson = tr("dap.export.model_json_size", { + before: formatBytes(report2.modelBytesBefore), + after: formatBytes(report2.modelBytesAfter) + }); + const animations = report2.animations.map((animation) => escapeHtml2(tr("dap.export.animation_report", { + animation: `${animation.sourceName} (${animation.key})`, + frames: animation.sampledFrames + }))).join("
"); + return `
${escapeHtml2(tr("dap.export.space_optimization"))}
${escapeHtml2(statistics)}
${escapeHtml2(modelJson)}
` + (animations ? `
${animations}
` : ""); + } + function openPackConfigurationDialog(animations, initial, defaultAnimationUuid) { + const specs = createExportAnimationSpecs(animations, initial.animationFps); + const form = { + pack_name: initial.packName, + project_name: initial.projectName, + base_item: initial.baseItem, + display_name: initial.displayName, + frame_objective: initial.frameObjective, + mode_objective: initial.modeObjective, + max_frame_objective: initial.maxFrameObjective, + playing_tag: initial.playingTag, + output_mode: initial.outputMode, + write_mode: initial.writeMode, + default_animation: defaultAnimationUuid + }; + if (!isSafeProjectName(form.pack_name.trim()) || !isSafeProjectName(form.project_name.trim())) { + Blockbench.showMessageBox({ title: tr("dap.export.invalid_identifier_title"), message: `${tr("dap.export.invalid_identifier_message")} + +${tr("dap.export.open_settings_hint")}`, icon: "error" }); + return; + } + const objectives = [ + form.frame_objective, + form.mode_objective, + form.max_frame_objective, + phaseObjectiveFor(form.frame_objective) + ]; + if (objectives.some((value) => !isValidObjectiveName(value)) || new Set(objectives).size !== objectives.length || !isValidPlayingTag(form.playing_tag)) { + Blockbench.showMessageBox({ title: tr("dap.export.failed"), message: `${tr("dap.export.objective_conflict")} + +${tr("dap.export.open_settings_hint")}`, icon: "error" }); + return; } - return written; + exportInProgress = true; + void runExport(form, specs, initial).catch((error) => { + console.error("JSB export failed", error); + Blockbench.showMessageBox({ title: tr("dap.export.failed"), message: error.message ?? String(error), icon: "error" }); + }).finally(() => { + exportInProgress = false; + }); } - function inspectExisting(scopeRoot, dir) { - const fs = getScopedFs(scopeRoot, true); - if (!fs) { - throw new Error(tr("dap.error.read_permission")); + function openExportDialog() { + if (exportInProgress) { + Blockbench.showQuickMessage(tr("dap.export.busy"), 2e3); + return; } - if (!fs.existsSync(dir)) return null; - const pathModule = getPathModule(); - let count = 0; - const walk = (current) => { - for (const entry of fs.readdirSync(current, { withFileTypes: true })) { - const child = pathModule.join(current, entry.name); - if (entry.isDirectory()) { - walk(child); - } else { - count++; - } - } - }; - walk(dir); - return count; + const animations = Animation.all.slice(); + if (!animations.length) { + Blockbench.showMessageBox({ title: tr("dap.export.no_animation_title"), message: tr("dap.export.no_animation_message"), icon: "error" }); + return; + } + const initial = initialExportSettings(animations); + const selectedSet = new Set(initial.selectedAnimationUuids); + const selected = animations.filter((animation) => selectedSet.has(animation.uuid)); + if (!selected.length) { + Blockbench.showMessageBox({ title: tr("dap.export.select_title"), message: `${tr("dap.export.select_required")} + +${tr("dap.export.open_settings_hint")}`, icon: "error" }); + return; + } + const conflicts = findAnimationKeyConflicts(selected); + if (conflicts.length) { + const details = conflicts.map((conflict) => tr("dap.export.key_conflict_entry", { + key: conflict.key || tr("dap.export.invalid_key"), + animations: conflict.animationNames.join(", ") + })).join("\n"); + Blockbench.showMessageBox({ title: tr("dap.export.key_conflict_title"), message: `${tr("dap.export.key_conflict_message", { details })} + +${tr("dap.export.open_settings_hint")}`, icon: "error" }); + return; + } + const defaultUuid = selectedSet.has(initial.defaultAnimationUuid) ? initial.defaultAnimationUuid : selected[0].uuid; + openPackConfigurationDialog(selected, initial, defaultUuid); } // src/vanilla-items.ts @@ -3075,485 +6849,665 @@ "zombified_piglin_spawn_egg" ]; - // src/export-dialog.ts - var DIALOG_ID = "display_anim_preview_export"; - var OUTPUT_MODE_KEYS = { - both_default: "dap.export.mode.both_default", - both_separate: "dap.export.mode.both_separate", - resource_only: "dap.export.mode.resource_only", - datapack_only: "dap.export.mode.datapack_only" - }; - function outputModes() { - return Object.fromEntries( - Object.entries(OUTPUT_MODE_KEYS).map(([id, key]) => [id, tr(key)]) - ); + // src/project-settings-dialog.ts + var overlay = null; + function fieldLabel(text) { + const label = el("label", text); + label.style.display = "block"; + label.style.fontSize = "inherit"; + label.style.marginBottom = "5px"; + label.style.color = "var(--color-text)"; + return label; } - var exportInProgress = false; - var GAME_FPS = 20; - var VANILLA_ITEM_OPTIONS = Object.fromEntries( - VANILLA_ITEM_IDS.map((id) => [`minecraft:${id}`, `minecraft:${id}`]) - ); - function sanitizeId(value, fallback) { - const safe = value.trim().toLowerCase().replace(/[^a-z0-9_.-]+/g, "_").replace(/^_+|_+$/g, ""); - return safe || fallback; - } - function sanitizeObjective(value, fallback) { - return sanitizeId(value, fallback).slice(0, 16); + function fieldWrap() { + const wrap = el("div"); + wrap.style.marginBottom = "15px"; + return wrap; } - function normalizeItemId(value) { - const trimmed = value.trim().toLowerCase(); - if (!trimmed) return "minecraft:potion"; - return trimmed.includes(":") ? trimmed : `minecraft:${trimmed}`; + function styleControl(control) { + control.style.height = "38px"; + control.style.padding = "0 10px"; + control.style.boxSizing = "border-box"; + control.style.color = "var(--color-text)"; + control.style.background = "var(--color-back)"; + control.style.border = "1px solid var(--color-border)"; + control.style.borderRadius = "0"; + control.style.outline = "none"; + applyFocusHighlight(control); } - function includesResource(mode) { - return mode !== "datapack_only"; + function validationLine() { + const line = el("div"); + line.style.fontSize = "inherit"; + line.style.marginTop = "5px"; + line.style.minHeight = "16px"; + return line; } - function includesDatapack(mode) { - return mode !== "resource_only"; + function showValidation(line, result) { + const colors = { valid: "#59c36a", warning: "#e6ad4f", error: "#e25d68", empty: "var(--color-subtle_text)" }; + const icons = { valid: "check_circle", warning: "warning", error: "error", empty: "info" }; + line.style.color = colors[result.state]; + line.innerHTML = `${icons[result.state]}${result.message}`; } - function subtleInfo(text) { - return `${text}`; + function identifierValidation(value) { + return isSafeProjectName(value.trim()) ? { state: "valid", message: tr("dap.settings.valid_identifier") } : { state: "error", message: tr("dap.settings.invalid_identifier") }; } - function describeTextureSizeMismatch() { - if (!Project) return null; - const mismatched = Texture.all.filter( - (texture) => texture.width !== Project.texture_width || texture.height !== Project.texture_height - ); - if (!mismatched.length) return null; - const list = mismatched.map((texture) => ` ${texture.name}: ${texture.width}\xD7${texture.height}`).join("\n"); - return tr("dap.export.texture_mismatch", { - project_width: Project.texture_width, - project_height: Project.texture_height, - textures: list - }); + function objectiveNameValidation(value) { + return isValidObjectiveName(value) ? { state: "valid", message: tr("dap.settings.valid_runtime_name") } : { state: "error", message: tr("dap.settings.invalid_runtime_name") }; } - function confirmWarnings(warnings) { - if (!warnings.length) return Promise.resolve(true); - const message = warnings.map( - (warning, index) => `**${index + 1}. ${warning.title}** - -${warning.message}` - ).join("\n\n---\n\n"); - return new Promise((resolve) => { - Blockbench.showMessageBox( - { - title: tr("dap.export.warnings_title"), - message, - icon: "warning", - buttons: [tr("dap.export.cancel_export"), tr("dap.export.export_anyway")], - confirm: 1, - cancel: 0 - }, - (button) => resolve(button === 1) - ); - }); + function playingTagValidation(value) { + return isValidPlayingTag(value) ? { state: "valid", message: tr("dap.settings.valid_playing_tag") } : { state: "error", message: tr("dap.settings.invalid_playing_tag") }; } - function pickParent(resourceId, title) { - return Blockbench.pickDirectory({ - resource_id: resourceId, - title - }) ?? null; - } - function chooseDestinations(mode, packName) { - if (mode === "both_default") { - const root = pickParent( - "display_anim_export", - tr("dap.export.pick_shared", { pack: packName }) - ); - if (!root) return null; - return [ - { - label: tr("dap.export.resource_pack"), - scopeRoot: root, - targetRoot: `${root}/resource-packs/${packName}`, - kind: "resource" - }, - { - label: tr("dap.export.datapack"), - scopeRoot: root, - targetRoot: `${root}/datapacks/${packName}`, - kind: "datapack" - } - ]; + function folderValidation(value, kind, insert, packName = "") { + if (!value.trim()) return { state: "empty", message: tr("dap.settings.folder_optional") }; + let fs; + try { + fs = requireNativeModule("fs", { scope: value, message: tr("dap.permission.export"), show_permission_dialog: false }); + if (!fs || !fs.existsSync(value)) return { state: "error", message: tr("dap.settings.folder_missing") }; + fs.readdirSync(value, { withFileTypes: true }); + } catch (_error) { + return { state: "error", message: tr("dap.settings.folder_unreadable") }; } - if (mode === "both_separate") { - const resourceParent = pickParent( - "display_anim_export_resource_parent", - tr("dap.export.pick_resource", { pack: packName }) - ); - if (!resourceParent) return null; - const datapackParent = pickParent( - "display_anim_export_datapack_parent", - tr("dap.export.pick_datapack", { pack: packName }) - ); - if (!datapackParent) return null; - return [ - { - label: tr("dap.export.resource_pack"), - scopeRoot: resourceParent, - targetRoot: `${resourceParent}/${packName}`, - kind: "resource" - }, - { - label: tr("dap.export.datapack"), - scopeRoot: datapackParent, - targetRoot: `${datapackParent}/${packName}`, - kind: "datapack" - } - ]; + if (!insert) return { state: "valid", message: tr("dap.settings.folder_valid_parent") }; + if (kind === "shared") { + const path2 = requireNativeModule("path"); + const resourceMeta = path2.join(value, "resource-packs", packName, "pack.mcmeta"); + const dataMeta = path2.join(value, "datapacks", packName, "pack.mcmeta"); + return fs.existsSync(resourceMeta) && fs.existsSync(dataMeta) ? { state: "valid", message: tr("dap.settings.folder_valid_shared") } : { state: "error", message: tr("dap.settings.folder_invalid_shared") }; } - if (mode === "resource_only") { - const parent2 = pickParent( - "display_anim_export_resource_parent", - tr("dap.export.pick_resource", { pack: packName }) - ); - return parent2 ? [{ label: tr("dap.export.resource_pack"), scopeRoot: parent2, targetRoot: `${parent2}/${packName}`, kind: "resource" }] : null; + const path = requireNativeModule("path"); + const packMeta = path.join(value, "pack.mcmeta"); + if (!fs.existsSync(packMeta)) return { state: "error", message: tr("dap.settings.folder_no_pack_meta") }; + try { + const parsed = JSON.parse(fs.readFileSync(packMeta, "utf8")); + if (!parsed || typeof parsed.pack !== "object") return { state: "error", message: tr("dap.settings.folder_invalid_pack_meta") }; + } catch (_error) { + return { state: "error", message: tr("dap.settings.folder_invalid_pack_meta") }; } - const parent = pickParent( - "display_anim_export_datapack_parent", - tr("dap.export.pick_datapack", { pack: packName }) - ); - return parent ? [{ label: tr("dap.export.datapack"), scopeRoot: parent, targetRoot: `${parent}/${packName}`, kind: "datapack" }] : null; - } - function writeDestinations(destinations, targets) { - const grouped = /* @__PURE__ */ new Map(); - destinations.forEach((destination, index) => { - const list = grouped.get(destination.scopeRoot) ?? []; - list.push(targets[index]); - grouped.set(destination.scopeRoot, list); - }); - let count = 0; - for (const [scopeRoot, scopedTargets] of grouped) { - count += writePacks(scopeRoot, scopedTargets); + const expected = path.join(value, kind === "resource" ? "assets" : "data"); + return fs.existsSync(expected) ? { state: "valid", message: tr("dap.settings.folder_valid_pack") } : { state: "warning", message: tr(kind === "resource" ? "dap.settings.folder_no_assets" : "dap.settings.folder_no_data") }; + } + function textField(parent, label, value, change, validate) { + const wrap = fieldWrap(); + wrap.appendChild(fieldLabel(label)); + const input = document.createElement("input"); + input.type = "text"; + input.value = value; + input.style.width = "100%"; + input.style.boxSizing = "border-box"; + styleControl(input); + const status = validationLine(); + input.oninput = (event) => { + change(event.target.value); + if (validate) showValidation(status, validate(event.target.value)); + }; + wrap.appendChild(input); + if (validate) { + showValidation(status, validate(value)); + wrap.appendChild(status); } - return count; + parent.appendChild(wrap); } - function formatBytes(bytes) { - if (bytes < 1024) return `${bytes} B`; - return `${(bytes / 1024).toFixed(1)} KB`; - } - async function runExport(form, animation) { - const mode = form.output_mode || "both_default"; - const sourceFps = animation.snapping || GAME_FPS; - const fps = GAME_FPS; - const frameCount = frameCountFor(animation.length, fps); - const packName = sanitizeId(form.pack_name, "display_animation"); - const assetNamespace = sanitizeId(form.asset_namespace, "kaleidoscope_lab"); - const itemModel = sanitizeId(form.item_model, "item"); - const dataNamespace = sanitizeId(form.data_namespace, packName); - const displayName = form.display_name.trim() || animation.name; - const destinations = chooseDestinations(mode, packName); - if (!destinations) { - Blockbench.showQuickMessage(tr("dap.export.cancelled"), 2500); - return; + function selectField(parent, label, value, options, change, disabled = false) { + const wrap = fieldWrap(); + wrap.appendChild(fieldLabel(label)); + const select = document.createElement("select"); + select.style.width = "100%"; + styleControl(select); + for (const [id, title] of options) { + const option = document.createElement("option"); + option.value = id; + option.innerText = title; + select.appendChild(option); } - let frames = []; - let displayContexts; - let exportedFrameCount = frameCount; - let outOfBounds = []; - const warnings = []; - if (includesResource(mode)) { - displayContexts = configuredDisplayAnimations().map( - ({ context, animated }) => ({ - context: context.id, - animated - }) - ); - exportedFrameCount = displayContexts.some((route) => route.animated) ? frameCount : 1; - Blockbench.showQuickMessage( - tr("dap.export.baking", { frames: exportedFrameCount, fps }), - 2e3 - ); - const result = bakeFrames(exportedFrameCount, fps); - frames = result.frames; - outOfBounds = result.outOfBounds; - if (!frames.length) { - Blockbench.showMessageBox({ - title: tr("dap.export.failed"), - message: tr("dap.export.no_frames"), - icon: "error" - }); + select.value = value; + select.disabled = disabled; + if (disabled) select.style.opacity = "0.65"; + select.onchange = (event) => change(event.target.value); + wrap.appendChild(select); + parent.appendChild(wrap); + } + function checkboxField(parent, label, help, checked, change) { + const wrap = fieldWrap(); + const input = document.createElement("input"); + input.type = "checkbox"; + input.checked = checked; + input.style.marginRight = "8px"; + input.onchange = () => change(input.checked); + const text = el("span", label); + const row = el("label"); + row.style.display = "flex"; + row.style.alignItems = "center"; + row.style.cursor = "pointer"; + row.appendChild(input); + row.appendChild(text); + const hint = el("button", "?"); + hint.style.display = "inline-flex"; + hint.style.alignItems = "center"; + hint.style.justifyContent = "center"; + hint.style.width = "17px"; + hint.style.height = "17px"; + hint.style.marginLeft = "7px"; + hint.style.border = "1px solid var(--color-subtle_text)"; + hint.style.borderRadius = "50%"; + hint.style.fontSize = "inherit"; + hint.style.color = "var(--color-subtle_text)"; + hint.style.padding = "0"; + hint.style.cursor = "pointer"; + const helpBox = el("div", help); + helpBox.style.display = "none"; + helpBox.style.margin = "8px 0 0 26px"; + helpBox.style.padding = "8px 10px"; + helpBox.style.background = "var(--color-back)"; + helpBox.style.borderLeft = "3px solid var(--color-accent)"; + helpBox.style.color = "var(--color-subtle_text)"; + helpBox.style.fontSize = "inherit"; + hint.onclick = () => { + helpBox.style.display = helpBox.style.display === "none" ? "block" : "none"; + }; + row.appendChild(hint); + wrap.appendChild(row); + wrap.appendChild(helpBox); + parent.appendChild(wrap); + } + function folderField(parent, label, value, resourceId, validate, change) { + const wrap = fieldWrap(); + wrap.appendChild(fieldLabel(label)); + const row = el("div"); + row.style.display = "flex"; + row.style.gap = "8px"; + const input = document.createElement("input"); + input.type = "text"; + input.value = value; + input.placeholder = tr("dap.settings.folder_empty"); + input.style.flex = "1"; + styleControl(input); + const status = validationLine(); + input.oninput = (event) => { + change(event.target.value); + showValidation(status, validate(event.target.value)); + }; + const browse = el("button", tr("dap.settings.browse")); + browse.style.height = "38px"; + browse.style.padding = "0 18px"; + browse.style.borderRadius = "0"; + browse.onclick = () => { + const picked = Blockbench.pickDirectory({ resource_id: resourceId, title: label }); + if (!picked) return; + input.value = picked; + change(picked); + showValidation(status, validate(picked)); + }; + row.appendChild(input); + row.appendChild(browse); + wrap.appendChild(row); + showValidation(status, validate(value)); + wrap.appendChild(status); + parent.appendChild(wrap); + } + function pageTitle(parent, title, description) { + const heading = el("h2", title); + heading.style.margin = "0 0 6px"; + parent.appendChild(heading); + const note = el("p", description); + note.style.color = "var(--color-subtle_text)"; + note.style.margin = "0 0 20px"; + parent.appendChild(note); + } + function copyableCode(parent, title, description, value) { + const wrap = fieldWrap(); + const heading = el("div", title); + heading.style.fontWeight = "600"; + heading.style.marginBottom = "7px"; + wrap.appendChild(heading); + const note = el("div", description); + note.style.marginBottom = "7px"; + note.style.color = "var(--color-subtle_text)"; + note.style.fontSize = "inherit"; + wrap.appendChild(note); + const row = el("div"); + row.style.display = "flex"; + row.style.alignItems = "stretch"; + const code = document.createElement("textarea"); + code.value = value; + code.readOnly = true; + code.style.flex = "1"; + code.style.margin = "0"; + code.style.padding = "10px 12px"; + code.style.background = "var(--color-back)"; + code.style.border = "1px solid var(--color-border)"; + code.style.resize = "vertical"; + code.style.minHeight = `${Math.max(44, value.split("\n").length * 22 + 18)}px`; + code.style.color = "var(--color-text)"; + code.style.fontFamily = "var(--font-code), monospace"; + code.style.boxSizing = "border-box"; + const copy = el("button"); + copy.innerHTML = `content_copy`; + copy.title = tr("dap.settings.copy"); + copy.style.width = "44px"; + copy.style.borderRadius = "0"; + copy.onclick = () => { + const clipboard = requireNativeModule("clipboard", { + message: tr("dap.settings.clipboard_permission") + }); + if (!clipboard) { + Blockbench.showQuickMessage(tr("dap.settings.copy_failed"), 2e3); return; } - if (sourceFps !== GAME_FPS) { - warnings.push({ - title: tr("dap.export.resampled_title"), - message: tr("dap.export.resampled_message", { - source_fps: sourceFps, - game_fps: GAME_FPS, - frames: frameCount - }) - }); - } - const textureSizeWarning = describeTextureSizeMismatch(); - if (textureSizeWarning) { - warnings.push({ title: tr("dap.export.texture_mismatch_title"), message: textureSizeWarning }); - } - const boundsWarning = describeOutOfBounds(outOfBounds); - if (boundsWarning) { - warnings.push({ title: tr("dap.export.bounds_title"), message: boundsWarning }); - } - } - const warningsAccepted = await confirmWarnings(warnings); - if (!warningsAccepted) { - Blockbench.showQuickMessage(tr("dap.export.cancelled"), 2500); + clipboard.writeText(value); + Blockbench.showQuickMessage(tr("dap.settings.copied"), 1500); + }; + row.appendChild(code); + row.appendChild(copy); + wrap.appendChild(row); + parent.appendChild(wrap); + } + function disposeProjectSettingsDialog() { + overlay?.remove(); + overlay = null; + } + function openProjectSettingsDialog() { + disposeProjectSettingsDialog(); + if (!Project) { + Blockbench.showQuickMessage(tr("dap.settings.no_project"), 2e3); return; } - Blockbench.showQuickMessage(tr("dap.export.preparing_files"), 2e3); - const resourceOptions = { - packName, - namespace: assetNamespace, - itemModel, - description: tr("dap.export.resource_description", { - name: displayName, - frames: exportedFrameCount, - fps - }), - displayContexts - }; - const datapackOptions = { - packName, - dataNamespace, - assetNamespace, - itemModel, - baseItem: normalizeItemId(form.base_item), - itemDisplayName: displayName, - frameObjective: sanitizeObjective(form.frame_objective, "dap_frame"), - modeObjective: sanitizeObjective(form.mode_objective, "dap_mode"), - playingTag: sanitizeId(form.playing_tag, "dap_playing"), - frameCount: exportedFrameCount, - description: tr("dap.export.datapack_description", { - name: displayName, - frames: exportedFrameCount - }) - }; - try { - let packReport = null; - const resourceBuild = includesResource(mode) ? buildResourcePack(frames, resourceOptions) : null; - if (resourceBuild) packReport = resourceBuild.report; - const targets = destinations.map((destination) => ({ - root: destination.targetRoot, - files: destination.kind === "resource" ? resourceBuild.files : buildDatapack(datapackOptions) - })); - const existing = destinations.map((destination) => ({ - destination, - count: inspectExisting(destination.scopeRoot, destination.targetRoot) - })).filter((entry) => entry.count !== null); - if (existing.length) { - const summary = existing.map( - ({ destination, count }) => `${destination.targetRoot}/ (${tr("dap.export.file_count", { count: count ?? 0 })})` - ).join("\n"); - const overwriteAccepted = await new Promise((resolve) => { - Blockbench.showMessageBox( - { - title: tr("dap.export.target_exists"), - message: tr("dap.export.target_exists_message", { summary }), - icon: "warning", - buttons: [tr("dap.export.cancel"), tr("dap.export.overwrite")], - confirm: 1, - cancel: 0 - }, - (button) => resolve(button === 1) - ); - }); - if (!overwriteAccepted) { - Blockbench.showQuickMessage(tr("dap.export.cancelled"), 2500); - return; + const animations = Animation.all.slice(); + const settings = initialExportSettings(animations); + const persist = () => rememberExportSettingsDraft(settings); + overlay = el("div"); + overlay.style.position = "fixed"; + overlay.style.inset = "0"; + overlay.style.zIndex = "10000"; + overlay.style.background = "rgba(0,0,0,.55)"; + overlay.style.display = "flex"; + overlay.style.alignItems = "center"; + overlay.style.justifyContent = "center"; + const shell = el("div"); + shell.id = "dap-project-settings-shell"; + shell.style.width = "900px"; + shell.style.maxWidth = "92vw"; + shell.style.height = "650px"; + shell.style.maxHeight = "88vh"; + shell.style.background = "var(--color-ui)"; + shell.style.border = "1px solid var(--color-border)"; + shell.style.boxShadow = "0 12px 50px rgba(0,0,0,.55)"; + shell.style.display = "flex"; + shell.style.flexDirection = "column"; + shell.style.borderRadius = "0"; + const scopedStyle = el("style"); + scopedStyle.innerHTML = ` + #dap-project-settings-shell .dap-sidebar-button:hover, + #dap-project-settings-shell .dap-sidebar-button:focus { + color: var(--color-text) !important; + background: var(--color-selected) !important; + } + #dap-project-settings-shell .dap-sidebar-button:hover *, + #dap-project-settings-shell .dap-sidebar-button:focus * { + color: inherit !important; + } + #dap-project-settings-shell .dap-hand-card { + border: 1px solid var(--color-border); + background: var(--color-back); + padding: 16px; + } + #dap-project-settings-shell .dap-hand-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: 14px; + margin-top: 14px; + } + #dap-project-settings-shell .dap-hand-grid > div { + margin-bottom: 0 !important; + } + #dap-project-settings-shell .dap-hand-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding-top: 14px; + margin-top: 14px; + border-top: 1px solid var(--color-border); + } + #dap-project-settings-shell .dap-hand-actions button { + height: 34px; + padding: 0 12px; + margin: 0; + } + `; + shell.appendChild(scopedStyle); + const header = el("div", tr("dap.settings.title")); + header.style.fontSize = "18px"; + header.style.fontWeight = "600"; + header.style.padding = "15px 18px"; + header.style.borderBottom = "1px solid var(--color-border)"; + shell.appendChild(header); + const body = el("div"); + body.style.display = "flex"; + body.style.flex = "1"; + body.style.minHeight = "0"; + const sidebar = el("div"); + sidebar.style.width = "190px"; + sidebar.style.padding = "0"; + sidebar.style.background = "var(--color-back)"; + sidebar.style.borderRight = "1px solid var(--color-border)"; + const content2 = el("div"); + content2.style.flex = "1"; + content2.style.padding = "24px 28px"; + content2.style.overflowY = "auto"; + body.appendChild(sidebar); + body.appendChild(content2); + shell.appendChild(body); + const renderHands = () => { + pageTitle(content2, tr("dap.settings.page.hands"), tr("dap.settings.hands_desc")); + checkboxField( + content2, + tr("dap.settings.hand_rendering"), + tr("dap.settings.hand_rendering_help"), + settings.handRenderingEnabled, + (value) => { + settings.handRenderingEnabled = value; + if (value) ensureHandRig(settings); + else setHandRigVisibility(settings); + persist(); + content2.innerHTML = ""; + renderHands(); } - } - try { - const count = writeDestinations(destinations, targets); - const locations = destinations.map((destination) => `${destination.label}\uFF1A${destination.targetRoot}/`).join("\n"); - const optimization = packReport ? ` - -${tr("dap.export.optimization", { - sampled: packReport.sampledFrames, - unique: packReport.uniqueModels, - duplicates: packReport.duplicateFrames, - before: formatBytes(packReport.modelBytesBefore), - after: formatBytes(packReport.modelBytesAfter) - })}` + (packReport.omittedUntexturedFaces ? ` -${tr("dap.export.omitted", { - faces: packReport.omittedUntexturedFaces, - elements: packReport.omittedEmptyElements - })}` : "") : ""; - const commands = includesDatapack(mode) ? ` - -${tr("dap.export.commands")} -/function ${dataNamespace}:give -/function ${dataNamespace}:play_loop` : ""; + ); + const versionNotice = el("div", tr("dap.hand.skin_version_help")); + versionNotice.className = "dap-hand-version-notice"; + versionNotice.style.cssText = "margin:12px 0;padding:12px;border:1px solid var(--color-warning);border-radius:6px;"; + content2.appendChild(versionNotice); + if (!settings.handRenderingEnabled) return; + const shaderNotice = el("div"); + shaderNotice.className = "dap-hand-shader-notice"; + shaderNotice.style.cssText = "margin:12px 0;padding:12px;border:1px solid var(--color-warning);border-radius:6px;"; + const shaderBadge = el("strong", tr("dap.hand.shader_incompatible")); + shaderBadge.style.color = "var(--color-warning)"; + shaderNotice.appendChild(shaderBadge); + shaderNotice.appendChild(el("p", tr("dap.hand.shader_incompatible_help"))); + content2.appendChild(shaderNotice); + const actions = el("div"); + actions.className = "dap-hand-actions"; + const actionButton = (label, click, danger = false) => { + const button = el("button", label); + if (danger) { + button.style.color = "#e25d68"; + button.style.marginLeft = "auto"; + } + button.onclick = click; + actions.appendChild(button); + }; + actionButton(tr("dap.hand.delete"), () => { Blockbench.showMessageBox({ - title: tr("dap.export.complete"), - message: `${tr("dap.export.locations", { count, locations })}${optimization}${commands}`, - icon: "check_circle" + title: tr("dap.hand.delete_title"), + message: tr("dap.hand.delete_message"), + icon: "warning", + buttons: [tr("dap.export.cancel"), tr("dap.hand.delete_confirm")], + confirmIndex: 1, + cancelIndex: 0 + }, (button) => { + if (button !== 1) return; + deleteHandRig(settings); + settings.handRenderingEnabled = false; + persist(); + content2.innerHTML = ""; + renderHands(); }); - } catch (err) { - console.error("Export failed", err); - Blockbench.showMessageBox({ - title: tr("dap.export.failed"), - message: tr("dap.export.write_error", { - error: err.message ?? String(err) - }), - icon: "error" + }, true); + content2.appendChild(actions); + }; + const pages = [ + ["settings", tr("dap.settings.page.general"), function renderGeneral() { + pageTitle(content2, tr("dap.settings.page.general"), tr("dap.settings.general_desc")); + textField(content2, tr("dap.export.pack_name"), settings.packName, (v) => { + settings.packName = v; + persist(); + }, identifierValidation); + textField(content2, tr("dap.export.project_name"), settings.projectName, (v) => { + settings.projectName = v; + persist(); + }, identifierValidation); + selectField( + content2, + tr("dap.export.base_item"), + settings.handRenderingEnabled ? "minecraft:player_head" : settings.baseItem, + VANILLA_ITEM_IDS.map((id) => [`minecraft:${id}`, `minecraft:${id}`]), + (v) => { + settings.baseItem = v; + persist(); + }, + settings.handRenderingEnabled + ); + textField(content2, tr("dap.export.display_name"), settings.displayName, (v) => { + settings.displayName = v; + persist(); }); - } - } catch (err) { - Blockbench.showMessageBox({ - title: tr("dap.export.prepare_failed"), - message: err.message ?? String(err), - icon: "error" - }); - } - } - function openExportDialog() { - if (exportInProgress) { - Blockbench.showQuickMessage(tr("dap.export.busy"), 2e3); - return; - } - const animation = Animation.selected ?? Animation.all[0]; - if (!animation) { - Blockbench.showMessageBox({ - title: tr("dap.export.no_animation_title"), - message: tr("dap.export.no_animation_message"), - icon: "error" - }); - return; - } - const fps = animation.snapping || GAME_FPS; - const frameCount = frameCountFor(animation.length, fps); - const gameFrameCount = frameCountFor(animation.length, GAME_FPS); - const defaultPackName = sanitizeId(animation.name, "display_animation"); - const defaultItemModel = sanitizeId(Project?.name ?? "", defaultPackName); - const fpsText = fps === GAME_FPS ? tr("dap.export.fps_exact", { frames: gameFrameCount }) : tr("dap.export.fps_resample", { - source_fps: fps, - source_frames: frameCount, - game_fps: GAME_FPS, - game_frames: gameFrameCount - }); - const helpText = tr("dap.export.folder_help"); - new Dialog(DIALOG_ID, { - title: tr("dap.export.title"), - form: { - info: { type: "info", text: subtleInfo(fpsText), full_width: true }, - help: { type: "info", text: subtleInfo(helpText), full_width: true }, - output_mode: { - label: tr("dap.export.output"), - type: "select", - value: "both_default", - options: outputModes() - }, - pack_name: { label: tr("dap.export.pack_name"), type: "text", value: defaultPackName }, - asset_namespace: { label: tr("dap.export.asset_namespace"), type: "text", value: "kaleidoscope_lab" }, - item_model: { label: tr("dap.export.item_model"), type: "text", value: defaultItemModel }, - base_item: { - label: tr("dap.export.base_item"), - type: "select", - value: "minecraft:potion", - options: VANILLA_ITEM_OPTIONS - }, - display_name: { label: tr("dap.export.display_name"), type: "text", value: animation.name }, - data_namespace: { label: tr("dap.export.data_namespace"), type: "text", value: defaultPackName }, - frame_objective: { label: tr("dap.export.frame_objective"), type: "text", value: "dap_frame" }, - mode_objective: { label: tr("dap.export.mode_objective"), type: "text", value: "dap_mode" }, - playing_tag: { label: tr("dap.export.playing_tag"), type: "text", value: "dap_playing" } - }, - onConfirm(result) { - exportInProgress = true; - void runExport(result, animation).catch((err) => { - console.error("Unexpected export failure", err); - Blockbench.showMessageBox({ - title: tr("dap.export.failed"), - message: err.message ?? String(err), - icon: "error" + checkboxField(content2, tr("dap.settings.developer_tips"), tr("dap.settings.developer_tips_help"), settings.debugEnabled === true, (v) => { + settings.debugEnabled = v; + persist(); + }); + }], + ["pan_tool", tr("dap.settings.page.hands"), renderHands], + ["animation", tr("dap.settings.page.animations"), function renderAnimations() { + pageTitle(content2, tr("dap.settings.page.animations"), tr("dap.settings.animations_desc")); + const selectedAnimations = animations.filter((animation) => settings.selectedAnimationUuids.includes(animation.uuid)); + const conflicts = findAnimationKeyConflicts(selectedAnimations); + const selectionStatus = validationLine(); + showValidation(selectionStatus, !selectedAnimations.length ? { state: "error", message: tr("dap.settings.animations_empty") } : conflicts.length ? { state: "error", message: tr("dap.settings.animations_invalid", { details: conflicts.map((item) => `${item.key || tr("dap.export.invalid_key")}: ${item.animationNames.join(", ")}`).join("; ") }) } : { state: "valid", message: tr("dap.settings.animations_valid", { count: selectedAnimations.length }) }); + selectionStatus.style.marginBottom = "12px"; + content2.appendChild(selectionStatus); + checkboxField( + content2, + tr("dap.settings.exact_bounds_export"), + tr("dap.settings.exact_bounds_export_help"), + settings.exactBoundsOnExport, + (value) => { + settings.exactBoundsOnExport = value; + persist(); + } + ); + const modelFingerprint = modelBoundsFingerprint(); + for (const animation of animations) { + const row = fieldWrap(); + row.style.padding = "10px"; + row.style.border = "1px solid var(--color-border)"; + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.checked = settings.selectedAnimationUuids.includes(animation.uuid); + checkbox.onchange = () => { + settings.selectedAnimationUuids = checkbox.checked ? [.../* @__PURE__ */ new Set([...settings.selectedAnimationUuids, animation.uuid])] : settings.selectedAnimationUuids.filter((uuid) => uuid !== animation.uuid); + if (!settings.selectedAnimationUuids.includes(settings.defaultAnimationUuid)) settings.defaultAnimationUuid = settings.selectedAnimationUuids[0] ?? ""; + persist(); + content2.innerHTML = ""; + renderAnimations(); + }; + row.appendChild(checkbox); + const label = el("span", ` ${animation.name} \u2192 ${animationKeyFromName(animation.name) || tr("dap.export.invalid_key")}`); + label.style.flex = "1"; + row.appendChild(label); + const status = detectionStatus(Project, animation, modelFingerprint); + const statusKey = status.exact ? status.exact.hits.length ? "dap.bounds.status.exact_failed" : "dap.bounds.status.exact_passed" : status.quick ? status.quick.hits.length ? "dap.bounds.status.quick_failed" : "dap.bounds.status.quick_passed" : status.stale ? "dap.bounds.status.stale" : "dap.bounds.status.unchecked"; + const badge = el("span", tr(statusKey)); + badge.style.fontSize = "inherit"; + badge.style.color = status.exact || status.quick ? "var(--color-accent)" : "var(--color-subtle_text)"; + row.style.display = "flex"; + row.style.alignItems = "center"; + row.style.gap = "7px"; + row.appendChild(badge); + content2.appendChild(row); + } + if (selectedAnimations.length) { + selectField(content2, tr("dap.export.default_animation"), settings.defaultAnimationUuid, selectedAnimations.map((a) => [a.uuid, `${a.name} (${animationKeyFromName(a.name)})`]), (v) => { + settings.defaultAnimationUuid = v; + persist(); }); - }).finally(() => { - exportInProgress = false; + } + }], + ["folder", tr("dap.settings.page.files"), function renderFiles() { + pageTitle(content2, tr("dap.settings.page.files"), tr("dap.settings.files_desc")); + const rerender = () => { + content2.innerHTML = ""; + renderFiles(); + }; + selectField(content2, tr("dap.export.write_mode"), settings.writeMode, [ + ["create", tr("dap.export.write_mode.create")], + ["insert", tr("dap.export.write_mode.insert")] + ], (v) => { + settings.writeMode = v; + persist(); + rerender(); }); - } - }).show(); - } - - // src/format.ts - var FORMAT_ID = "display_animation_sequence"; - var FORMAT_COORDINATE_OPTIONS = { - centered_grid: false - }; - var ownedFormat = null; - function createFormat(id) { - const javaBlockCodec = resolveJavaBlockCodec(); - return new ModelFormat(id, { - id, - name: tr("dap.format.name"), - icon: "icon-format_block", - category: "minecraft", - target: "Minecraft: Java Edition", - description: tr("dap.format.description"), - show_in_start_screen: true, - box_uv: false, - optional_box_uv: true, - single_texture: false, - bone_rig: true, - ...FORMAT_COORDINATE_OPTIONS, - rotate_cubes: true, - integer_size: false, - animation_mode: true, - display_mode: true, - codec: javaBlockCodec + selectField(content2, tr("dap.export.output"), settings.outputMode, [ + ["both_default", tr("dap.export.mode.both_default")], + ["both_separate", tr("dap.export.mode.both_separate")], + ["resource_only", tr("dap.export.mode.resource_only")], + ["datapack_only", tr("dap.export.mode.datapack_only")] + ], (v) => { + settings.outputMode = v; + persist(); + rerender(); + }); + if (settings.outputMode === "both_default") { + folderField(content2, tr("dap.settings.shared_root"), settings.sharedRoot, "display_anim_settings_shared", (v) => folderValidation(v, "shared", settings.writeMode === "insert", settings.packName), (v) => { + settings.sharedRoot = v; + persist(); + }); + } else { + if (settings.outputMode !== "datapack_only") { + folderField(content2, tr("dap.settings.resource_folder"), settings.resourcePackFolder, "display_anim_settings_resource", (v) => folderValidation(v, "resource", settings.writeMode === "insert"), (v) => { + settings.resourcePackFolder = v; + persist(); + }); + } + if (settings.outputMode !== "resource_only") { + folderField(content2, tr("dap.settings.datapack_folder"), settings.datapackFolder, "display_anim_settings_datapack", (v) => folderValidation(v, "datapack", settings.writeMode === "insert"), (v) => { + settings.datapackFolder = v; + persist(); + }); + } + } + }], + ["database", tr("dap.settings.page.datapack"), () => { + pageTitle(content2, tr("dap.settings.page.datapack"), tr("dap.settings.datapack_desc")); + textField(content2, tr("dap.export.frame_objective"), settings.frameObjective, (v) => { + settings.frameObjective = v; + persist(); + }, objectiveNameValidation); + textField(content2, tr("dap.export.mode_objective"), settings.modeObjective, (v) => { + settings.modeObjective = v; + persist(); + }, objectiveNameValidation); + textField(content2, tr("dap.export.max_frame_objective"), settings.maxFrameObjective, (v) => { + settings.maxFrameObjective = v; + persist(); + }, objectiveNameValidation); + textField(content2, tr("dap.export.playing_tag"), settings.playingTag, (v) => { + settings.playingTag = v; + persist(); + }, playingTagValidation); + }], + ["code", tr("dap.settings.page.api"), () => { + pageTitle(content2, tr("dap.settings.page.api"), tr("dap.settings.api_desc")); + const project = settings.projectName || ""; + const animationPlaceholder = `<${tr("dap.settings.api_animation_placeholder")}>`; + const playerPlaceholder = `<${tr("dap.settings.api_player_placeholder")}>`; + copyableCode(content2, tr("dap.settings.api_item"), tr("dap.settings.api_item_note"), `${EXPORT_NAMESPACE}:${project}`); + copyableCode(content2, tr("dap.settings.api_common"), tr("dap.settings.api_common_note"), `/function ${EXPORT_NAMESPACE}:${project}/give +/function ${EXPORT_NAMESPACE}:${project}/stop`); + copyableCode(content2, tr("dap.settings.api_short"), tr("dap.settings.api_short_note"), `/function ${EXPORT_NAMESPACE}:${project}/play/${animationPlaceholder} +/function ${EXPORT_NAMESPACE}:${project}/loop/${animationPlaceholder} +/function ${EXPORT_NAMESPACE}:${project}/frame/${animationPlaceholder} {frame:12}`); + copyableCode(content2, tr("dap.settings.api_macro"), tr("dap.settings.api_macro_note"), `/function ${EXPORT_NAMESPACE}:${project}/play {animation:"${animationPlaceholder}",mode:"once"} +/function ${EXPORT_NAMESPACE}:${project}/play {animation:"${animationPlaceholder}",mode:"loop"} +/function ${EXPORT_NAMESPACE}:${project}/frame {animation:"${animationPlaceholder}",frame:12}`); + copyableCode(content2, tr("dap.settings.api_context"), tr("dap.settings.api_context_note"), `execute as ${playerPlaceholder} run function ${EXPORT_NAMESPACE}:${project}/play/${animationPlaceholder}`); + const reference = el("p", tr("dap.settings.api_reference", { project })); + reference.style.color = "var(--color-subtle_text)"; + reference.style.lineHeight = "1.55"; + content2.appendChild(reference); + }] + ]; + const sidebarButtons = []; + let activePage = 0; + const showPage = (index, render) => { + activePage = index; + content2.innerHTML = ""; + sidebarButtons.forEach((button, buttonIndex) => { + button.style.background = buttonIndex === index ? "var(--color-selected)" : "transparent"; + button.style.borderLeft = buttonIndex === index ? "5px solid var(--color-accent)" : "5px solid transparent"; + button.style.color = buttonIndex === index ? "var(--color-text)" : "var(--color-subtle_text)"; + }); + render(); + }; + pages.forEach(([icon, title, render], index) => { + const button = el("button"); + button.className = "dap-sidebar-button"; + button.innerHTML = `${icon}${title}`; + button.style.display = "flex"; + button.style.alignItems = "center"; + button.style.gap = "9px"; + button.style.width = "100%"; + button.style.height = "50px"; + button.style.padding = "0 18px"; + button.style.marginBottom = "0"; + button.style.textAlign = "left"; + button.style.border = "0"; + button.style.borderLeft = "5px solid transparent"; + button.style.borderRadius = "0"; + button.onmouseenter = () => { + button.style.color = "var(--color-text)"; + if (activePage !== index) button.style.background = "var(--color-back)"; + }; + button.onmouseleave = () => { + button.style.color = activePage === index ? "var(--color-text)" : "var(--color-subtle_text)"; + button.style.background = activePage === index ? "var(--color-selected)" : "transparent"; + }; + button.onclick = () => showPage(index, render); + sidebarButtons.push(button); + sidebar.appendChild(button); }); - } - function registerModelFormat() { - if (!Formats[FORMAT_ID]) { - ownedFormat = createFormat(FORMAT_ID); - console.log(`Registered custom format "${FORMAT_ID}".`); - } - } - function unregisterModelFormat() { - if (ownedFormat && Formats[FORMAT_ID] === ownedFormat) { - ownedFormat.delete(); - console.log(`Unregistered custom format "${FORMAT_ID}".`); - } - ownedFormat = null; + const footer = el("div"); + footer.style.padding = "10px 18px"; + footer.style.borderTop = "1px solid var(--color-border)"; + footer.style.display = "flex"; + footer.style.justifyContent = "space-between"; + footer.appendChild(el("span", tr("dap.settings.saved_hint"))); + const close = el("button", tr("dap.settings.close")); + close.style.minWidth = "96px"; + close.style.height = "38px"; + close.style.borderRadius = "0"; + close.onclick = disposeProjectSettingsDialog; + footer.appendChild(close); + shell.appendChild(footer); + overlay.appendChild(shell); + document.body.appendChild(overlay); + showPage(0, pages[0][2]); } // src/plugin.ts var OPEN_ACTION_ID = "display_anim_preview_open_action"; var CHECK_BOUNDS_ACTION_ID = "display_anim_preview_check_bounds"; var EXPORT_ACTION_ID = "display_anim_preview_export_packs"; + var SETTINGS_ACTION_ID = "display_anim_preview_project_settings"; + var TOOLS_MENU_ID = "display_anim_preview_tools"; + var FIRST_PERSON_ACTION_ID = "display_anim_first_person_open"; + var firstPersonAction = null; var openAction = null; var checkBoundsAction = null; var exportAction = null; - function checkAnimationBounds() { - const animation = Animation.selected ?? Animation.all[0]; - if (!animation) { - Blockbench.showQuickMessage(tr("dap.bounds.no_animation"), 2e3); - return; - } - const fps = animation.snapping || 20; - const frameCount = frameCountFor(animation.length, fps); - const { frames, outOfBounds } = bakeFrames(frameCount, fps); - const description = describeOutOfBounds(outOfBounds); - if (!description) { - Blockbench.showMessageBox({ - title: tr("dap.bounds.passed_title"), - message: tr("dap.bounds.passed_message", { frames: frames.length, fps }), - icon: "check_circle" - }); - return; - } - Blockbench.showMessageBox({ - title: tr("dap.export.bounds_title"), - message: description, - icon: "warning" - }); + var settingsAction = null; + var toolsMenu = null; + function openNewProjectPreview(data) { + const event = data; + if (event?.project?.format?.id === FORMAT_ID) openControlPanel(); } Plugin.register("display_anim_preview", { title: isChineseOnlyBuild() ? "Java \u9010\u5E27\u663E\u793A\u52A8\u753B" : "Java Display Animator", author: "rieyi", - description: isChineseOnlyBuild() ? "\u6309\u663E\u793A\u4F4D\u7F6E\u9884\u89C8 Minecraft Java \u9010\u5E27\u70D8\u7119\u7269\u54C1\u52A8\u753B\uFF0C\u5E76\u5BFC\u51FA\u5B8C\u6574\u8D44\u6E90\u5305\u548C\u52A8\u753B\u9A71\u52A8\u6570\u636E\u5305\u3002" : "Preview frame-baked Minecraft Java item animations per display context and export complete resource packs and animation-driving datapacks.", + description: isChineseOnlyBuild() ? "\u6309\u663E\u793A\u4F4D\u7F6E\u9884\u89C8 Minecraft Java \u9010\u5E27\u70D8\u7119\u7269\u54C1\u52A8\u753B\uFF0C\u5E76\u5C06\u591A\u6BB5\u52A8\u753B\u5BFC\u51FA\u4E3A\u5B8C\u6574\u8D44\u6E90\u5305\u548C\u52A8\u753B\u9A71\u52A8\u6570\u636E\u5305\u3002" : "Preview frame-baked Minecraft Java item animations per display context and export multiple animations as complete resource packs and animation-driving datapacks.", ...true ? {} : { about: isChineseOnlyBuild() ? ABOUT_ZH : ABOUT_EN }, icon: "icon.png", tags: ["Minecraft: Java Edition", "Animation", "Exporter"], - version: "1.0.0", + version: "1.1.2", min_version: "5.1.5", variant: "desktop", creation_date: "2026-08-07", @@ -3566,9 +7520,29 @@ ${tr("dap.export.commands")} }, onload() { registerTranslations(); + registerHandRigProperties(); registerDisplayAnimationProperty(); + registerExportAnimationSettingsProperty(); registerModelFormat(); + Blockbench.on("new_project", openNewProjectPreview); initializePlaybackSync(); + registerFirstPersonPanel(); + firstPersonAction = new Action(FIRST_PERSON_ACTION_ID, { + name: tr("dap.fp.open"), + description: tr("dap.fp.open_desc"), + icon: "visibility", + category: "animation", + condition: () => Modes.selected.id === "animate" && [FORMAT_ID, "java_block_sequence"].includes(Format.id), + click: openFirstPersonPanel + }); + const firstPersonIndex = Toolbars.timeline.children.findIndex( + (item) => (typeof item === "string" ? item : item.id) === FIRST_PERSON_ACTION_ID + ); + Toolbars.timeline.children = Toolbars.timeline.children.filter( + (item) => (typeof item === "string" ? item : item.id) !== FIRST_PERSON_ACTION_ID + ); + Toolbars.timeline.add(firstPersonAction, firstPersonIndex < 0 ? void 0 : firstPersonIndex); + MenuBar.addAction(firstPersonAction, "animation"); openAction = new Action(OPEN_ACTION_ID, { name: tr("dap.action.open"), description: tr("dap.action.open_desc"), @@ -3584,7 +7558,7 @@ ${tr("dap.export.commands")} description: tr("dap.action.bounds_desc"), icon: "settings_overscan", category: "animation", - click: checkAnimationBounds + click: () => runBoundsCheck() }); exportAction = new Action(EXPORT_ACTION_ID, { name: tr("dap.action.export"), @@ -3593,27 +7567,45 @@ ${tr("dap.export.commands")} category: "animation", click: openExportDialog }); - for (const action of [openAction, checkBoundsAction, exportAction]) { - for (const path of ["filter", "tools"]) { - try { - MenuBar.addAction(action, path); - } catch (err) { - console.warn(`Menu path "${path}" not available, skipping`, err); - } - } - } + settingsAction = new Action(SETTINGS_ACTION_ID, { + name: tr("dap.action.settings"), + description: tr("dap.action.settings_desc"), + icon: "tune", + category: "animation", + click: openProjectSettingsDialog + }); + MenuBar.addAction(exportAction, "file.export"); + toolsMenu = new Action(TOOLS_MENU_ID, { + name: tr("dap.menu.name"), + icon: "movie", + searchable: false, + children: [settingsAction, openAction, firstPersonAction, checkBoundsAction, "_", exportAction] + }); + MenuBar.addAction(toolsMenu, "tools"); }, onunload() { + Blockbench.removeListener("new_project", openNewProjectPreview); + toolsMenu?.delete(); + toolsMenu = null; + firstPersonAction?.delete(); + firstPersonAction = null; + disposeFirstPersonPanel(); disposeControlPanel(); disposePlaybackSync(); + disposeProjectSettingsDialog(); + disposeBoundsCheckPanel(); openAction?.delete(); openAction = null; checkBoundsAction?.delete(); checkBoundsAction = null; exportAction?.delete(); exportAction = null; + settingsAction?.delete(); + settingsAction = null; unregisterModelFormat(); unregisterDisplayAnimationProperty(); + unregisterExportAnimationSettingsProperty(); + unregisterHandRigProperties(); } }); })();