+
+
+
+
+
Created by Titus Evans (SnaveSutit)
+
+ {PACKAGE.description}
+
+
+
Special Thanks to
+
+
+
Ian Senne (FetchBot)
+
For always being there to help.
+
+
+
Jesse Spicer (Dominexis)
+
For making quaternions and matrices make sense.
+
+
+
Jannis Petersen
+
For creating an amazing tool and community.
+
+
+
Ewan Howell
+
For keeping me sane as I dig through Blockbench source code.
+
+
+
Boxic
+
For throwing absurd amounts of money at me.
+
+
+
Flubberschnub
+
For showing off the power of AJ with his boss fights.
+
+
+
Cool People who helped make AJ what it is today
+
+ {#each PACKAGE.special_thanks.sort() as special}
+
{special}
+ {/each}
+
+
+
+
diff --git a/src/components/animatedJavaLoadingPopup.svelte b/src/components/animatedJavaLoadingPopup.svelte
new file mode 100644
index 00000000..98b9a1e3
--- /dev/null
+++ b/src/components/animatedJavaLoadingPopup.svelte
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {#if !$autoBoundingBox}
+
+ {/if}
+
+
+
+
+
+
+
+ {#if $enablePluginMode}
+
+ {:else}
+
+
+
+ {#if $enableResourcePack}
+
+
+ {#if $enableAdvancedResourcePackSettings}
+
+
+ {translate('dialog.blueprint_settings.advanced_settings_warning')}
+
+
+
+
+
+
+
+
+
+
+ {:else}
+
+
+
+
+
+ {/if}
+ {/if}
+
+ {#if $enableDataPack}
+
+
+ {#if $enableAdvancedDataPackSettings}
+
+ {translate('dialog.blueprint_settings.advanced_settings_warning')}
+
+
+
+ {:else}
+
+ {/if}
+
+
+
+
+
+
+
+ {/if}
+ {/if}
+
+
+
diff --git a/src/components/boneConfigDialog.svelte b/src/components/boneConfigDialog.svelte
new file mode 100644
index 00000000..0bb13328
--- /dev/null
+++ b/src/components/boneConfigDialog.svelte
@@ -0,0 +1,246 @@
+
+
+
+
+
+
+ Selected Variant: {variant.displayName}
+
+ {#if variant.isDefault}
+
+ The settings below will be applied to this bone by default.
+
+ {:else}
+
+ The settings below will be applied to this bone only when this variant is applied.
+
+ {/if}
+
+ {#if pluginModeEnabled}
+
+
+
+
+
+
+
+
+ {#if $overrideGlowColor}
+
+ {/if}
+
+
+
+
+
+
+
+ {#if $overrideBrightness}
+
+ {/if}
+
+
+
+
+ {:else}
+
+
+
+
+ {#if $useNBT}
+
+ {translate('dialog.bone_config.use_nbt.use_nbt_warning')}
+
+
+ {:else}
+
+
+
+
+
+
+ {#if $overrideGlowColor}
+
+ {/if}
+
+
+
+
+
+
+
+ {#if $overrideBrightness}
+
+ {/if}
+
+
+
+
+ {/if}
+ {/if}
+
+
+
diff --git a/src/components/customKeyframePanel.svelte b/src/components/customKeyframePanel.svelte
new file mode 100644
index 00000000..aca54111
--- /dev/null
+++ b/src/components/customKeyframePanel.svelte
@@ -0,0 +1,57 @@
+
+
+
+
+
+ {#if selectedKeyframe.channel === 'variant'}
+
+ {:else if selectedKeyframe.channel === 'commands'}
+
+ {:else}
+
Unknown keyframe channel: '{selectedKeyframe.channel}'
+ {/if}
+
+
+ {translate('panel.keyframe.execute_condition.title')}
+
+
+
+
+
+
diff --git a/src/components/dialogItems/baseDialogItem.svelte b/src/components/dialogItems/baseDialogItem.svelte
new file mode 100644
index 00000000..975c5fc3
--- /dev/null
+++ b/src/components/dialogItems/baseDialogItem.svelte
@@ -0,0 +1,75 @@
+
+
+
+
+ =
+ | ((value: Value) => { type: string; message: string })
+ | undefined
+
+type CollectionItem = { name: string; value: string }
diff --git a/src/components/dialogItems/fileSelect.svelte b/src/components/dialogItems/fileSelect.svelte
new file mode 100644
index 00000000..1e719b1a
--- /dev/null
+++ b/src/components/dialogItems/fileSelect.svelte
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
diff --git a/src/components/dialogItems/folderSelect.svelte b/src/components/dialogItems/folderSelect.svelte
new file mode 100644
index 00000000..7274ef93
--- /dev/null
+++ b/src/components/dialogItems/folderSelect.svelte
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
diff --git a/src/components/dialogItems/lineInput.svelte b/src/components/dialogItems/lineInput.svelte
new file mode 100644
index 00000000..dfc7dd75
--- /dev/null
+++ b/src/components/dialogItems/lineInput.svelte
@@ -0,0 +1,49 @@
+
+
+
+
+ {label}
+
+
+
+
+
diff --git a/src/components/dialogItems/numberSlider.svelte b/src/components/dialogItems/numberSlider.svelte
new file mode 100644
index 00000000..a90e756b
--- /dev/null
+++ b/src/components/dialogItems/numberSlider.svelte
@@ -0,0 +1,71 @@
+
+
+
+
+
diff --git a/src/components/dialogItems/sectionHeader.svelte b/src/components/dialogItems/sectionHeader.svelte
new file mode 100644
index 00000000..05f3b32a
--- /dev/null
+++ b/src/components/dialogItems/sectionHeader.svelte
@@ -0,0 +1,23 @@
+
+
+
+
+
diff --git a/src/components/dialogItems/select.svelte b/src/components/dialogItems/select.svelte
new file mode 100644
index 00000000..e5cda793
--- /dev/null
+++ b/src/components/dialogItems/select.svelte
@@ -0,0 +1,33 @@
+
+
+
+
+ {label}
+
+
diff --git a/src/components/dialogItems/vector2d.svelte b/src/components/dialogItems/vector2d.svelte
new file mode 100644
index 00000000..194cf4aa
--- /dev/null
+++ b/src/components/dialogItems/vector2d.svelte
@@ -0,0 +1,118 @@
+
+
+
+
+
diff --git a/src/components/dialogItems/vector3d.svelte b/src/components/dialogItems/vector3d.svelte
new file mode 100644
index 00000000..2d0fcfec
--- /dev/null
+++ b/src/components/dialogItems/vector3d.svelte
@@ -0,0 +1,139 @@
+
+
+
+
+
diff --git a/src/components/exportProgressDialog.svelte b/src/components/exportProgressDialog.svelte
new file mode 100644
index 00000000..84303920
--- /dev/null
+++ b/src/components/exportProgressDialog.svelte
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
{$progressDescription}
+
+
+
+ {#if $progress !== 0}
+
+ {/if}
+
+
+
diff --git a/src/components/formatPage.svelte b/src/components/formatPage.svelte
new file mode 100644
index 00000000..4a79972f
--- /dev/null
+++ b/src/components/formatPage.svelte
@@ -0,0 +1,49 @@
+
+
+Create advanced animated models in Vanilla Java Edition
+
+
+ Target :
+ Minecraft: Java Edition
+
+
+
+ Good to know:
+
+
+
+ The rotations of elements are limited to 22.5 degree steps, and only one axis can be rotated
+ at a time. However, the rotation of bones is not restricted.
+
+
+ Documentation for Animated Java can be found at https://animated-java.dev/
+
+
+
+
+ createBlueprint()}>
+ arrow_forward
+ Create new AJ Blueprint
+
+
+
+
diff --git a/src/components/importAJModelLoaderDialog.svelte b/src/components/importAJModelLoaderDialog.svelte
new file mode 100644
index 00000000..3c55b1ec
--- /dev/null
+++ b/src/components/importAJModelLoaderDialog.svelte
@@ -0,0 +1,34 @@
+
+
+
+
+{translate('action.upgrade_old_aj_model_loader.body')}
+
+
+ {translate('action.upgrade_old_aj_model_loader.button')}
+
+
+
diff --git a/src/components/keyframePanels/commandsKeyframePanel.svelte b/src/components/keyframePanels/commandsKeyframePanel.svelte
new file mode 100644
index 00000000..4e91debe
--- /dev/null
+++ b/src/components/keyframePanels/commandsKeyframePanel.svelte
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+ {translate('panel.keyframe.commands.title')}
+
+
+
+
+
+
+ {translate('panel.keyframe.repeat.title')}
+
+
+
+
+
+
+ {translate('panel.keyframe.repeat_frequency.title')}
+
+
+
+
+
diff --git a/src/components/keyframePanels/variantKeyframePanel.svelte b/src/components/keyframePanels/variantKeyframePanel.svelte
new file mode 100644
index 00000000..2965f67c
--- /dev/null
+++ b/src/components/keyframePanels/variantKeyframePanel.svelte
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+ {translate('panel.keyframe.variant.title')}
+
+
+
+
+
diff --git a/src/components/locatorConfigDialog.svelte b/src/components/locatorConfigDialog.svelte
new file mode 100644
index 00000000..41b6dd78
--- /dev/null
+++ b/src/components/locatorConfigDialog.svelte
@@ -0,0 +1,76 @@
+
+
+
+
+
+ {#if pluginModeEnabled}
+ {#each translate('dialog.locator_config.plugin_mode_warning').split('\n') as line}
+
{line}
+ {/each}
+ {:else}
+
+
+ {#if $useEntity}
+
+
+
+ {/if}
+
+
+ {/if}
+
+
+
diff --git a/src/components/projectTitle.svelte b/src/components/projectTitle.svelte
new file mode 100644
index 00000000..6f398a8a
--- /dev/null
+++ b/src/components/projectTitle.svelte
@@ -0,0 +1,25 @@
+
+
+
+
+{#if $pluginMode}
+
+{:else}
+
+{/if}
+
+
diff --git a/src/components/textDisplayConfigDialog.svelte b/src/components/textDisplayConfigDialog.svelte
new file mode 100644
index 00000000..b27d55f3
--- /dev/null
+++ b/src/components/textDisplayConfigDialog.svelte
@@ -0,0 +1,191 @@
+
+
+
+
+
+ {#if pluginModeEnabled}
+
+
+
+
+
+
+ {#if $overrideGlowColor}
+
+ {/if}
+
+
+
+
+
+
+
+ {#if $overrideBrightness}
+
+ {/if}
+
+
+ {:else}
+
+
+ {#if $useNBT}
+
+ {translate('dialog.text_display_config.use_nbt.use_nbt_warning')}
+
+
+ {:else}
+
+
+
+
+
+
+ {#if $overrideGlowColor}
+
+ {/if}
+
+
+
+
+
+
+
+ {#if $overrideBrightness}
+
+ {/if}
+
+
+ {/if}
+ {/if}
+
+
+
diff --git a/src/components/textDisplayElementPanel.svelte b/src/components/textDisplayElementPanel.svelte
new file mode 100644
index 00000000..af80cec7
--- /dev/null
+++ b/src/components/textDisplayElementPanel.svelte
@@ -0,0 +1,132 @@
+
+
+
+
+
+ {translate('panel.text_display.title')}
+
+
+
+
+
+
+
diff --git a/src/components/unexpectedErrorDialog.svelte b/src/components/unexpectedErrorDialog.svelte
new file mode 100644
index 00000000..e70000e3
--- /dev/null
+++ b/src/components/unexpectedErrorDialog.svelte
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
{@html pickRandomFlavorQuote()}
+
+
+
+ {@html translate(
+ 'dialog.unexpected_error.paragraph',
+ 'Discord ',
+ 'Github ',
+ )}
+
+
+
+
+
+
+
+
+
diff --git a/src/components/vanillaBlockDisplayConfigDialog.svelte b/src/components/vanillaBlockDisplayConfigDialog.svelte
new file mode 100644
index 00000000..bebc2717
--- /dev/null
+++ b/src/components/vanillaBlockDisplayConfigDialog.svelte
@@ -0,0 +1,189 @@
+
+
+
+
+
+ {#if pluginModeEnabled}
+
+
+
+
+
+
+ {#if $overrideGlowColor}
+
+ {/if}
+
+
+
+
+
+
+
+ {#if $overrideBrightness}
+
+ {/if}
+
+
+ {:else}
+
+
+ {#if $useNBT}
+
+ {translate('dialog.bone_config.use_nbt.use_nbt_warning')}
+
+
+ {:else}
+
+
+
+
+
+
+ {#if $overrideGlowColor}
+
+ {/if}
+
+
+
+
+
+
+
+ {#if $overrideBrightness}
+
+ {/if}
+
+
+ {/if}
+ {/if}
+
+
+
diff --git a/src/components/vanillaBlockDisplayElementPanel.svelte b/src/components/vanillaBlockDisplayElementPanel.svelte
new file mode 100644
index 00000000..d4c8ebd4
--- /dev/null
+++ b/src/components/vanillaBlockDisplayElementPanel.svelte
@@ -0,0 +1,71 @@
+
+
+
+
+
+ {translate('panel.vanilla_block_display.title')}
+
+
+
+
+
+ {$error}
+
+
+
diff --git a/src/components/vanillaItemDisplayConfigDialog.svelte b/src/components/vanillaItemDisplayConfigDialog.svelte
new file mode 100644
index 00000000..bebc2717
--- /dev/null
+++ b/src/components/vanillaItemDisplayConfigDialog.svelte
@@ -0,0 +1,189 @@
+
+
+
+
+
+ {#if pluginModeEnabled}
+
+
+
+
+
+
+ {#if $overrideGlowColor}
+
+ {/if}
+
+
+
+
+
+
+
+ {#if $overrideBrightness}
+
+ {/if}
+
+
+ {:else}
+
+
+ {#if $useNBT}
+
+ {translate('dialog.bone_config.use_nbt.use_nbt_warning')}
+
+
+ {:else}
+
+
+
+
+
+
+ {#if $overrideGlowColor}
+
+ {/if}
+
+
+
+
+
+
+
+ {#if $overrideBrightness}
+
+ {/if}
+
+
+ {/if}
+ {/if}
+
+
+
diff --git a/src/components/vanillaItemDisplayElementPanel.svelte b/src/components/vanillaItemDisplayElementPanel.svelte
new file mode 100644
index 00000000..860373b7
--- /dev/null
+++ b/src/components/vanillaItemDisplayElementPanel.svelte
@@ -0,0 +1,71 @@
+
+
+
+
+
+ {translate('panel.vanilla_item_display.title')}
+
+
+
+
+
+ {$error}
+
+
+
diff --git a/src/components/variantConfigDialog.svelte b/src/components/variantConfigDialog.svelte
new file mode 100644
index 00000000..1790b86e
--- /dev/null
+++ b/src/components/variantConfigDialog.svelte
@@ -0,0 +1,283 @@
+
+
+
+
+
+
+
+ {#key $name}
+ {#if $generateNameFromDisplayName}
+
+ {:else}
+
+ {/if}
+ {/key}
+
+
+
+
+ {$uuid}
+
+
+
+
+ {#key textureMapUpdated}
+ {#each [...textureMap.map.entries()] as entry, index}
+
+
+
+
+
selectNewPrimaryTexture(e, entry[0])}
+ >
+
+ {#each primaryTextures as texture}
+
+ {texture.name}
+
+ {/each}
+
+
+
+ east
+
+
+
+
selectNewSecondaryTexture(e, entry[0])}
+ >
+
+ {#each secondaryTextures as texture}
+
+ {texture.name}
+
+ {/each}
+
+
+
+
+ deleteTextureMapping(entry[0])}>delete
+
+ {:else}
+
+ {translate('dialog.variant_config.texture_map.no_mappings')}
+
+ {/each}
+ {/key}
+
+
+
+
+
diff --git a/src/components/variantsPanel.svelte b/src/components/variantsPanel.svelte
new file mode 100644
index 00000000..3609e367
--- /dev/null
+++ b/src/components/variantsPanel.svelte
@@ -0,0 +1,208 @@
+
+
+
+
+
+
+
diff --git a/src/constants.ts b/src/constants.ts
new file mode 100644
index 00000000..472c888b
--- /dev/null
+++ b/src/constants.ts
@@ -0,0 +1,2 @@
+import PACKAGEJSON from '../package.json'
+export const PACKAGE: typeof PACKAGEJSON = PACKAGEJSON
diff --git a/src/events.ts b/src/events.ts
deleted file mode 100644
index d8a0474e..00000000
--- a/src/events.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { consoleGroupCollapsed } from './util/console'
-import { Subscribable } from './util/subscribable'
-import * as PACKAGE from '../package.json'
-
-export class PluginEvent extends Subscribable {
- protected static events: Record> = {}
-
- constructor(public name: string) {
- super()
- PluginEvent.events[name] = this
- }
-}
-
-export const LOAD = new PluginEvent('load')
-export const UNLOAD = new PluginEvent('unload')
-export const INSTALL = new PluginEvent('install')
-export const UNINSTALL = new PluginEvent('uninstall')
-
-export const INJECT_MODS = new PluginEvent('loadMods')
-export const EXTRACT_MODS = new PluginEvent('unloadMods')
-
-export const LOAD_PROJECT = new PluginEvent('loadProject')
-export const CONVERT_PROJECT = new PluginEvent('convertProject')
-export const SAVE_PROJECT = new PluginEvent('saveProject')
-export const PRE_SELECT_PROJECT = new PluginEvent('preSelectProject')
-export const SELECT_PROJECT = new PluginEvent('selectProject')
-export const POST_SELECT_PROJECT = new PluginEvent('postSelectProject')
-
-export const UNSELECT_PROJECT = new PluginEvent('unselectProject')
-
-export const UPDATE_SELECTION = new PluginEvent('updateSelection')
-
-export const VARIANT_PROPERTIES_UPDATE = new PluginEvent('variantPropertiesUpdate')
-
-type Link = { link: string; section?: string }
-export const DOCS_LINK_CLICKED = new PluginEvent ('onDocsLinkClicked')
-
-const INJECT_HANDLER = consoleGroupCollapsed(
- `Injecting BlockbenchMods added by ${PACKAGE.name}`,
- () => INJECT_MODS.dispatch()
-)
-const EXTRACT_HANDLER = consoleGroupCollapsed(
- `Extracting BlockbenchMods added by ${PACKAGE.name}`,
- () => EXTRACT_MODS.dispatch()
-)
-LOAD.subscribe(INJECT_HANDLER)
-UNLOAD.subscribe(EXTRACT_HANDLER)
-INSTALL.subscribe(INJECT_HANDLER)
-UNINSTALL.subscribe(EXTRACT_HANDLER)
-
-Blockbench.on('load_project', () => LOAD_PROJECT.dispatch())
-Blockbench.on('save_project', () => SAVE_PROJECT.dispatch())
-Blockbench.on('select_project', ({ project }: { project: ModelProject }) => {
- SELECT_PROJECT.dispatch(project)
- queueMicrotask(() => POST_SELECT_PROJECT.dispatch(project))
-})
-Blockbench.on('update_selection', () => UPDATE_SELECTION.dispatch())
-Blockbench.on('unselect_project', () => UNSELECT_PROJECT.dispatch())
diff --git a/src/exporter.ts b/src/exporter.ts
deleted file mode 100644
index a6a66321..00000000
--- a/src/exporter.ts
+++ /dev/null
@@ -1,311 +0,0 @@
-import * as events from './events'
-import { exportResources } from './resourcePackExporter'
-import { GUIStructure } from './guiStructure'
-import { projectSettingStructure } from './projectSettings'
-import { IRenderedAnimation, renderAllAnimations } from './rendering/animationRenderer'
-import { IRenderedRig, renderRig } from './rendering/modelRenderer'
-import { animatedJavaSettings, IInfoPopup, Setting as AJSetting, Setting } from './settings'
-import { openAJExportInProgressDialog } from './ui/ajExportInProgress'
-import { openAjFailedProjectExportReadinessDialog } from './ui/popups/failedProjectExportReadiness'
-import { openUnexpectedErrorDialog } from './ui/popups/unexpectedError'
-import { consoleGroupCollapsed } from './util/console'
-import { ExpectedError } from './util/misc'
-import { NamespacedString } from './util/moddingTools'
-import { translate } from './util/translation'
-
-type ProjectSettings = Record>
-
-export interface IAnimatedJavaExportData {
- ajSettings: typeof animatedJavaSettings
- projectSettings: ProjectSettings
- exporterSettings: S
- renderedAnimations: IRenderedAnimation[]
- rig: IRenderedRig
-}
-
-interface IAnimatedJavaExporterOptions {
- id: NamespacedString
- name: string
- description: string
- getSettings(): S
- settingsStructure: GUIStructure
- onStartup?: () => void
- export(exportData: IAnimatedJavaExportData): Promise | void
-}
-
-export class AnimatedJavaExporter<
- S extends ProjectSettings = Record>
-> {
- static exporters: Record> = {}
- id: NamespacedString
- name: string
- description: string
- getSettings: IAnimatedJavaExporterOptions['getSettings']
- settingsStructure: GUIStructure
- onStartup?: IAnimatedJavaExporterOptions['onStartup']
- export: IAnimatedJavaExporterOptions['export']
- constructor(options: IAnimatedJavaExporterOptions) {
- this.id = options.id
- this.name = options.name
- this.description = options.description
- this.getSettings = options.getSettings
- this.settingsStructure = options.settingsStructure
- this.onStartup = options.onStartup
- this.export = consoleGroupCollapsed(
- `Exporting Animated Java Rig via ${this.name} (${this.id})`,
- options.export
- )
-
- events.LOAD_PROJECT.subscribe(() => {
- if (this.onStartup) this.onStartup()
- }, true)
-
- AnimatedJavaExporter.exporters[this.id] = this
- }
-
- static get all() {
- return Object.values(AnimatedJavaExporter.exporters)
- }
-}
-
-let activelyExporting = false
-export async function safeExportProject() {
- if (activelyExporting) return
- activelyExporting = true
- const dialog = openAJExportInProgressDialog()
- await exportProject().catch(e => {
- Blockbench.setProgress(0)
- Blockbench.setStatusBarText('')
- console.error(e)
- dialog.cancel()
- if (e instanceof ExpectedError) return
- openUnexpectedErrorDialog(e)
- })
- activelyExporting = false
- dialog.cancel()
-}
-
-export const exportProject = consoleGroupCollapsed('exportProject', async () => {
- verifyProjectExportReadiness()
- if (!Project?.animated_java_settings) return // Project being optional is annoying
-
- // Pre-export
- const selectedVariant = Project.animated_java_variants!.selectedVariant!
- Project.animated_java_variants?.select()
-
- const selectedExporterId = Project?.animated_java_settings?.exporter?.selected
- ?.value as NamespacedString
-
- const exporter = AnimatedJavaExporter.exporters[selectedExporterId]
- if (!exporter) throw new Error(`No exporter found with id "${selectedExporterId}"`)
-
- const ajSettings = animatedJavaSettings
- const projectSettings = Project.animated_java_settings
- const exporterSettings = Project.animated_java_exporter_settings![selectedExporterId]
-
- const rigItemId = Project.animated_java_settings.rig_item.value
-
- let textureExportFolder: string, rigExportFolder: string, rigItemModelExportPath: string
- const advancedResourcePackSettingsEnabled =
- Project.animated_java_settings.enable_advanced_resource_pack_settings.value
- if (advancedResourcePackSettingsEnabled) {
- // Advanced Resource Pack settings
- textureExportFolder = Project.animated_java_settings.texture_export_folder.value
- rigExportFolder = Project.animated_java_settings.rig_export_folder.value
- rigItemModelExportPath = Project.animated_java_settings.rig_item_model.value
- console.log('Using advanced resource pack settings')
- } else {
- // Automatic Resource Pack settings
- const resourcePackFolder = PathModule.parse(
- Project.animated_java_settings.resource_pack_mcmeta.value
- ).dir
- const projectNamespace = Project.animated_java_settings.project_namespace.value
- textureExportFolder = PathModule.join(
- resourcePackFolder,
- `assets/animated_java/textures/item/${projectNamespace}/`
- )
- rigExportFolder = PathModule.join(
- resourcePackFolder,
- `assets/animated_java/models/item/${projectNamespace}/`
- )
- rigItemModelExportPath = PathModule.join(
- resourcePackFolder,
- `assets/animated_java/models/item/${projectNamespace}/${rigItemId.split(':')[1]}.json`
- )
- console.log('Using automatic resource pack settings')
- }
-
- const rig = renderRig(rigExportFolder, textureExportFolder)
- const renderedAnimations = await renderAllAnimations(rig)
-
- await exportResources(
- ajSettings,
- projectSettings,
- rig,
- rigExportFolder,
- textureExportFolder,
- rigItemModelExportPath
- )
- // Resources MUST be exported before the exporter is ran
- await exporter.export({
- ajSettings,
- projectSettings: projectSettings as any,
- exporterSettings,
- renderedAnimations,
- rig,
- })
-
- Blockbench.showQuickMessage(translate('animated_java.quickmessage.exported_successfully'), 2000)
- // Post-export
- Project.animated_java_variants?.select(selectedVariant)
-})
-
-function verifySettings(structure: GUIStructure, settings: Array>) {
- const issues: IInfoPopup[] = []
- for (const el of structure) {
- switch (el.type) {
- case 'group':
- issues.push(...verifySettings(el.children, settings))
- break
- case 'toggle': {
- const setting = settings.find(s => s.id === el.settingId)
- if (!setting) throw new Error(`No setting found with id "${el.settingId}"`)
- if (setting.value) issues.push(...verifySettings(el.active, settings))
- else issues.push(...verifySettings(el.inactive, settings))
- break
- }
- case 'setting': {
- const setting = settings.find(s => s.id === el.settingId)
- if (!setting) throw new Error(`No setting found with id "${el.settingId}"`)
- const info = setting.verify()
- if (info?.type !== 'error') continue
- issues.push({
- type: 'error',
- title: translate('animated_java.popup.failed_project_export_readiness.issue', [
- setting.displayName,
- ]),
- lines: [info.title, ...info.lines],
- })
- break
- }
- }
- }
- return issues
-}
-
-export function verifyProjectExportReadiness() {
- const issues: IInfoPopup[] = []
-
- if (!Project) {
- // FIXME - Needs translation
- issues.push({
- type: 'error',
- title: 'No Project Found',
- lines: [
- 'No project was loaded when the export button was pressed.',
- 'Please load a project and try again.',
- ],
- })
- return
- }
-
- if (!Project.animated_java_settings) {
- // FIXME - Needs translation
- issues.push({
- type: 'error',
- title: 'No Animated Java Settings Found',
- lines: ['No Animated Java settings found for this project'],
- })
- return
- }
-
- // Verify Project Settings
- issues.push(
- ...verifySettings(
- projectSettingStructure,
- Object.values(Project.animated_java_settings) as unknown as Array>
- )
- )
-
- // Verify Exporter Settings
- const exporter =
- AnimatedJavaExporter.exporters[
- Project.animated_java_settings.exporter.selected!.value as NamespacedString
- ]
- if (!exporter)
- // FIXME - Needs translation
- issues.push({
- type: 'error',
- title: 'No Exporter Selected',
- lines: ['No exporter was selected for this project'],
- })
- else
- issues.push(
- ...verifySettings(
- exporter.settingsStructure,
- Object.values(
- Project.animated_java_exporter_settings![exporter.id]
- ) as unknown as Array>
- )
- )
-
- // Verify variant keyframes
- for (const animation of Project.animations) {
- const effects = animation.animators.effects
- if (!effects) continue
- for (const keyframe of effects.keyframes) {
- if (!(keyframe.channel === 'variants')) continue
- for (const dataPoint of keyframe.data_points) {
- if (!dataPoint.variant) continue
- const variant = Project.animated_java_variants?.variants.find(
- v => v.uuid === dataPoint.variant
- )
- if (!variant) {
- // FIXME - Needs translation
- issues.push({
- type: 'error',
- title: 'Variant Not Found',
- lines: [
- `Variant UUID "${
- dataPoint.variant as string
- }" referenced in animation "${animation.name}" in a keyframe at ${
- keyframe.time
- } seconds, but no variant with that UUID was found.`,
- ],
- })
- }
- }
- }
- }
-
- // Verify Outliner
- for (const node of Outliner.root as any[]) {
- if (
- node instanceof Group ||
- (OutlinerElement.types.camera && node instanceof OutlinerElement.types.camera) ||
- node instanceof Locator
- )
- continue
- // FIXME - Needs translation
- issues.push({
- type: 'error',
- title: 'Invalid Outliner',
- lines: [
- `The root of the Outliner can only contain bones.`,
- `Please remove the ${
- Object.getPrototypeOf(node).constructor.name as string
- } named "${node?.name as string}" or move it into a bone.`,
- ],
- })
- }
-
- // Verify and clean textures
- for (const texture of Project.textures) {
- texture.name = texture.name.replace(/\.png$/, '')
- }
-
- if (issues.find(v => v.type === 'error')) {
- openAjFailedProjectExportReadinessDialog(issues)
- throw new ExpectedError('Project is not ready for export')
- }
-}
diff --git a/src/global.d.ts b/src/global.d.ts
index 5b6ea74d..3e72c14f 100644
--- a/src/global.d.ts
+++ b/src/global.d.ts
@@ -1,89 +1,44 @@
-import type { AnimatedJavaExporter } from './exporter'
-import type { generateSearchTree, JsonText } from './minecraft'
-import type { IAnimatedJavaProjectSettings } from './projectSettings'
-import type { createInfo, Setting } from './settings'
-import type { addTranslations, translate } from './util/translation'
-import type { formatStr, roundTo, roundToN } from './util/misc'
-import type { ProgressBarController } from './util/progress'
-import type { VariantsContainer } from './variants'
-import { Writable } from 'svelte/store'
+///
-declare global {
- type NotUndefined = T extends undefined ? never : T
-
- interface IDocsManifest {
- structure: Record
- pages: IDocsManifestPage[]
- }
- interface IDocsManifestPage {
- title: string
- url: string
- children?: string[]
- content: string
- }
-
- const AnimatedJava: {
- loaded?: boolean
- docClick: (link: string) => void
- events: typeof import('./events')
- progress: Writable
- progress_text: Writable
-
- API: {
- Exporter: typeof AnimatedJavaExporter
- Settings: typeof import('./settings')
- translate: typeof translate
- addTranslations: typeof addTranslations
- formatStr: typeof formatStr
- roundTo: typeof roundTo
- roundToN: typeof roundToN
- VirtualFileSystem: typeof import('./util/virtualFileSystem')
- deepslate: typeof import('deepslate')
- ProgressBarController: typeof ProgressBarController
- createInfo: typeof createInfo
- JsonText: typeof JsonText
- generateSearchTree: typeof generateSearchTree
- minecraft: typeof import('./minecraft')
- }
- }
+declare module '*.png' {
+ const value: string
+ export = value
+}
- //-------------------------------
- // Blockbench Type modifications
- //-------------------------------
+declare module '*.gif' {
+ const value: string
+ export = value
+}
- // eslint-disable-next-line @typescript-eslint/naming-convention
- interface _Animation {
- affected_bones: Array<{ name: string; value: string }>
- affected_bones_is_a_whitelist: boolean
- }
+declare module '*.svg' {
+ const value: string
+ export = value
+}
- interface AnimationOptions {
- affected_bones?: Array<{ name: string; value: string }>
- affected_bones_is_a_whitelist?: boolean
- }
+declare module '*.webp' {
+ const value: string
+ export = value
+}
- interface AnimationUndoCopy {
- affected_bones: Array<{ name: string; value: string }>
- affected_bones_is_a_whitelist: boolean
- }
+declare module '*.mcb' {
+ const value: string
+ export = value
+}
- interface Cube {
- forceVisible?: boolean
- }
+declare module '*.txt' {
+ const value: string
+ export = value
+}
- interface Group {
- nbt?: string
- }
+declare module '*.worker.ts' {
+ export = Worker
+}
- interface Locator {
- entity_type: string
- nbt: string
- }
+declare module '*.molang' {
+ const value: Record
+ export = value
+}
- interface ModelProject {
- animated_java_settings?: IAnimatedJavaProjectSettings
- animated_java_exporter_settings?: Record>>
- animated_java_variants?: VariantsContainer
- animated_java_uuid?: string
- }
+declare module 'fflate/browser' {
+ export * from 'fflate'
}
diff --git a/src/globalWorker.d.ts b/src/globalWorker.d.ts
deleted file mode 100644
index 27116e53..00000000
--- a/src/globalWorker.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-//GENERATED FILE
diff --git a/src/guiStructure.ts b/src/guiStructure.ts
deleted file mode 100644
index a659eaa6..00000000
--- a/src/guiStructure.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { SettingID } from './settings'
-
-interface IGUIElement {
- type: string
-}
-
-interface IGUIElements {
- group: IGUIElement & {
- type: 'group'
- openByDefault: boolean
- title: string
- children: GUIStructure
- }
- setting: IGUIElement & {
- type: 'setting'
- settingId: SettingID
- }
- toggle: {
- type: 'toggle'
- settingId: SettingID
- title?: string
- activeTitle?: string
- inactiveTitle?: string
- active: GUIStructure
- inactive: GUIStructure
- }
-}
-
-export type AnyGUIElement = IGUIElements[keyof IGUIElements]
-
-export type GUIStructure = AnyGUIElement[]
diff --git a/src/index.ts b/src/index.ts
index 1ee37ed1..5808073f 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,119 +1,136 @@
-// These imports are in a specific order. Try not to change them around too much!
-// FIXME - Deepslate should only be importing the NBT library.
-import * as deepslate from 'deepslate'
-import PACKAGE from '../package.json'
-import './exporter'
-import { AnimatedJavaExporter } from './exporter'
-import { generateSearchTree, JsonText } from './minecraft'
-import './modelFormat'
-import { createChaos } from './mods/cubeMod'
-import './mods/cubeMod'
-import './mods/cubeFaceMod'
-import './mods/animationMod'
-import './mods/keyframeMod'
-import './mods/modeMod'
-import './mods/textureMod'
-import './mods/groupMod'
-import './mods/locatorMod'
-import './mods/outlinerNodeMod'
-import './mods/cameraPluginMod'
-import './mods/boneAnimatorMod'
-import './mods/modelFormatMod'
-import './projectSettings'
-import * as AJSettings from './settings'
-import { createInfo } from './settings'
-import './ui/ajAnimationProperties'
-import { openAJDocsDialog } from './ui/ajDocs'
-import './ui/ajKeyframe'
-import './ui/ajMenuBar'
-import './ui/ajProjectSettings'
-import './ui/ajSettings'
-import './ui/ajVariantsPanel'
-import './ui/popups/invalidCubes'
-import './ui/ajStartScreen'
-import { consoleGroupCollapsed } from './util/console'
-import * as events from './events'
-import {
- transposeMatrix,
- formatStr,
- roundTo,
- roundToN,
- ExpectedError,
- LimitClock,
-} from './util/misc'
-import './util/moddingTools'
-import { ProgressBarController } from './util/progress'
+import { PACKAGE } from './constants'
+import { events } from './util/events'
import './util/translation'
-import { addTranslations, translate } from './util/translation'
-import * as VirtualFileSystem from './util/virtualFileSystem'
-import { openUnexpectedErrorDialog } from './ui/popups/unexpectedError'
-import * as minecraft from './minecraft'
-import { openAJExportInProgressDialog } from './ui/ajExportInProgress'
-import { AJMetaFile } from './ajmeta'
-import { writable } from 'svelte/store'
-Prism.languages.mcfunction = {}
+// Blueprint Format
+import './blueprintFormat'
+// Interface
+import './interface/animatedJavaBarItem'
+import './interface/boneConfigDialog'
+import './interface/variantsPanel'
+import './interface/importAJModelLoader'
+import './interface/customKeyframePanel'
+import './interface/textDisplayElementPanel'
+import './interface/textDisplayConfigDialog'
+import './interface/vanillaItemDisplayConfigDialog'
+import './interface/vanillaItemDisplayElementPanel'
+import './interface/vanillaBlockDisplayConfigDialog'
+import './interface/vanillaBlockDisplayElementPanel'
+// Blockbench Mods
+import './interface/locatorConfigDialog'
+import './mods/animationControllerMod'
+import './mods/animationPropertiesAction'
+import './mods/animationPropertiesMod'
+import './mods/bonePropertiesMod'
+import './mods/cubeOutlineMod'
+import './mods/customKeyframesMod'
+import './mods/elementToolbarsMod'
+import './mods/exportOverActionMod'
+import './mods/groupContextMenuMod'
+import './mods/groupNameMod'
+import './mods/keyframeMod'
+import './mods/locatorAnimatorMod'
+import './mods/locatorContextMenuMod'
+import './mods/locatorPropertiesMod'
+import './mods/modelFormatConvertToMod'
+import './mods/modelFormatMod'
+import './mods/molangMod'
+import './mods/panelMod'
+import './mods/previewRaycastMod'
+import './mods/projectSettingsActionOverride'
+import './mods/saveAllAnimationsActionMod'
+import './mods/saveProjectActionMod'
+import './mods/saveProjectAsActionMod'
+import './mods/variantPreviewCubeFaceMod'
+import './mods/showDefaultPoseMod'
+// Outliner
+import './outliner/textDisplay'
+import './outliner/vanillaItemDisplay'
+import './outliner/vanillaBlockDisplay'
+// Compilers
+import { compileDataPack } from './systems/datapackCompiler'
+// Minecraft Systems
+import './systems/minecraft/versionManager'
+import './systems/minecraft/registryManager'
+import './systems/minecraft/blockstateManager'
+import './systems/minecraft/assetManager'
+import './systems/minecraft/fontManager'
+// Misc imports
+import { TRANSPARENT_TEXTURE, Variant } from './variants'
+import './systems/minecraft/registryManager'
+import { MINECRAFT_REGISTRY } from './systems/minecraft/registryManager'
+import { compileResourcePack } from './systems/resourcepackCompiler'
+import { openExportProgressDialog } from './interface/exportProgressDialog'
+import { isDataPackPath, isResourcePackPath } from './util/minecraftUtil'
+import { blueprintSettingErrors } from './blueprintSettings'
+import { openUnexpectedErrorDialog } from './interface/unexpectedErrorDialog'
+import { BLUEPRINT_CODEC, BLUEPRINT_FORMAT } from './blueprintFormat'
+import { TextDisplay } from './outliner/textDisplay'
+import { getLatestVersionClientDownloadUrl } from './systems/minecraft/assetManager'
+import { hideLoadingPopup, showLoadingPopup } from './interface/animatedJavaLoadingPopup'
+import { getVanillaFont } from './systems/minecraft/fontManager'
+import * as assetManager from './systems/minecraft/assetManager'
+import * as itemModelManager from './systems/minecraft/itemModelManager'
+import * as blockModelManager from './systems/minecraft/blockModelManager'
+import { VanillaItemDisplay } from './outliner/vanillaItemDisplay'
+import { VanillaBlockDisplay, debugBlockState, debugBlocks } from './outliner/vanillaBlockDisplay'
+import { BLOCKSTATE_REGISTRY } from './systems/minecraft/blockstateManager'
-// import './outliner/textDisplay'
+// Show loading popup
+void showLoadingPopup().then(async () => {
+ await Promise.all([
+ new Promise(resolve => events.MINECRAFT_ASSETS_LOADED.subscribe(() => resolve())),
+ new Promise(resolve => events.MINECRAFT_REGISTRY_LOADED.subscribe(() => resolve())),
+ new Promise(resolve => events.MINECRAFT_FONTS_LOADED.subscribe(() => resolve())),
+ new Promise(resolve => events.BLOCKSTATE_REGISTRY_LOADED.subscribe(() => resolve())),
+ ])
+ .then(() => {
+ hideLoadingPopup()
+ })
+ .catch(error => {
+ console.error(error)
+ Blockbench.showToastNotification({
+ text: 'Animated Java failed to load! Please restart Blockbench',
+ color: 'var(--color-error)',
+ })
+ })
+})
// @ts-ignore
globalThis.AnimatedJava = {
- // settings: AJSettings.animatedJavaSettings,
- createChaos,
- docClick(link: string) {
- if (link.startsWith('/docs/')) {
- link = link.substring(5)
- let section: string | undefined
- if (link.includes('#')) [link, section] = link.split('#')
- openAJDocsDialog(link, section)
- return
- } else if (link.startsWith('tag:')) {
- console.log(`Tag links aren't implemented: '${link}'`)
- }
- Blockbench.openLink(link)
- },
- // Expose this plugin's events to other plugins
- events,
- openUnexpectedErrorDialog,
- openAJExportInProgressDialog,
- progress: writable(),
- progress_text: writable(),
-
API: {
- addTranslations,
- AJMetaFile,
- columnToRowMajor: transposeMatrix,
- createInfo,
- deepslate,
- ExpectedError,
- Exporter: AnimatedJavaExporter,
- formatStr,
- generateSearchTree,
- JsonText,
- LimitClock,
- minecraft,
- ProgressBarController,
- roundTo,
- roundToN,
- Settings: AJSettings,
- translate,
- VirtualFileSystem,
+ compileDataPack,
+ compileResourcePack,
+ Variant,
+ MINECRAFT_REGISTRY,
+ openExportProgressDialog,
+ isResourcePackPath,
+ isDataPackPath,
+ blueprintSettingErrors,
+ openUnexpectedErrorDialog,
+ TRANSPARENT_TEXTURE,
+ BLUEPRINT_FORMAT,
+ BLUEPRINT_CODEC,
+ TextDisplay,
+ getLatestVersionClientDownloadUrl,
+ getVanillaFont,
+ assetManager,
+ itemModelManager,
+ blockModelManager,
+ VanillaItemDisplay,
+ VanillaBlockDisplay,
+ debugBlocks,
+ debugBlockState,
+ BLOCKSTATE_REGISTRY,
},
}
+
// Uninstall events
events.EXTRACT_MODS.subscribe(() => {
// @ts-ignore
globalThis.AnimatedJava = undefined
})
-events.SELECT_PROJECT.subscribe(() => {
- if (localStorage.getItem('aj:showWelcome') === 'false') return
- requestAnimationFrame(() => {
- AnimatedJava.docClick('page:index')
- })
- localStorage.setItem('aj:showWelcome', 'false')
-}, true)
-
BBPlugin.register(PACKAGE.name, {
title: PACKAGE.title,
author: PACKAGE.author.name,
@@ -124,20 +141,16 @@ BBPlugin.register(PACKAGE.name, {
min_version: PACKAGE.min_blockbench_version,
tags: ['Minecraft: Java Edition', 'Animation', 'Display Entities'],
await_loading: true,
- onload: consoleGroupCollapsed(`${PACKAGE.name}:onload`, () => {
+ onload() {
events.LOAD.dispatch()
- AnimatedJava.loaded = true
- }),
- onunload: consoleGroupCollapsed(`${PACKAGE.name}:onunload`, () => {
+ },
+ onunload() {
events.UNLOAD.dispatch()
- }),
- oninstall: consoleGroupCollapsed(`${PACKAGE.name}:oninstall`, () => {
+ },
+ oninstall() {
events.INSTALL.dispatch()
- }),
- onuninstall: consoleGroupCollapsed(`${PACKAGE.name}:onuninstall`, () => {
+ },
+ onuninstall() {
events.UNINSTALL.dispatch()
- }),
+ },
})
-
-import('../exporters/datapackExporter')
-import('../exporters/jsonExporter')
diff --git a/src/interface/aboutDialog.ts b/src/interface/aboutDialog.ts
new file mode 100644
index 00000000..73a376c4
--- /dev/null
+++ b/src/interface/aboutDialog.ts
@@ -0,0 +1,16 @@
+import { PACKAGE } from '../constants'
+import AboutSvelte from '../components/about.svelte'
+import { SvelteDialog } from '../util/svelteDialog'
+import { translate } from '../util/translation'
+
+export function openAboutDialog() {
+ new SvelteDialog({
+ id: `${PACKAGE.name}:aboutDialog`,
+ title: translate('dialog.about.title'),
+ width: 700,
+ svelteComponent: AboutSvelte,
+ svelteComponentProperties: {},
+ buttons: [translate('dialog.about.close_button')],
+ preventKeybinds: true,
+ }).show()
+}
diff --git a/src/interface/animatedJavaBarItem.ts b/src/interface/animatedJavaBarItem.ts
new file mode 100644
index 00000000..dcacee16
--- /dev/null
+++ b/src/interface/animatedJavaBarItem.ts
@@ -0,0 +1,99 @@
+import AnimatedJavaIcon from '../assets/animated_java_icon.svg'
+import { BLUEPRINT_FORMAT } from '../blueprintFormat'
+import { PACKAGE } from '../constants'
+import { exportProject } from '../systems/exporter'
+import { events } from '../util/events'
+import { createAction, createBarMenu } from '../util/moddingTools'
+import { translate } from '../util/translation'
+import { openAboutDialog } from './aboutDialog'
+import { openBlueprintSettingsDialog } from './blueprintSettingsDialog'
+
+function createIconImg() {
+ const IMG = document.createElement('img')
+ Object.assign(IMG, {
+ src: AnimatedJavaIcon,
+ width: 16,
+ height: 16,
+ })
+ Object.assign(IMG.style, {
+ position: 'relative',
+ top: '2px',
+ borderRadius: '8px',
+ marginRight: '6px',
+ })
+ return IMG
+}
+const BLOCKBENCH_MENU_BAR = document.querySelector('#menu_bar') as HTMLDivElement
+export const MENU = createBarMenu(
+ `${PACKAGE.name}:menu`,
+ [],
+ () => Format === BLUEPRINT_FORMAT
+) as BarMenu & { label: HTMLDivElement }
+MENU.label.style.display = 'none'
+MENU.label.innerHTML = 'Animated Java'
+MENU.label.prepend(createIconImg())
+BLOCKBENCH_MENU_BAR.appendChild(MENU.label)
+
+events.SELECT_PROJECT.subscribe(project => {
+ MENU.label.style.display = project.format === BLUEPRINT_FORMAT ? 'inline-block' : 'none'
+})
+
+MenuBar.addAction(
+ createAction(`${PACKAGE.name}:about`, {
+ icon: 'info',
+ category: 'animated_java',
+ name: translate('action.open_about.name'),
+ condition() {
+ return Format === BLUEPRINT_FORMAT
+ },
+ click() {
+ openAboutDialog()
+ },
+ }),
+ MENU.id
+)
+
+MenuBar.addAction(
+ createAction(`${PACKAGE.name}:blueprint_settings`, {
+ icon: 'settings',
+ category: 'animated_java',
+ name: translate('action.open_blueprint_settings.name'),
+ condition() {
+ return Format === BLUEPRINT_FORMAT
+ },
+ click() {
+ openBlueprintSettingsDialog()
+ },
+ }),
+ MENU.id
+)
+
+MenuBar.addAction(
+ createAction(`${PACKAGE.name}:documentation`, {
+ icon: 'find_in_page',
+ category: 'animated_java',
+ name: translate('action.open_documentation.name'),
+ condition() {
+ return Format === BLUEPRINT_FORMAT
+ },
+ click() {
+ Blockbench.openLink('https://animated-java.github.io/')
+ },
+ }),
+ MENU.id
+)
+
+MenuBar.addAction(
+ createAction(`${PACKAGE.name}:export`, {
+ icon: 'insert_drive_file',
+ category: 'animated_java',
+ name: translate('action.export.name'),
+ condition() {
+ return Format === BLUEPRINT_FORMAT && !Project?.animated_java.enable_plugin_mode
+ },
+ click() {
+ void exportProject()
+ },
+ }),
+ MENU.id
+)
diff --git a/src/interface/animatedJavaLoadingPopup.ts b/src/interface/animatedJavaLoadingPopup.ts
new file mode 100644
index 00000000..b02bd5ce
--- /dev/null
+++ b/src/interface/animatedJavaLoadingPopup.ts
@@ -0,0 +1,30 @@
+import { SvelteComponent } from 'svelte'
+import AnimatedJavaLoadingPopup from '../components/animatedJavaLoadingPopup.svelte'
+import { injectSvelteCompomponent } from '../util/injectSvelte'
+import { Valuable } from '../util/stores'
+
+const LOADED = new Valuable(false)
+let activeComponent: SvelteComponent | undefined
+
+export async function showLoadingPopup() {
+ if (activeComponent) return
+ activeComponent = await injectSvelteCompomponent({
+ svelteComponent: AnimatedJavaLoadingPopup,
+ svelteComponentProperties: {
+ loaded: LOADED,
+ },
+ elementSelector() {
+ return document.body
+ },
+ })
+}
+
+export function hideLoadingPopup() {
+ if (!activeComponent) return
+ LOADED.set(true)
+ setTimeout(() => {
+ if (!activeComponent) return
+ activeComponent.$destroy()
+ activeComponent = undefined
+ }, 2000)
+}
diff --git a/src/interface/animationPropertiesDialog.ts b/src/interface/animationPropertiesDialog.ts
new file mode 100644
index 00000000..4aa894b2
--- /dev/null
+++ b/src/interface/animationPropertiesDialog.ts
@@ -0,0 +1,40 @@
+import AniamtionPropertiesSvelteComponent from '../components/animationProperties.svelte'
+import { PACKAGE } from '../constants'
+import { Valuable } from '../util/stores'
+import { SvelteDialog } from '../util/svelteDialog'
+import { translate } from '../util/translation'
+
+export function openAnimationPropertiesDialog(animation: _Animation) {
+ const animationName = new Valuable(animation.name)
+ const loopMode = new Valuable(animation.loop)
+ const excludedBones = new Valuable(animation.excluded_bones)
+
+ new SvelteDialog({
+ id: `${PACKAGE.name}:animationPropertiesDialog`,
+ title: translate('dialog.animation_properties.title', animation.name),
+ width: 600,
+ svelteComponent: AniamtionPropertiesSvelteComponent,
+ svelteComponentProperties: {
+ animationName,
+ loopMode,
+ excludedBones,
+ },
+ preventKeybinds: true,
+ onConfirm() {
+ animation.name = animationName.get()
+ animation.createUniqueName(Blockbench.Animation.all)
+ animation.loop = loopMode.get()
+ animation.excluded_bones = excludedBones.get()
+ },
+ }).show()
+}
+
+// export const ANIMATION_PROPERTIES_ACTION = createAction(`${PACKAGE.name}:bone_config`, {
+// icon: 'settings',
+// name: translate('action.open_bone_config.name'),
+// condition: () => Format === BLUEPRINT_FORMAT,
+// click: () => {
+// if (!Blockbench.Animation.selected) return
+// openBoneConfigDialog(Blockbench.Animation.selected)
+// },
+// })
diff --git a/src/interface/blueprintSettingsDialog.ts b/src/interface/blueprintSettingsDialog.ts
new file mode 100644
index 00000000..4a4cb6d2
--- /dev/null
+++ b/src/interface/blueprintSettingsDialog.ts
@@ -0,0 +1,114 @@
+import { PACKAGE } from '../constants'
+import { SvelteDialog } from '../util/svelteDialog'
+import { Valuable } from '../util/stores'
+import BlueprintSettingsDialogSvelteComponent from '../components/blueprintSettingsDialog.svelte'
+import { toSafeFuntionName } from '../util/minecraftUtil'
+import { defaultValues } from '../blueprintSettings'
+import { translate } from '../util/translation'
+import { updateBoundingBox } from '../blueprintFormat'
+
+function getSettings() {
+ return {
+ blueprintName: new Valuable(Project!.name, value => {
+ if (!value) {
+ return 'My Blueprint'
+ }
+ return value
+ }),
+ textureSizeX: new Valuable(Project!.texture_width),
+ textureSizeY: new Valuable(Project!.texture_height),
+ showBoundingBox: new Valuable(Project!.animated_java.show_bounding_box),
+ autoBoundingBox: new Valuable(Project!.animated_java.auto_bounding_box),
+ boundingBoxX: new Valuable(Project!.animated_java.bounding_box[0]),
+ boundingBoxY: new Valuable(Project!.animated_java.bounding_box[1]),
+ // Export Settings
+ enablePluginMode: new Valuable(Project!.animated_java.enable_plugin_mode),
+ exportNamespace: new Valuable(Project!.animated_java.export_namespace, value => {
+ if (!value) {
+ return defaultValues.export_namespace
+ }
+ return toSafeFuntionName(value)
+ }),
+ enableResourcePack: new Valuable(Project!.animated_java.enable_resource_pack),
+ enableDataPack: new Valuable(Project!.animated_java.enable_data_pack),
+ // Resource Pack Settings
+ displayItem: new Valuable(Project!.animated_java.display_item, value => {
+ if (!value) {
+ return defaultValues.display_item
+ }
+ return value
+ }),
+ customModelDataOffset: new Valuable(Project!.animated_java.customModelDataOffset),
+ enableAdvancedResourcePackSettings: new Valuable(
+ Project!.animated_java.enable_advanced_resource_pack_settings
+ ),
+ resourcePack: new Valuable(Project!.animated_java.resource_pack),
+ displayItemPath: new Valuable(Project!.animated_java.display_item_path),
+ modelFolder: new Valuable(Project!.animated_java.model_folder),
+ textureFolder: new Valuable(Project!.animated_java.texture_folder),
+ // Data Pack Settings
+ enableAdvancedDataPackSettings: new Valuable(
+ Project!.animated_java.enable_advanced_data_pack_settings
+ ),
+ dataPack: new Valuable(Project!.animated_java.data_pack),
+ summonCommands: new Valuable(Project!.animated_java.summon_commands),
+ interpolationDuration: new Valuable(Project!.animated_java.interpolation_duration),
+ teleportationDuration: new Valuable(Project!.animated_java.teleportation_duration),
+ useStorageForAnimation: new Valuable(Project!.animated_java.use_storage_for_animation),
+ }
+}
+
+function setSettings(settings: ReturnType) {
+ if (!Project) return
+ Project.name = settings.blueprintName.get()
+
+ setProjectResolution(settings.textureSizeX.get(), settings.textureSizeY.get(), true)
+
+ Project.animated_java.show_bounding_box = settings.showBoundingBox.get()
+ Project.animated_java.auto_bounding_box = settings.autoBoundingBox.get()
+ Project.animated_java.bounding_box = [settings.boundingBoxX.get(), settings.boundingBoxY.get()]
+
+ // Export Settings
+ Project.animated_java.enable_plugin_mode = settings.enablePluginMode.get()
+ Project.pluginMode.set(settings.enablePluginMode.get()) // Required to update the project title.
+ Project.animated_java.export_namespace = settings.exportNamespace.get()
+ Project.animated_java.enable_resource_pack = settings.enableResourcePack.get()
+ Project.animated_java.enable_data_pack = settings.enableDataPack.get()
+ // Resource Pack Settings
+ Project.animated_java.display_item = settings.displayItem.get()
+ Project.animated_java.customModelDataOffset = settings.customModelDataOffset.get()
+ Project.animated_java.enable_advanced_resource_pack_settings =
+ settings.enableAdvancedResourcePackSettings.get()
+ Project.animated_java.resource_pack = settings.resourcePack.get()
+ Project.animated_java.display_item_path = settings.displayItemPath.get()
+ Project.animated_java.model_folder = settings.modelFolder.get()
+ Project.animated_java.texture_folder = settings.textureFolder.get()
+ // Data Pack Settings
+ Project.animated_java.enable_advanced_data_pack_settings =
+ settings.enableAdvancedDataPackSettings.get()
+ Project.animated_java.data_pack = settings.dataPack.get()
+ Project.animated_java.summon_commands = settings.summonCommands.get()
+ Project.animated_java.interpolation_duration = settings.interpolationDuration.get()
+ Project.animated_java.teleportation_duration = settings.teleportationDuration.get()
+ Project.animated_java.use_storage_for_animation = settings.useStorageForAnimation.get()
+ console.log('Successfully saved project settings', Project)
+}
+
+export function openBlueprintSettingsDialog() {
+ if (!Project) return
+
+ const settings = getSettings()
+
+ return new SvelteDialog({
+ id: `${PACKAGE.name}:blueprintSettingsDialog`,
+ title: translate('dialog.blueprint_settings.title'),
+ width: 512,
+ svelteComponent: BlueprintSettingsDialogSvelteComponent,
+ svelteComponentProperties: settings,
+ preventKeybinds: true,
+ onConfirm() {
+ setSettings(settings)
+ updateBoundingBox()
+ },
+ }).show()
+}
diff --git a/src/interface/boneConfigDialog.ts b/src/interface/boneConfigDialog.ts
new file mode 100644
index 00000000..1b86aa2a
--- /dev/null
+++ b/src/interface/boneConfigDialog.ts
@@ -0,0 +1,171 @@
+import { BLUEPRINT_FORMAT } from '../blueprintFormat'
+import { BoneConfig } from '../nodeConfigs'
+import BoneConfigDialogSvelteComponent from '../components/boneConfigDialog.svelte'
+import { PACKAGE } from '../constants'
+import { createAction } from '../util/moddingTools'
+import { Valuable } from '../util/stores'
+import { SvelteDialog } from '../util/svelteDialog'
+import { translate } from '../util/translation'
+import { Variant } from '../variants'
+
+// TODO: These should probably be part of the BoneConfig class
+function propagateInheritanceUp(group: Group, config: BoneConfig, variant?: string): void {
+ // Recurse to the topmost parent that doesn't have inherit_settings enabled, then inherit down from there
+ if (group.parent instanceof Group) {
+ const parentConfig = variant
+ ? group.parent.configs.variants[variant]
+ : group.parent.configs.default
+ if (parentConfig) {
+ const parentBoneConfig = BoneConfig.fromJSON(parentConfig)
+ if (parentConfig.inherit_settings) {
+ propagateInheritanceUp(group.parent, parentBoneConfig, variant)
+ }
+ console.log('Inheriting from', group.parent.name, parentConfig)
+ config.inheritFrom(parentBoneConfig)
+ if (variant) group.configs.variants[variant] = config.toJSON()
+ else group.configs.default = config.toJSON()
+ }
+ }
+}
+
+function propagateInheritanceDown(group: Group, config: BoneConfig, variant?: string) {
+ for (const child of group.children) {
+ if (!(child instanceof Group)) continue
+ const childConfig = variant ? child.configs.variants[variant] : child.configs.default
+ if (childConfig && childConfig.inherit_settings) {
+ const childBoneConfig = BoneConfig.fromJSON(childConfig)
+ console.log('Inheriting to', child.name, childBoneConfig)
+ childBoneConfig.inheritFrom(config)
+ if (variant) child.configs.variants[variant] = childBoneConfig.toJSON()
+ else child.configs.default = childBoneConfig.toJSON()
+ propagateInheritanceDown(child, childBoneConfig, variant)
+ }
+ }
+}
+
+export function openBoneConfigDialog(bone: Group) {
+ // Blockbench's JSON stringifier doesn't handle custom toJSON functions, so I'm storing the config JSON in the bone instead of the actual BoneConfig object
+ let boneConfigJSON = (bone.configs.default ??= new BoneConfig().toJSON())
+ let parentConfigJSON =
+ bone.parent instanceof Group
+ ? (bone.parent.configs.default ??= new BoneConfig().toJSON())
+ : undefined
+
+ if (Variant.selected && !Variant.selected.isDefault) {
+ // Get the variant's config, or create a new one if it doesn't exist
+ boneConfigJSON = bone.configs.variants[Variant.selected.uuid] ??= new BoneConfig().toJSON()
+ parentConfigJSON =
+ bone.parent instanceof Group
+ ? (bone.parent.configs.variants[Variant.selected.uuid] ??=
+ new BoneConfig().toJSON())
+ : undefined
+ }
+
+ const parentConfig = parentConfigJSON
+ ? BoneConfig.fromJSON(parentConfigJSON)
+ : BoneConfig.getDefault()
+
+ const oldConfig = BoneConfig.fromJSON(boneConfigJSON)
+
+ const billboard = new Valuable(oldConfig.billboard)
+ const overrideBrightness = new Valuable(oldConfig.overrideBrightness)
+ const brightnessOverride = new Valuable(oldConfig.brightnessOverride)
+ const enchanted = new Valuable(oldConfig.enchanted)
+ const glowing = new Valuable(oldConfig.glowing)
+ const overrideGlowColor = new Valuable(oldConfig.overrideGlowColor)
+ const glowColor = new Valuable(oldConfig.glowColor)
+ const inheritSettings = new Valuable(oldConfig.inheritSettings)
+ const invisible = new Valuable(oldConfig.invisible)
+ const nbt = new Valuable(oldConfig.nbt)
+ const shadowRadius = new Valuable(oldConfig.shadowRadius)
+ const shadowStrength = new Valuable(oldConfig.shadowStrength)
+ const useNBT = new Valuable(oldConfig.useNBT)
+
+ new SvelteDialog({
+ id: `${PACKAGE.name}:boneConfig`,
+ title: translate('dialog.bone_config.title'),
+ width: 400,
+ svelteComponent: BoneConfigDialogSvelteComponent,
+ svelteComponentProperties: {
+ variant: Variant.selected,
+ billboard,
+ overrideBrightness,
+ brightnessOverride,
+ enchanted,
+ glowing,
+ overrideGlowColor,
+ glowColor,
+ inheritSettings,
+ invisible,
+ nbt,
+ shadowRadius,
+ shadowStrength,
+ useNBT,
+ },
+ preventKeybinds: true,
+ onConfirm() {
+ const newConfig = new BoneConfig()
+
+ newConfig.billboard = billboard.get()
+ newConfig.overrideBrightness = overrideBrightness.get()
+ newConfig.brightnessOverride = brightnessOverride.get()
+ newConfig.enchanted = enchanted.get()
+ newConfig.glowing = glowing.get()
+ newConfig.overrideGlowColor = overrideGlowColor.get()
+ newConfig.glowColor = glowColor.get()
+ newConfig.inheritSettings = inheritSettings.get()
+ newConfig.invisible = invisible.get()
+ newConfig.nbt = nbt.get()
+ newConfig.shadowRadius = shadowRadius.get()
+ newConfig.shadowStrength = shadowStrength.get()
+ newConfig.useNBT = useNBT.get()
+
+ newConfig.billboard === parentConfig.billboard && (newConfig.billboard = undefined)
+ newConfig.overrideBrightness === parentConfig.overrideBrightness &&
+ (newConfig.overrideBrightness = undefined)
+ newConfig.brightnessOverride === parentConfig.brightnessOverride &&
+ (newConfig.brightnessOverride = undefined)
+ newConfig.enchanted === parentConfig.enchanted && (newConfig.enchanted = undefined)
+ newConfig.glowing === parentConfig.glowing && (newConfig.glowing = undefined)
+ newConfig.overrideGlowColor === parentConfig.overrideGlowColor &&
+ (newConfig.overrideGlowColor = undefined)
+ newConfig.glowColor === parentConfig.glowColor && (newConfig.glowColor = undefined)
+ newConfig.invisible === parentConfig.invisible && (newConfig.invisible = undefined)
+ newConfig.nbt === parentConfig.nbt && (newConfig.nbt = undefined)
+ newConfig.shadowRadius === parentConfig.shadowRadius &&
+ (newConfig.shadowRadius = undefined)
+ newConfig.shadowStrength === parentConfig.shadowStrength &&
+ (newConfig.shadowStrength = undefined)
+ newConfig.useNBT === parentConfig.useNBT && (newConfig.useNBT = undefined)
+
+ if (newConfig.checkIfEqual(BoneConfig.fromJSON(bone.configs.default))) {
+ // Don't save the variant config if it's the same as the default
+ delete bone.configs.variants[Variant.selected!.uuid]
+ return
+ }
+
+ if (Variant.selected && !Variant.selected.isDefault) {
+ if (newConfig.inheritSettings) {
+ propagateInheritanceUp(bone, newConfig, Variant.selected.uuid)
+ }
+ bone.configs.variants[Variant.selected.uuid] = newConfig.toJSON()
+ propagateInheritanceDown(bone, newConfig, Variant.selected.uuid)
+ } else {
+ if (newConfig.inheritSettings) {
+ propagateInheritanceUp(bone, newConfig)
+ }
+ bone.configs.default = newConfig.toJSON()
+ propagateInheritanceDown(bone, newConfig)
+ }
+ },
+ }).show()
+}
+
+export const BONE_CONFIG_ACTION = createAction(`${PACKAGE.name}:bone_config`, {
+ icon: 'settings',
+ name: translate('action.open_bone_config.name'),
+ condition: () => Format === BLUEPRINT_FORMAT,
+ click: () => {
+ openBoneConfigDialog(Group.selected)
+ },
+})
diff --git a/src/interface/customKeyframePanel.ts b/src/interface/customKeyframePanel.ts
new file mode 100644
index 00000000..b6033a47
--- /dev/null
+++ b/src/interface/customKeyframePanel.ts
@@ -0,0 +1,45 @@
+import CustomKeyframePanelSvelteComponent from '../components/customKeyframePanel.svelte'
+import { CUSTOM_CHANNELS } from '../mods/customKeyframesMod'
+import { events } from '../util/events'
+import { injectSvelteCompomponent } from '../util/injectSvelte'
+import { Valuable } from '../util/stores'
+import { translate } from '../util/translation'
+
+const CURRENT_PANEL = new Valuable(undefined)
+
+export function injectCustomKeyframePanel(selectedKeyframe: _Keyframe) {
+ if (!CUSTOM_CHANNELS.includes(selectedKeyframe.channel)) return
+
+ const element = document.querySelector(
+ '#panel_keyframe .panel_vue_wrapper .keyframe_data_point'
+ )
+ if (!element) {
+ console.warn(
+ 'Failed to find keyframe panel element. Aborting custom keyframe panel injection.'
+ )
+ return
+ }
+ for (const child of [...element.children]) {
+ child.remove()
+ }
+
+ injectSvelteCompomponent({
+ svelteComponent: CustomKeyframePanelSvelteComponent,
+ svelteComponentProperties: { currentPanel: CURRENT_PANEL, selectedKeyframe },
+ elementSelector() {
+ return element
+ },
+ postMount() {
+ const label = jQuery('#panel_keyframe .panel_vue_wrapper #keyframe_type_label label')
+ if (label && selectedKeyframe.channel) {
+ const property = selectedKeyframe.animator.channels[selectedKeyframe.channel]
+ label.text(translate('panel.keyframe.keyframe_title', `${property.name}`))
+ }
+ },
+ })
+}
+
+events.SELECT_KEYFRAME.subscribe(kf => {
+ CURRENT_PANEL.get()?.remove()
+ requestAnimationFrame(() => injectCustomKeyframePanel(kf))
+})
diff --git a/src/interface/exportProgressDialog.ts b/src/interface/exportProgressDialog.ts
new file mode 100644
index 00000000..10e6b915
--- /dev/null
+++ b/src/interface/exportProgressDialog.ts
@@ -0,0 +1,45 @@
+import ExportProgressDialogSvelteComponent from '../components/exportProgressDialog.svelte'
+import { PACKAGE } from '../constants'
+import { Valuable } from '../util/stores'
+import { SvelteDialog } from '../util/svelteDialog'
+import { translate } from '../util/translation'
+
+export const PROGRESS = new Valuable(0)
+export const MAX_PROGRESS = new Valuable(1)
+export const PROGRESS_DESCRIPTION = new Valuable('')
+
+export function openExportProgressDialog(debug?: boolean) {
+ const originalLog = console.log
+ PROGRESS.set(0)
+ MAX_PROGRESS.set(1)
+ PROGRESS_DESCRIPTION.set('Preparing...')
+
+ if (debug) {
+ MAX_PROGRESS.set(1000)
+ setInterval(() => {
+ // logMessage('Debug:', Math.random())
+ PROGRESS.set(PROGRESS.get() + 1)
+ if (PROGRESS.get() >= MAX_PROGRESS.get()) {
+ PROGRESS.set(0)
+ }
+ }, 10)
+ }
+
+ const dialog = new SvelteDialog({
+ id: `${PACKAGE.name}:exportProgressDialog`,
+ title: translate('dialog.export_progress.title'),
+ width: 512,
+ svelteComponent: ExportProgressDialogSvelteComponent,
+ svelteComponentProperties: {
+ progress: PROGRESS,
+ maxProgress: MAX_PROGRESS,
+ progressDescription: PROGRESS_DESCRIPTION,
+ },
+ preventKeybinds: true,
+ buttons: [],
+ onClose: () => {
+ console.log = originalLog
+ },
+ }).show()
+ return dialog
+}
diff --git a/src/interface/importAJModelLoader.ts b/src/interface/importAJModelLoader.ts
new file mode 100644
index 00000000..e82f0424
--- /dev/null
+++ b/src/interface/importAJModelLoader.ts
@@ -0,0 +1,60 @@
+import { SvelteComponentDev } from 'svelte/internal'
+import ImportAjModelLoaderDialog from '../components/importAJModelLoaderDialog.svelte'
+import { PACKAGE } from '../constants'
+import { injectSvelteCompomponent } from '../util/injectSvelte'
+import { createModelLoader } from '../util/moddingTools'
+import { translate } from '../util/translation'
+import { openUnexpectedErrorDialog } from './unexpectedErrorDialog'
+import * as ModelDatFixerUpper from '../systems/modelDataFixerUpper'
+import { BLUEPRINT_CODEC } from '../blueprintFormat'
+
+let activeComponent: SvelteComponentDev | null = null
+
+createModelLoader(`${PACKAGE.name}-upgradeAJModelLoader`, {
+ icon: 'folder_open',
+ category: 'animated_java',
+ name: translate('action.upgrade_old_aj_model_loader.name'),
+ condition() {
+ return true
+ },
+ format_page: {
+ component: {
+ template: `
`,
+ },
+ },
+ onFormatPage() {
+ if (activeComponent) {
+ activeComponent.$destroy()
+ }
+ void injectSvelteCompomponent({
+ svelteComponent: ImportAjModelLoaderDialog,
+ svelteComponentProperties: {},
+ elementSelector() {
+ return document.querySelector(`#${PACKAGE.name}-upgradeAJModelLoader-target`)
+ },
+ postMount(el) {
+ activeComponent = el
+ },
+ injectIndex: 2,
+ })
+ },
+})
+
+export function convertAJModelToBlueprint(path: string) {
+ try {
+ console.log(`Convert Old .ajmodel: ${path}`)
+ const blueprint = ModelDatFixerUpper.process(JSON.parse(fs.readFileSync(path, 'utf8')))
+
+ BLUEPRINT_CODEC.load(blueprint, {
+ name: 'Upgrade .ajmodel to Blueprint',
+ path: undefined,
+ })
+
+ requestAnimationFrame(() => {
+ Project!.openSettings()
+ })
+ } catch (e) {
+ console.error(e)
+ openUnexpectedErrorDialog(e as Error)
+ }
+}
diff --git a/src/interface/locatorConfigDialog.ts b/src/interface/locatorConfigDialog.ts
new file mode 100644
index 00000000..16d86985
--- /dev/null
+++ b/src/interface/locatorConfigDialog.ts
@@ -0,0 +1,51 @@
+import { BLUEPRINT_FORMAT } from '../blueprintFormat'
+import { LocatorConfig } from '../nodeConfigs'
+import LocatorConfigDialog from '../components/locatorConfigDialog.svelte'
+import { PACKAGE } from '../constants'
+import { createAction } from '../util/moddingTools'
+import { Valuable } from '../util/stores'
+import { SvelteDialog } from '../util/svelteDialog'
+import { translate } from '../util/translation'
+
+export function openLocatorConfigDialog(locator: Locator) {
+ // Blockbench's JSON stringifier doesn't handle custom toJSON functions, so I'm storing the config JSON in the locator instead of the actual LocatorConfig object
+ const locatorConfig = LocatorConfig.fromJSON((locator.config ??= new LocatorConfig().toJSON()))
+
+ const useEntity = new Valuable(locatorConfig.useEntity)
+ const entityType = new Valuable(locatorConfig.entityType)
+ const summonCommands = new Valuable(locatorConfig.summonCommands)
+ const tickingCommands = new Valuable(locatorConfig.tickingCommands)
+
+ new SvelteDialog({
+ id: `${PACKAGE.name}:locatorConfig`,
+ title: translate('dialog.locator_config.title'),
+ width: 600,
+ svelteComponent: LocatorConfigDialog,
+ svelteComponentProperties: {
+ useEntity,
+ entityType,
+ summonCommands,
+ tickingCommands,
+ },
+ preventKeybinds: true,
+ onConfirm() {
+ locatorConfig.useEntity = useEntity.get()
+ locatorConfig.entityType = entityType.get()
+ locatorConfig.summonCommands = summonCommands.get()
+ locatorConfig.tickingCommands = tickingCommands.get()
+
+ locator.config = locatorConfig.toJSON()
+ },
+ }).show()
+}
+
+export const LOCATOR_CONFIG_ACTION = createAction(`${PACKAGE.name}:locator_config`, {
+ icon: 'settings',
+ name: translate('action.open_locator_config.name'),
+ condition: () => Format === BLUEPRINT_FORMAT,
+ click: () => {
+ const locator = Locator.selected.at(0)
+ if (!locator) return
+ openLocatorConfigDialog(locator)
+ },
+})
diff --git a/src/interface/textDisplayConfigDialog.ts b/src/interface/textDisplayConfigDialog.ts
new file mode 100644
index 00000000..33fde1c8
--- /dev/null
+++ b/src/interface/textDisplayConfigDialog.ts
@@ -0,0 +1,95 @@
+import { isCurrentFormat } from '../blueprintFormat'
+import { TextDisplayConfig } from '../nodeConfigs'
+import { PACKAGE } from '../constants'
+import { createAction } from '../util/moddingTools'
+import { Valuable } from '../util/stores'
+import { SvelteDialog } from '../util/svelteDialog'
+import { translate } from '../util/translation'
+import { Variant } from '../variants'
+import TextDisplayConfigDialog from '../components/textDisplayConfigDialog.svelte'
+import { TextDisplay } from '../outliner/textDisplay'
+
+export function openBoneConfigDialog(bone: TextDisplay) {
+ // Blockbench's JSON stringifier doesn't handle custom toJSON functions, so I'm storing the config JSON in the bone instead of the actual BoneConfig object
+ const oldConfig = TextDisplayConfig.fromJSON((bone.config ??= new TextDisplayConfig().toJSON()))
+
+ const billboard = new Valuable(oldConfig.billboard)
+ const overrideBrightness = new Valuable(oldConfig.overrideBrightness)
+ const brightnessOverride = new Valuable(oldConfig.brightnessOverride)
+ const glowing = new Valuable(oldConfig.glowing)
+ const overrideGlowColor = new Valuable(oldConfig.overrideGlowColor)
+ const glowColor = new Valuable(oldConfig.glowColor)
+ const invisible = new Valuable(oldConfig.invisible)
+ const nbt = new Valuable(oldConfig.nbt)
+ const shadowRadius = new Valuable(oldConfig.shadowRadius)
+ const shadowStrength = new Valuable(oldConfig.shadowStrength)
+ const useNBT = new Valuable(oldConfig.useNBT)
+
+ new SvelteDialog({
+ id: `${PACKAGE.name}:textDisplayConfigDialog`,
+ title: translate('dialog.text_display_config.title'),
+ width: 400,
+ svelteComponent: TextDisplayConfigDialog,
+ svelteComponentProperties: {
+ variant: Variant.selected,
+ billboard,
+ overrideBrightness,
+ brightnessOverride,
+ glowing,
+ overrideGlowColor,
+ glowColor,
+ invisible,
+ nbt,
+ shadowRadius,
+ shadowStrength,
+ useNBT,
+ },
+ preventKeybinds: true,
+ onConfirm() {
+ const newConfig = new TextDisplayConfig()
+
+ newConfig.billboard = billboard.get()
+ newConfig.overrideBrightness = overrideBrightness.get()
+ newConfig.brightnessOverride = brightnessOverride.get()
+ newConfig.glowing = glowing.get()
+ newConfig.overrideGlowColor = overrideGlowColor.get()
+ newConfig.glowColor = glowColor.get()
+ newConfig.invisible = invisible.get()
+ newConfig.nbt = nbt.get()
+ newConfig.shadowRadius = shadowRadius.get()
+ newConfig.shadowStrength = shadowStrength.get()
+ newConfig.useNBT = useNBT.get()
+
+ const defaultConfig = TextDisplayConfig.getDefault()
+
+ newConfig.billboard === defaultConfig.billboard && (newConfig.billboard = undefined)
+ newConfig.overrideBrightness === defaultConfig.overrideBrightness &&
+ (newConfig.overrideBrightness = undefined)
+ newConfig.brightnessOverride === defaultConfig.brightnessOverride &&
+ (newConfig.brightnessOverride = undefined)
+ newConfig.glowing === defaultConfig.glowing && (newConfig.glowing = undefined)
+ newConfig.overrideGlowColor === defaultConfig.overrideGlowColor &&
+ (newConfig.overrideGlowColor = undefined)
+ newConfig.glowColor === defaultConfig.glowColor && (newConfig.glowColor = undefined)
+ newConfig.invisible === defaultConfig.invisible && (newConfig.invisible = undefined)
+ newConfig.nbt === defaultConfig.nbt && (newConfig.nbt = undefined)
+ newConfig.shadowRadius === defaultConfig.shadowRadius &&
+ (newConfig.shadowRadius = undefined)
+ newConfig.shadowStrength === defaultConfig.shadowStrength &&
+ (newConfig.shadowStrength = undefined)
+ newConfig.useNBT === defaultConfig.useNBT && (newConfig.useNBT = undefined)
+
+ bone.config = newConfig.toJSON()
+ },
+ }).show()
+}
+
+export const TEXT_DISPLAY_CONFIG_ACTION = createAction(`${PACKAGE.name}:text_display_config`, {
+ icon: 'settings',
+ name: translate('action.open_text_display_config.name'),
+ condition: () => isCurrentFormat(),
+ click: () => {
+ if (TextDisplay.selected.length === 0) return
+ openBoneConfigDialog(TextDisplay.selected[0])
+ },
+})
diff --git a/src/interface/textDisplayElementPanel.ts b/src/interface/textDisplayElementPanel.ts
new file mode 100644
index 00000000..cddd3908
--- /dev/null
+++ b/src/interface/textDisplayElementPanel.ts
@@ -0,0 +1,100 @@
+import { isCurrentFormat } from '../blueprintFormat'
+import TextDisplayElementPanel from '../components/textDisplayElementPanel.svelte'
+import { PACKAGE } from '../constants'
+import { TextDisplay } from '../outliner/textDisplay'
+import { injectSvelteCompomponentMod } from '../util/injectSvelte'
+import { floatToHex } from '../util/misc'
+import { translate } from '../util/translation'
+
+injectSvelteCompomponentMod({
+ svelteComponent: TextDisplayElementPanel,
+ svelteComponentProperties: {},
+ elementSelector() {
+ return document.querySelector('#panel_element')
+ },
+})
+
+export const TEXT_DISPLAY_WIDTH_SLIDER = new NumSlider(
+ `${PACKAGE.name}:textDisplayLineWidthSlider`,
+ {
+ name: translate('tool.text_display.line_width.title'),
+ icon: 'format_size',
+ description: translate('tool.text_display.line_width.description'),
+ settings: {
+ min: 1,
+ max: 10000,
+ interval: 1,
+ },
+ condition: () => isCurrentFormat() && !!TextDisplay.selected.length,
+ get() {
+ const selected = TextDisplay.selected[0]
+ if (!selected) return 0
+ return selected.lineWidth
+ },
+ change(value) {
+ const selected = TextDisplay.selected[0]
+ if (!selected) return
+ selected.lineWidth = Math.clamp(value(selected.lineWidth), 1, 10000)
+ },
+ }
+)
+
+export const TEXT_DISPLAY_BACKGROUND_COLOR_PICKER = new ColorPicker(
+ `${PACKAGE.name}:textDisplayBackgroundColorPicker`,
+ {
+ name: translate('tool.text_display.background_color.title'),
+ icon: 'format_color_fill',
+ description: translate('tool.text_display.background_color.description'),
+ condition: () => isCurrentFormat() && !!TextDisplay.selected.length,
+ }
+)
+// @ts-expect-error
+TEXT_DISPLAY_BACKGROUND_COLOR_PICKER.jq.spectrum('option', 'defaultColor', '#0000003f')
+TEXT_DISPLAY_BACKGROUND_COLOR_PICKER.get = function () {
+ const selected = TextDisplay.selected[0]
+ if (!selected) return new tinycolor('#0000003f')
+ return new tinycolor(selected.backgroundColor + floatToHex(selected.backgroundAlpha))
+}
+TEXT_DISPLAY_BACKGROUND_COLOR_PICKER.set = function (this: ColorPicker, color: string) {
+ this.value = new tinycolor(color)
+ // @ts-expect-error
+ this.jq.spectrum('set', this.value.toHex8String())
+
+ const selected = TextDisplay.selected[0]
+ if (!selected) return this
+ selected.backgroundColor = this.value.toHexString()
+ selected.backgroundAlpha = this.value.getAlpha()
+ return this
+}
+TEXT_DISPLAY_BACKGROUND_COLOR_PICKER.change = function (
+ this: ColorPicker,
+ color: tinycolor.Instance
+) {
+ const selected = TextDisplay.selected[0]
+ if (!selected) return this
+ selected.backgroundColor = color.toHexString()
+ selected.backgroundAlpha = color.getAlpha()
+ return this
+}
+
+export const TEXT_DISPLAY_SHADOW_TOGGLE = new Toggle(`${PACKAGE.name}:textDisplayShadowToggle`, {
+ name: translate('tool.text_display.text_shadow.title'),
+ icon: 'check_box_outline_blank',
+ description: translate('tool.text_display.text_shadow.description'),
+ condition: () => isCurrentFormat() && !!TextDisplay.selected.length,
+ click() {
+ //
+ },
+ onChange() {
+ const scope = TEXT_DISPLAY_SHADOW_TOGGLE
+ scope.setIcon(scope.value ? 'check_box' : 'check_box_outline_blank')
+ const selected = TextDisplay.selected[0]
+ if (!selected) return
+ selected.shadow = TEXT_DISPLAY_SHADOW_TOGGLE.value
+ },
+})
+TEXT_DISPLAY_SHADOW_TOGGLE.set = function (value) {
+ if (this.value === value) return this
+ this.click()
+ return this
+}
diff --git a/src/interface/unexpectedErrorDialog.ts b/src/interface/unexpectedErrorDialog.ts
new file mode 100644
index 00000000..85691059
--- /dev/null
+++ b/src/interface/unexpectedErrorDialog.ts
@@ -0,0 +1,18 @@
+import UnexpectedErrorDialog from '../components/unexpectedErrorDialog.svelte'
+import { PACKAGE } from '../constants'
+import { SvelteDialog } from '../util/svelteDialog'
+import { translate } from '../util/translation'
+
+export function openUnexpectedErrorDialog(error: Error) {
+ new SvelteDialog({
+ id: `${PACKAGE.name}:unexpectedError`,
+ title: translate('dialog.unexpected_error.title'),
+ width: 600,
+ svelteComponent: UnexpectedErrorDialog,
+ svelteComponentProperties: {
+ error,
+ },
+ preventKeybinds: true,
+ buttons: [translate('dialog.unexpected_error.close_button')],
+ }).show()
+}
diff --git a/src/interface/vanillaBlockDisplayConfigDialog.ts b/src/interface/vanillaBlockDisplayConfigDialog.ts
new file mode 100644
index 00000000..6bc730eb
--- /dev/null
+++ b/src/interface/vanillaBlockDisplayConfigDialog.ts
@@ -0,0 +1,100 @@
+import { isCurrentFormat } from '../blueprintFormat'
+import { TextDisplayConfig } from '../nodeConfigs'
+import { PACKAGE } from '../constants'
+import { createAction } from '../util/moddingTools'
+import { Valuable } from '../util/stores'
+import { SvelteDialog } from '../util/svelteDialog'
+import { translate } from '../util/translation'
+import { Variant } from '../variants'
+import { VanillaBlockDisplay } from '../outliner/vanillaBlockDisplay'
+import VanillaBlockDisplayConfigDialog from '../components/vanillaBlockDisplayConfigDialog.svelte'
+
+export function openVanillaBlockDisplayConfigDialog(display: VanillaBlockDisplay) {
+ // Blockbench's JSON stringifier doesn't handle custom toJSON functions, so I'm storing the config JSON in the bone instead of the actual BoneConfig object
+ const oldConfig = TextDisplayConfig.fromJSON(
+ (display.config ??= new TextDisplayConfig().toJSON())
+ )
+
+ const billboard = new Valuable(oldConfig.billboard)
+ const overrideBrightness = new Valuable(oldConfig.overrideBrightness)
+ const brightnessOverride = new Valuable(oldConfig.brightnessOverride)
+ const glowing = new Valuable(oldConfig.glowing)
+ const overrideGlowColor = new Valuable(oldConfig.overrideGlowColor)
+ const glowColor = new Valuable(oldConfig.glowColor)
+ const invisible = new Valuable(oldConfig.invisible)
+ const nbt = new Valuable(oldConfig.nbt)
+ const shadowRadius = new Valuable(oldConfig.shadowRadius)
+ const shadowStrength = new Valuable(oldConfig.shadowStrength)
+ const useNBT = new Valuable(oldConfig.useNBT)
+
+ new SvelteDialog({
+ id: `${PACKAGE.name}:vanillaItemDisplayConfigDialog`,
+ title: translate('dialog.vanilla_item_display_config.title'),
+ width: 400,
+ svelteComponent: VanillaBlockDisplayConfigDialog,
+ svelteComponentProperties: {
+ variant: Variant.selected,
+ billboard,
+ overrideBrightness,
+ brightnessOverride,
+ glowing,
+ overrideGlowColor,
+ glowColor,
+ invisible,
+ nbt,
+ shadowRadius,
+ shadowStrength,
+ useNBT,
+ },
+ preventKeybinds: true,
+ onConfirm() {
+ const newConfig = new TextDisplayConfig()
+
+ newConfig.billboard = billboard.get()
+ newConfig.overrideBrightness = overrideBrightness.get()
+ newConfig.brightnessOverride = brightnessOverride.get()
+ newConfig.glowing = glowing.get()
+ newConfig.overrideGlowColor = overrideGlowColor.get()
+ newConfig.glowColor = glowColor.get()
+ newConfig.invisible = invisible.get()
+ newConfig.nbt = nbt.get()
+ newConfig.shadowRadius = shadowRadius.get()
+ newConfig.shadowStrength = shadowStrength.get()
+ newConfig.useNBT = useNBT.get()
+
+ const defaultConfig = TextDisplayConfig.getDefault()
+
+ newConfig.billboard === defaultConfig.billboard && (newConfig.billboard = undefined)
+ newConfig.overrideBrightness === defaultConfig.overrideBrightness &&
+ (newConfig.overrideBrightness = undefined)
+ newConfig.brightnessOverride === defaultConfig.brightnessOverride &&
+ (newConfig.brightnessOverride = undefined)
+ newConfig.glowing === defaultConfig.glowing && (newConfig.glowing = undefined)
+ newConfig.overrideGlowColor === defaultConfig.overrideGlowColor &&
+ (newConfig.overrideGlowColor = undefined)
+ newConfig.glowColor === defaultConfig.glowColor && (newConfig.glowColor = undefined)
+ newConfig.invisible === defaultConfig.invisible && (newConfig.invisible = undefined)
+ newConfig.nbt === defaultConfig.nbt && (newConfig.nbt = undefined)
+ newConfig.shadowRadius === defaultConfig.shadowRadius &&
+ (newConfig.shadowRadius = undefined)
+ newConfig.shadowStrength === defaultConfig.shadowStrength &&
+ (newConfig.shadowStrength = undefined)
+ newConfig.useNBT === defaultConfig.useNBT && (newConfig.useNBT = undefined)
+
+ display.config = newConfig.toJSON()
+ },
+ }).show()
+}
+
+export const VANILLA_BLOCK_DISPLAY_CONFIG_ACTION = createAction(
+ `${PACKAGE.name}:open_vanilla_block_display_config`,
+ {
+ icon: 'settings',
+ name: translate('action.open_vanilla_block_display_config.name'),
+ condition: () => isCurrentFormat(),
+ click: () => {
+ if (VanillaBlockDisplay.selected.length === 0) return
+ openVanillaBlockDisplayConfigDialog(VanillaBlockDisplay.selected[0])
+ },
+ }
+)
diff --git a/src/interface/vanillaBlockDisplayElementPanel.ts b/src/interface/vanillaBlockDisplayElementPanel.ts
new file mode 100644
index 00000000..63db554a
--- /dev/null
+++ b/src/interface/vanillaBlockDisplayElementPanel.ts
@@ -0,0 +1,10 @@
+import VanillaBlockDisplayElementPanel from '../components/vanillaBlockDisplayElementPanel.svelte'
+import { injectSvelteCompomponentMod } from '../util/injectSvelte'
+
+injectSvelteCompomponentMod({
+ svelteComponent: VanillaBlockDisplayElementPanel,
+ svelteComponentProperties: {},
+ elementSelector() {
+ return document.querySelector('#panel_element')
+ },
+})
diff --git a/src/interface/vanillaItemDisplayConfigDialog.ts b/src/interface/vanillaItemDisplayConfigDialog.ts
new file mode 100644
index 00000000..a77ed3f1
--- /dev/null
+++ b/src/interface/vanillaItemDisplayConfigDialog.ts
@@ -0,0 +1,100 @@
+import { isCurrentFormat } from '../blueprintFormat'
+import { TextDisplayConfig } from '../nodeConfigs'
+import { PACKAGE } from '../constants'
+import { createAction } from '../util/moddingTools'
+import { Valuable } from '../util/stores'
+import { SvelteDialog } from '../util/svelteDialog'
+import { translate } from '../util/translation'
+import { Variant } from '../variants'
+import { VanillaItemDisplay } from '../outliner/vanillaItemDisplay'
+import VanillaItemDisplayConfigDialog from '../components/vanillaItemDisplayConfigDialog.svelte'
+
+export function openVanillaItemDisplayConfigDialog(display: VanillaItemDisplay) {
+ // Blockbench's JSON stringifier doesn't handle custom toJSON functions, so I'm storing the config JSON in the bone instead of the actual BoneConfig object
+ const oldConfig = TextDisplayConfig.fromJSON(
+ (display.config ??= new TextDisplayConfig().toJSON())
+ )
+
+ const billboard = new Valuable(oldConfig.billboard)
+ const overrideBrightness = new Valuable(oldConfig.overrideBrightness)
+ const brightnessOverride = new Valuable(oldConfig.brightnessOverride)
+ const glowing = new Valuable(oldConfig.glowing)
+ const overrideGlowColor = new Valuable(oldConfig.overrideGlowColor)
+ const glowColor = new Valuable(oldConfig.glowColor)
+ const invisible = new Valuable(oldConfig.invisible)
+ const nbt = new Valuable(oldConfig.nbt)
+ const shadowRadius = new Valuable(oldConfig.shadowRadius)
+ const shadowStrength = new Valuable(oldConfig.shadowStrength)
+ const useNBT = new Valuable(oldConfig.useNBT)
+
+ new SvelteDialog({
+ id: `${PACKAGE.name}:vanillaItemDisplayConfigDialog`,
+ title: translate('dialog.vanilla_item_display_config.title'),
+ width: 400,
+ svelteComponent: VanillaItemDisplayConfigDialog,
+ svelteComponentProperties: {
+ variant: Variant.selected,
+ billboard,
+ overrideBrightness,
+ brightnessOverride,
+ glowing,
+ overrideGlowColor,
+ glowColor,
+ invisible,
+ nbt,
+ shadowRadius,
+ shadowStrength,
+ useNBT,
+ },
+ preventKeybinds: true,
+ onConfirm() {
+ const newConfig = new TextDisplayConfig()
+
+ newConfig.billboard = billboard.get()
+ newConfig.overrideBrightness = overrideBrightness.get()
+ newConfig.brightnessOverride = brightnessOverride.get()
+ newConfig.glowing = glowing.get()
+ newConfig.overrideGlowColor = overrideGlowColor.get()
+ newConfig.glowColor = glowColor.get()
+ newConfig.invisible = invisible.get()
+ newConfig.nbt = nbt.get()
+ newConfig.shadowRadius = shadowRadius.get()
+ newConfig.shadowStrength = shadowStrength.get()
+ newConfig.useNBT = useNBT.get()
+
+ const defaultConfig = TextDisplayConfig.getDefault()
+
+ newConfig.billboard === defaultConfig.billboard && (newConfig.billboard = undefined)
+ newConfig.overrideBrightness === defaultConfig.overrideBrightness &&
+ (newConfig.overrideBrightness = undefined)
+ newConfig.brightnessOverride === defaultConfig.brightnessOverride &&
+ (newConfig.brightnessOverride = undefined)
+ newConfig.glowing === defaultConfig.glowing && (newConfig.glowing = undefined)
+ newConfig.overrideGlowColor === defaultConfig.overrideGlowColor &&
+ (newConfig.overrideGlowColor = undefined)
+ newConfig.glowColor === defaultConfig.glowColor && (newConfig.glowColor = undefined)
+ newConfig.invisible === defaultConfig.invisible && (newConfig.invisible = undefined)
+ newConfig.nbt === defaultConfig.nbt && (newConfig.nbt = undefined)
+ newConfig.shadowRadius === defaultConfig.shadowRadius &&
+ (newConfig.shadowRadius = undefined)
+ newConfig.shadowStrength === defaultConfig.shadowStrength &&
+ (newConfig.shadowStrength = undefined)
+ newConfig.useNBT === defaultConfig.useNBT && (newConfig.useNBT = undefined)
+
+ display.config = newConfig.toJSON()
+ },
+ }).show()
+}
+
+export const VANILLA_ITEM_DISPLAY_CONFIG_ACTION = createAction(
+ `${PACKAGE.name}:open_vanilla_item_display_config`,
+ {
+ icon: 'settings',
+ name: translate('action.open_vanilla_item_display_config.name'),
+ condition: () => isCurrentFormat(),
+ click: () => {
+ if (VanillaItemDisplay.selected.length === 0) return
+ openVanillaItemDisplayConfigDialog(VanillaItemDisplay.selected[0])
+ },
+ }
+)
diff --git a/src/interface/vanillaItemDisplayElementPanel.ts b/src/interface/vanillaItemDisplayElementPanel.ts
new file mode 100644
index 00000000..6d136dcc
--- /dev/null
+++ b/src/interface/vanillaItemDisplayElementPanel.ts
@@ -0,0 +1,10 @@
+import VanillaItemDisplayElementPanel from '../components/vanillaItemDisplayElementPanel.svelte'
+import { injectSvelteCompomponentMod } from '../util/injectSvelte'
+
+injectSvelteCompomponentMod({
+ svelteComponent: VanillaItemDisplayElementPanel,
+ svelteComponentProperties: {},
+ elementSelector() {
+ return document.querySelector('#panel_element')
+ },
+})
diff --git a/src/interface/variantConfigDialog.ts b/src/interface/variantConfigDialog.ts
new file mode 100644
index 00000000..bb5daa26
--- /dev/null
+++ b/src/interface/variantConfigDialog.ts
@@ -0,0 +1,43 @@
+import VariantConfigDialogSvelteComponent from '../components/variantConfigDialog.svelte'
+import { PACKAGE } from '../constants'
+import { events } from '../util/events'
+import { Valuable } from '../util/stores'
+import { SvelteDialog } from '../util/svelteDialog'
+import { translate } from '../util/translation'
+import { Variant } from '../variants'
+
+export function openVariantConfigDialog(variant: Variant) {
+ const displayName = new Valuable(variant.displayName)
+ const name = new Valuable(variant.name)
+ const uuid = new Valuable(variant.uuid)
+ const textureMap = variant.textureMap.copy()
+ const generateNameFromDisplayName = new Valuable(variant.generateNameFromDisplayName)
+ const excludedBones = new Valuable(variant.excludedBones)
+
+ new SvelteDialog({
+ id: `${PACKAGE.name}:variantConfig`,
+ title: translate('dialog.variant_config.title'),
+ width: 512,
+ svelteComponent: VariantConfigDialogSvelteComponent,
+ svelteComponentProperties: {
+ variant,
+ displayName,
+ name,
+ uuid,
+ textureMap,
+ generateNameFromDisplayName,
+ excludedBones,
+ },
+ preventKeybinds: true,
+ onConfirm() {
+ variant.displayName = displayName.get()
+ variant.name = name.get()
+ variant.uuid = uuid.get()
+ variant.textureMap = textureMap
+ variant.generateNameFromDisplayName = generateNameFromDisplayName.get()
+ variant.excludedBones = excludedBones.get()
+ events.UPDATE_VARIANT.dispatch(variant)
+ variant.select()
+ },
+ }).show()
+}
diff --git a/src/interface/variantsPanel.ts b/src/interface/variantsPanel.ts
new file mode 100644
index 00000000..c2688fc4
--- /dev/null
+++ b/src/interface/variantsPanel.ts
@@ -0,0 +1,81 @@
+import { isCurrentFormat } from '../blueprintFormat'
+import VariantsPanel from '../components/variantsPanel.svelte'
+import { PACKAGE } from '../constants'
+import { createAction, createMenu } from '../util/moddingTools'
+import { SveltePanel } from '../util/sveltePanel'
+import { translate } from '../util/translation'
+import { Variant } from '../variants'
+import { openVariantConfigDialog } from './variantConfigDialog'
+
+export const CREATE_VARIANT_ACTION = createAction(`${PACKAGE.name}:createVariant`, {
+ name: translate('action.variants.create'),
+ icon: 'add',
+ click() {
+ new Variant('New Variant')
+ },
+})
+
+export const DUPLICATE_VARIANT_ACTION = createAction(`${PACKAGE.name}:duplicateVariant`, {
+ name: translate('action.variants.duplicate'),
+ icon: 'content_copy',
+ condition: () => !!Variant.selected,
+ click() {
+ if (!Variant.selected) return
+ Variant.selected.duplicate()
+ },
+})
+
+export const DELETE_VARIANT_ACTION = createAction(`${PACKAGE.name}:deleteVariant`, {
+ name: translate('action.variants.delete'),
+ icon: 'delete',
+ condition: () => !!Variant.selected && !Variant.selected.isDefault,
+ click() {
+ if (!Variant.selected || Variant.selected.isDefault) return
+ Variant.selected.delete()
+ },
+})
+
+export const OPEN_VARIANT_CONFIG_ACTION = createAction(`${PACKAGE.name}:openVariantConfig`, {
+ name: translate('action.variants.open_config'),
+ icon: 'settings',
+ condition: () => !!Variant.selected,
+ click() {
+ if (!Variant.selected) return
+ openVariantConfigDialog(Variant.selected)
+ },
+})
+
+export const VARIANT_PANEL_CONTEXT_MENU = createMenu(
+ [
+ CREATE_VARIANT_ACTION.id,
+ DUPLICATE_VARIANT_ACTION.id,
+ OPEN_VARIANT_CONFIG_ACTION.id,
+ new MenuSeparator(),
+ DELETE_VARIANT_ACTION.id,
+ ],
+ {}
+)
+
+export const VARIANTS_PANEL = new SveltePanel({
+ id: `${PACKAGE.name}:variantsPanel`,
+ name: translate('panel.variants.title'),
+ expand_button: true,
+ default_side: 'right',
+ default_position: {
+ slot: 'left_bar',
+ height: 200,
+ float_position: [0, 0],
+ float_size: [200, 200],
+ folded: false,
+ },
+ icon: 'settings',
+ condition: () =>
+ !!(
+ isCurrentFormat() &&
+ Modes.selected &&
+ (Modes.selected.id === Modes.options.edit.id ||
+ Modes.selected.id === Modes.options.paint.id)
+ ),
+ svelteComponent: VariantsPanel,
+ svelteComponentProperties: {},
+})
diff --git a/src/lang/en.yaml b/src/lang/en.yaml
deleted file mode 100644
index 27f334b5..00000000
--- a/src/lang/en.yaml
+++ /dev/null
@@ -1,366 +0,0 @@
-animated_java.title: Animated Java
-
-animated_java.menubar.settings: Animated Java
-
-animated_java.menubar.items.about: About
-animated_java.menubar.items.settings: Settings
-animated_java.menubar.items.project_settings: Project Settings
-animated_java.menubar.items.documentation: Documentation
-animated_java.menubar.items.export_project: Export Project
-animated_java.menubar.items.bone_config: Bone Config
-animated_java.menubar.items.camera_config: Camera Config
-animated_java.menubar.items.locator_config: Locator Config
-
-animated_java.quickmessage.exported_successfully: Project Exported Successfully!
-
-### Dialogs
-animated_java.dialog.close_button: Done
-
-### About
-animated_java.dialog.about.title: About Animated Java
-
-### Export in Progress
-animated_java.dialog.export_in_progress.title: Exporting Project...
-
-### Settings
-animated_java.settings.accessability_options_group: Accessability
-
-animated_java.dialog.settings.title: Animated Java Settings
-animated_java.settings.reduced_motion: Reduced Motion
-animated_java.settings.reduced_motion.description: |-
- Disable all UI animations in Animated Java's Menus.
- This will disable animations and other effects that may cause motion sickness.
-
-animated_java.settings.resource_pack_group: Resource Pack
-
-animated_java.settings.minify_output: Minify Output
-animated_java.settings.minify_output.description: |-
- Minify the output of the exported resource pack.
- This will remove all comments and whitespace from the output.
- This will make the output smaller, but will make it harder to read.
-
-### Documentation
-animated_java.dialog.documentation.title: Animated Java Documentation
-
-animated_java.dialog.documentation.loading: Loading Documentation...
-
-animated_java.dialog.documentation.error.failed_to_load.title: Failed to Load Documentation! :(
-animated_java.dialog.documentation.error.failed_to_load.description: |-
- Make sure you're connected to the internet!
-
-### Project Settings
-animated_java.dialog.project_settings.title: Animated Java Project Settings
-
-animated_java.dialog.project_settings.project_group: Project
-
-animated_java.project_settings.project_namespace: Project Name
-animated_java.project_settings.project_namespace.description: |-
- The name and namespace of the project.
-animated_java.project_settings.project_namespace.error.unset: |-
- Project Name cannot be empty
-
-animated_java.project_settings.project_resolution: Project Resolution
-animated_java.project_settings.project_resolution.description: |-
- The UV resolution of the Project.
- This should equal the resolution of the largest texture in your project.
-
-animated_java.project_settings.target_minecraft_version: Target Minecraft Version
-animated_java.project_settings.target_minecraft_version.description: |-
- The version of Minecraft you expect to use the exported rig in.
-
-animated_java.project_settings.resourcepack_group: Resource Pack
-
-animated_java.project_settings.rig_item: Rig Item
-animated_java.project_settings.rig_item.description: |-
- The item to use to display the models used in the rig.
-animated_java.project_settings.rig_item.error.unset: |-
- Rig Item cannot be empty
-animated_java.project_settings.rig_item.error.space: |-
- Rig Item must be a valid item ID
- Item ID cannot contain spaces.
-animated_java.project_settings.rig_item.error.invalid_namespace: |-
- Rig Item must be a valid item ID
- Item ID must have a namespace.
-animated_java.project_settings.rig_item.warning.unknown_item: |-
- Rig Item isn't in vanilla minecraft
- This may cause issues when exporting.
- Ignore this warning if you're using snapshots or mods.
-
-animated_java.project_settings.enable_advanced_resource_pack_settings: Enable Advanced Resource Pack Settings
-animated_java.project_settings.enable_advanced_resource_pack_settings.description: |-
- Enable advanced resource pack settings.
- This will allow you to set the Rig Item Model and Rig Export Folder manually instead of Animated Java automatically figuring it out for you.
- If you're not sure what this does, leave it disabled.
-
-animated_java.project_settings.rig_item_model: Rig Item Model
-animated_java.project_settings.rig_item_model.description: |-
- The export location of the rig item's model.
- Should be in a valid resource pack under `minecraft:models/item`
-animated_java.project_settings.rig_item_model.error.unset: |-
- Rig Item Model cannot be empty.
-animated_java.project_settings.rig_item_model.error.invalid_path: |-
- Rig Item Model must be in a valid resource pack
- The targeted file must be under 'resources/assets//models/'.
-animated_java.project_settings.rig_item_model.error.item_does_not_match: |-
- Rig Item Model must have the same item name as Rig Item
- You've selected "%rigItem" as your Rig Item, but Rig Item Model targets "%pathItem".
-animated_java.project_settings.rig_item_model.error.rig_item_unset: |-
- Rig Item is not set
- Please set the Rig Item before setting the Rig Item Model.
-
-animated_java.project_settings.rig_export_folder: Rig Export Folder
-animated_java.project_settings.rig_export_folder.description: |-
- What folder to put the generated model files in.
- Should be inside of a valid resource pack.
-animated_java.project_settings.rig_export_folder.error.unset: |-
- Rig Export Folder cannot be empty
-animated_java.project_settings.rig_export_folder.error.invalid_path: |-
- Rig Export Folder must be in a valid resource pack
- The targeted folder must be under 'resources/assets//models/'.
-
-animated_java.project_settings.texture_export_folder: Texture Export Folder
-animated_java.project_settings.texture_export_folder.description: |-
- What folder to put the generated texture files in.
- Should be inside of a valid resource pack.
-animated_java.project_settings.texture_export_folder.error.unset: |-
- Texture Export Folder cannot be empty
-animated_java.project_settings.texture_export_folder.error.invalid_path: |-
- Texture Export Folder must be in a valid resource pack
- The targeted folder must be under 'resources/assets//textures/'.
-
-animated_java.project_settings.resource_pack_mcmeta: Resource Pack
-animated_java.project_settings.resource_pack_mcmeta.description: |-
- The Resource Pack to inject the Rig's models into.
- This setting should be targeting the pack.mcmeta file of a valid Resource Pack.
-animated_java.project_settings.resource_pack_mcmeta.error.unset: |-
- You must select a Resource Pack.
-animated_java.project_settings.resource_pack_mcmeta.error.invalid_path: |-
- The selected path is not a valid Resource Pack!
- Make sure you've selected the correct pack.mcmeta, and that the Resource Pack has an assets folder.
-
-animated_java.project_settings.verbose: Verbose
-animated_java.project_settings.verbose.description: |-
- Whether to print verbose output to the chat when running AJ functions.
- Useful for debugging.
-
-animated_java.project_settings.exporter_settings: '%exporter Settings'
-
-animated_java.project_settings.exporter: Exporter
-animated_java.project_settings.exporter.description: |-
- The exporter to use when exporting this project.
-
-### Bone Config
-animated_java.dialog.bone_config: Animated Java Bone Config
-animated_java.bone_config.nbt: Bone Entity NBT
-animated_java.bone_config.nbt.description: |-
- Custom NBT to apply to this bone entity.
- Note that some internal NBT may take priority over this setting.
-
-### Camera Config
-animated_java.dialog.camera_config: Animated Java Camera Config
-
-animated_java.camera_config.entity_type: Teleported Entity Type
-animated_java.camera_config.entity_type.description: |-
- The entity to use when summoning the camera.
- For instance, if you set this to `minecraft:armor_stand`, The camera will be a armor_stand in-game.
-animated_java.camera_config.entity_type.error.space: |-
- Entity IDs cannot contain spaces.
-animated_java.camera_config.entity_type.error.invalid_namespace: |-
- Entity IDs must have a namespace.
-animated_java.camera_config.entity_type.warning.unknown_entity: |-
- Entity ID isn't in vanilla minecraft
- This may cause issues when exporting.
- Ignore this warning if you're using snapshots or mods.
-
-animated_java.camera_config.nbt: NBT
-animated_java.camera_config.nbt.description: |-
- Custom NBT to apply to the summoned camera entity.
- Note that some internal NBT tags will take priority over this setting.
-
-### Locator Config
-animated_java.dialog.locator_config: Animated Java Locator Config
-
-animated_java.locator_config.entity_type: Entity Type
-animated_java.locator_config.entity_type.description: |-
- The entity to use when summoning the locator.
- For instance, if you set this to `minecraft:pig`, The locator will be a pig in-game.
-animated_java.locator_config.entity_type.error.space: |-
- Entity IDs cannot contain spaces.
-animated_java.locator_config.entity_type.error.invalid_namespace: |-
- Entity IDs must have a namespace.
-animated_java.locator_config.entity_type.warning.unknown_entity: |-
- Entity ID isn't in vanilla minecraft
- This may cause issues when exporting.
- Ignore this warning if you're using snapshots or mods.
-
-animated_java.locator_config.nbt: NBT
-animated_java.locator_config.nbt.description: |-
- Custom NBT to apply to the summoned locator entity.
- Note that some internal NBT tags will take priority over this setting.
-
-### Animation Config
-animated_java.dialog.animation_config.title: Animation Properties
-
-animated_java.animation_config.animation_name: Name
-animated_java.animation_config.animation_name.description: |-
- The name of the animation.
-animated_java.animation_config.animation_name.error.duplicate_name: |-
- An animation with the name "%name" already exists.
- Animation names must be unique.
-
-animated_java.animation_config.loop: Loop Mode
-animated_java.animation_config.loop.description: |-
- The loop mode of the animation.
- once: The animation will only play once and immediately reset once complete.
- loop: The animation will loop forever.
- hold: The animation will hold on the last frame.
-animated_java.animation_config.loop.options.once: Play Once
-animated_java.animation_config.loop.options.loop: Loop
-animated_java.animation_config.loop.options.hold: Hold on Last Frame
-
-animated_java.animation_config.loop_delay: Loop Delay
-animated_java.animation_config.loop_delay.description: |-
- The delay (in ticks) between loops of the animation.
- Only applies if the loop mode is set to loop.
-
-animated_java.animation_config.start_delay: Start Delay
-animated_java.animation_config.start_delay.description: |-
- The delay (in ticks) before the animation starts after the play function is ran.
-
-animated_java.animation_config.affected_bones_is_a_whitelist: Ignored Bones is a Whitelist
-animated_java.animation_config.affected_bones_is_a_whitelist.description: |-
- If true, only the bones in the Ignored Bones list will be affected by this animation.
- If false, all bones except the ones in the Ignored Bones list will be affected by this animation.
-
-animated_java.animation_config.affected_bones: Ignored Bones
-animated_java.animation_config.affected_bones.add_new_item_message: Add a Bone
-animated_java.animation_config.affected_bones.description: |-
- The bones that this animation will affect.
- If this list is a whitelist, only the bones in the Ignored Bones list will be affected by this animation.
- If this list is a blacklist, all bones except the ones in the Ignored Bones list will be affected by this animation.
-
-### Variants Panel
-animated_java.panels.variants.name: Variants
-animated_java.panels.variants.items: Variant
-animated_java.panels.variants.delete_default_variant: Cannot delete default Variant
-animated_java.panels.variants.delete_variant: Delete Variant
-animated_java.panels.variants.edit_variant: Edit Variant
-animated_java.panels.variants.default_variant: Default Variant
-animated_java.actions.add_variant.name: Add Variant
-animated_java.actions.add_variant.description: Create a new Variant
-animated_java.actions.variant_properties.name: Variant Properties
-animated_java.actions.variant_properties.description: Open the Variant properties dialog.
-animated_java.actions.duplicate_variant.name: Duplicate Variant
-animated_java.actions.duplicate_variant.description: Duplicate this Variant.
-
-### Variants Properties Dialog
-animated_java.dialog.variant_properties.title: Variant Properties
-
-animated_java.dialog.variant_properties.variant_name: Name
-animated_java.dialog.variant_properties.variant_name.description: |-
- The name of the variant.
-animated_java.variant_properties.variant_name.error.duplicate_name: |-
- A variant with the name "%name" already exists.
- Variant names must be unique.
-
-animated_java.dialog.variant_properties.affected_bones_is_a_whitelist: Affected Bones is a Whitelist
-animated_java.dialog.variant_properties.affected_bones_is_a_whitelist.description: |-
- Whether the affected bones list is a whitelist or blacklist.
- If true, only the bones in the Affected Bones list will be affected by this Variant.
- If false, all bones except the ones in the Affected Bones list will be affected by this Variant.
-
-animated_java.dialog.variant_properties.affected_bones: Affected Bones
-animated_java.dialog.variant_properties.affected_bones.add_new_item_message: Add a Bone
-animated_java.dialog.variant_properties.affected_bones.description: |-
- The list of bones affected by this variant.
- If this is a whitelist, only the bones in the list will be modified by this Variant.
- If this is a blacklist, all bones except the ones in the list will be modified by this Variant.
-animated_java.dialog.variant_properties.textureMap: Texture Map
-animated_java.dialog.variant_properties.textureMap.description: |-
- The texture map to use for this variant.
- Lets you choose what textures are replaced when this Variant is appied.
-
-### Custom Keyframes
-animated_java.keyframe.animation: Animation
-animated_java.keyframe.animation.description: |-
- The animation to apply when this keyframe is reached.
-
-animated_java.keyframe.variant: Variant
-animated_java.keyframe.variant.description: |-
- The variant to apply when this keyframe is reached.
-
-animated_java.keyframe.commands: Commands
-animated_java.keyframe.commands.description: |-
- A list of commands (A function) that run when the keyframe is reached.
-
-animated_java.keyframe.tweenDuration: Tween Duration
-animated_java.keyframe.tweenDuration.description: |-
- The duration (in ticks) of the tween between this keyframe and the next animation.
-
-animated_java.keyframe.tweenMode: Tween Mode
-animated_java.keyframe.tweenMode.description: |-
- The tween mode of the keyframe.
- Play: The animation will play from the start.
- Resume: The animation will play from the same anim_time as this keyframe.
-animated_java.keyframe.tweenMode.play: Play
-animated_java.keyframe.tweenMode.resume: Resume
-
-animated_java.keyframe.executeCondition: Execute Condition
-animated_java.keyframe.executeCondition.description: |-
- The execute command condition that must be met for the keyframe to be applied.
- This is an execute subcommand chain, so you can use any and all execute subcommands in this field.
- Example: "if score @s example matches 1.."
- Hint: You can separate subcommands with newlines!
-
-animated_java.timeline.animation: Animations
-animated_java.timeline.variant: Variants
-animated_java.timeline.commands: Commands
-
-### Popups
-animated_java.popup.close_button: Close
-animated_java.popup.confirm_button: Done
-animated_java.popup.cancel_button: Done
-
-# Unexpected Error
-animated_java.popup.unexpectedError.title: Unexpected Error
-animated_java.popup.unexpectedError.body: |-
- An unexpected error occurred!
- Please report this error on our Github or in the support channel on our official Discord server.
-
-# Confirm predicate file overwrite
-animated_java.popup.confirm_predicate_file_overwrite.title: Confirm Predicate File Overwrite
-animated_java.popup.confirm_predicate_file_overwrite.body: |-
- The file "%file" already exists and is not a Rig Item Model!
- Do you want to overwrite it?
-
- Full file path:
- %path
-
-# Invalid Texture Mapping(s)
-animated_java.popup.invalid_texture_mapping.title: Invalid Texture Mapping(s)
-animated_java.popup.invalid_texture_mapping.body: |-
- The Variant "%variant" has invalid texture mappings!
- One or more of the textures in it's texture map are missing from the project, or are invalid.
- The invalid mappings and a brief explanation of why they're invalid can be found below:
-animated_java.popup.invalid_texture_mapping.reason: Reason(s) for Invalidation
-animated_java.popup.invalid_texture_mapping.reason.invalid_from_texture: |-
- Couldn't find the "from" texture in the project.
-animated_java.popup.invalid_texture_mapping.reason.invalid_to_texture: |-
- Couldn't find the "to" texture in the project.
-animated_java.popup.invalid_texture_mapping.footer: |-
- The missing/invalid texture mappings will be removed from the variant.
- After closing this popup, please make sure you open the Variant's properties dialog and double check the texture mappings are correct.
-
-# Invalid Cube(s)
-animated_java.popup.invalid_cubes.title: Invalid Cube(s)
-animated_java.popup.invalid_cubes.body: |-
- Some Cubes have invalid rotations!
- The invalid cubes can be found below sorted by the Bones they're in.
- They will also be highlighted in the 3D view once you exit this dialog.
-
-# Failed Project Export Readiness
-animated_java.popup.failed_project_export_readiness.title: Export Failed
-animated_java.popup.failed_project_export_readiness.body: |-
- The project is not ready to be exported!
- The following issues were found:
-animated_java.popup.failed_project_export_readiness.issue: Project Setting "%s" has the following errors
diff --git a/src/lang/en.yml b/src/lang/en.yml
new file mode 100644
index 00000000..2c0f3add
--- /dev/null
+++ b/src/lang/en.yml
@@ -0,0 +1,397 @@
+### Actions
+animated_java.action.open_blueprint_settings.name: Blueprint Settings
+animated_java.action.open_documentation.name: Documentation
+animated_java.action.open_about.name: About
+animated_java.action.open_bone_config.name: Bone Config
+animated_java.action.open_locator_config.name: Locator Config
+animated_java.action.open_text_display_config.name: Text Display Config
+animated_java.action.export.name: Export
+animated_java.action.create_text_display.title: Add Text Display
+animated_java.action.create_vanilla_item_display.title: Add Vanilla Item Display
+animated_java.action.create_vanilla_block_display.title: Add Vanilla Block Display
+animated_java.action.open_vanilla_item_display_config.name: Vanilla Item Display Config
+animated_java.action.open_vanilla_block_display_config.name: Vanilla Block Display Config
+
+### Dialogs
+
+## About
+animated_java.dialog.about.title: About Animated Java
+animated_java.dialog.about.close_button: Close
+
+## Unexpected Error Dialog
+animated_java.dialog.unexpected_error.title: An Unexpected Error Occurred!
+animated_java.dialog.unexpected_error.close_button: Close
+animated_java.dialog.unexpected_error.copy_error_message_button.message: Error Message Copied to Clipboard!
+animated_java.dialog.unexpected_error.copy_error_message_button.description: Click to copy the error message to the clipboard.
+animated_java.dialog.unexpected_error.paragraph: 'Please report this error by joining our {0} and creating a thread in the #animated-java-support channel, or by creating an issue on our {1}. Thank you!'
+
+## Blueprint Settings Dialog
+animated_java.dialog.blueprint_settings.title: Blueprint Settings
+animated_java.dialog.blueprint_settings.advanced_settings_warning: Advanced settings should only be used if absolutely needed!
+animated_java.dialog.blueprint_settings.blueprint_name.title: Blueprint Name
+animated_java.dialog.blueprint_settings.blueprint_name.description: The name of the Blueprint. Only used to identify the project in the workspace.
+
+animated_java.dialog.blueprint_settings.texture_size.title: Texture Size
+animated_java.dialog.blueprint_settings.texture_size.description: The resolution of the UV editor. This should be the same size as the largest texture. For best results use a square texture, and make sure it's size is a power of 2.
+animated_java.dialog.blueprint_settings.texture_size.warning.not_square: The Texture Size should be square for best results.
+animated_java.dialog.blueprint_settings.texture_size.warning.not_a_power_of_2: The Texture Size should be a power of 2 for best results.
+animated_java.dialog.blueprint_settings.texture_size.error.does_not_match_largest_texture: The Texture Size should match the largest texture's size.
+
+# Export Settings
+animated_java.dialog.blueprint_settings.export_settings.title: Export Settings
+
+animated_java.dialog.blueprint_settings.export_namespace.title: Export Namespace
+animated_java.dialog.blueprint_settings.export_namespace.description: The namespace to export the project to. This is the namespace that will be used in the exported Resource Pack and Data Pack.
+animated_java.dialog.blueprint_settings.export_namespace.error.empty: The export namespace cannot be empty!
+animated_java.dialog.blueprint_settings.export_namespace.error.reserved: The export namespace "{0}" is reserved for internal functionality! Please choose a different namespace.
+animated_java.dialog.blueprint_settings.export_namespace.error.invalid_characters: The export namespace contains invalid characters! Namespaces can only contain letters, numbers, and underscores.
+
+animated_java.dialog.blueprint_settings.enable_plugin_mode.title: Plugin Mode
+animated_java.dialog.blueprint_settings.enable_plugin_mode.description: Whether or not to enable Plugin Mode. Enable when you plan to use the .ajblueprint file in a plugin instead of a Resource Pack and/or Data Pack.
+
+animated_java.dialog.blueprint_settings.enable_resource_pack.title: Export Resources
+animated_java.dialog.blueprint_settings.enable_resource_pack.description: Whether or not to export resources into a Resource Pack.
+
+animated_java.dialog.blueprint_settings.enable_data_pack.title: Export Data Pack
+animated_java.dialog.blueprint_settings.enable_data_pack.description: Whether or not to export into a Data Pack.
+
+animated_java.dialog.blueprint_settings.show_bounding_box.title: Show Bounding Box
+animated_java.dialog.blueprint_settings.show_bounding_box.description: Whether or not to show the bounding box in the editor.
+
+animated_java.dialog.blueprint_settings.auto_bounding_box.title: Auto Bounding Box
+animated_java.dialog.blueprint_settings.auto_bounding_box.description: |-
+ Whether or not to automatically calculate the bounding box based on the model's geometry.
+ NOTE: The auto bounding box will NOT take bone offsets from animations into account, so the bounding box may be smaller than needed in some cases.
+
+animated_java.dialog.blueprint_settings.bounding_box.title: Bounding Box
+animated_java.dialog.blueprint_settings.bounding_box.description: Determines the culling box of the model. The model will stop rendering when this box is off-screen.
+
+# Resource Pack Settings
+animated_java.dialog.blueprint_settings.resource_pack_settings.title: Resource Pack Settings
+
+animated_java.dialog.blueprint_settings.enable_advanced_resource_pack_settings.title: Use Advanced Settings
+animated_java.dialog.blueprint_settings.enable_advanced_resource_pack_settings.description: Whether or not to enable the advanced Resource Pack settings.
+
+animated_java.dialog.blueprint_settings.display_item.title: Display Item
+animated_java.dialog.blueprint_settings.display_item.description: The item to display the Blueprints models in-game. Multiple Blueprints can be placed on the same item and they will be merged automatically.
+animated_java.dialog.blueprint_settings.display_item.error.no_item_selected: No item selected!
+animated_java.dialog.blueprint_settings.display_item.error.invalid_item_id.no_namespace: The provided item ID is invalid! Item IDs should be in the format namespace:item_id.
+animated_java.dialog.blueprint_settings.display_item.error.invalid_item_id.whitespace: The provided item ID is invalid! Item IDs should not contain any whitespace.
+animated_java.dialog.blueprint_settings.display_item.warning.item_does_not_exist: The selected item does not exist in vanilla!
+
+animated_java.dialog.blueprint_settings.custom_model_data_offset.title: CMD Offset
+animated_java.dialog.blueprint_settings.custom_model_data_offset.description: The offset to use for the Custom Model Data of the Display Item. Allows multiple Blueprints on the same item, but in separate, unaffiliated Resource Packs.
+
+animated_java.dialog.blueprint_settings.resource_pack.title: Resource Pack
+animated_java.dialog.blueprint_settings.resource_pack.description: The root folder of the Resource Pack to export the project into.
+animated_java.dialog.blueprint_settings.resource_pack.error.no_folder_selected: No folder selected!
+animated_java.dialog.blueprint_settings.resource_pack.error.folder_does_not_exist: The selected folder does not exist!
+animated_java.dialog.blueprint_settings.resource_pack.error.not_a_folder: The selected path is not a folder!
+animated_java.dialog.blueprint_settings.resource_pack.error.missing_pack_mcmeta: The selected folder is missing a pack.mcmeta file!
+animated_java.dialog.blueprint_settings.resource_pack.error.missing_assets_folder: The selected Resource Pack is missing an assets folder!
+
+animated_java.dialog.blueprint_settings.display_item_path.title: Display Item Path
+animated_java.dialog.blueprint_settings.display_item_path.description: Where to place the Display Item. This should be a path to a .json file in a Resource Pack.
+
+animated_java.dialog.blueprint_settings.model_folder.title: Model Folder
+animated_java.dialog.blueprint_settings.model_folder.description: Where to place all of the exported models. This should be a path to a folder in a Resource Pack.
+
+animated_java.dialog.blueprint_settings.texture_folder.title: Texture Folder
+animated_java.dialog.blueprint_settings.texture_folder.description: Where to place all of the exported textures. This should be a path to a folder in a Resource Pack.
+
+# Data Pack Settings
+animated_java.dialog.blueprint_settings.data_pack_settings.title: Data Pack Settings
+
+animated_java.dialog.blueprint_settings.enable_advanced_data_pack_settings.title: Use Advanced Settings
+animated_java.dialog.blueprint_settings.enable_advanced_data_pack_settings.description: Whether or not to enable the advanced Data Pack settings.
+
+animated_java.dialog.blueprint_settings.data_pack.title: Data Pack
+animated_java.dialog.blueprint_settings.data_pack.description: The root folder of the Data Pack to export the project into.
+animated_java.dialog.blueprint_settings.data_pack.error.no_folder_selected: No folder selected!
+animated_java.dialog.blueprint_settings.data_pack.error.folder_does_not_exist: The selected folder does not exist!
+animated_java.dialog.blueprint_settings.data_pack.error.not_a_folder: The selected path is not a folder!
+animated_java.dialog.blueprint_settings.data_pack.error.missing_pack_mcmeta: The selected folder is missing a pack.mcmeta file!
+animated_java.dialog.blueprint_settings.data_pack.error.missing_data_folder: The selected Data Pack is missing a data folder!
+
+animated_java.dialog.blueprint_settings.summon_commands.title: On-Summon Commands
+animated_java.dialog.blueprint_settings.summon_commands.description: Commands to run as the root entity when summoned. Treat this text input as a normal mcfunction.
+
+animated_java.dialog.blueprint_settings.interpolation_duration.title: Interp Duration
+animated_java.dialog.blueprint_settings.interpolation_duration.description: The duration of the smoothing between keyframes in ticks. This is the time it takes for the model to transition from one keyframe to the next. Higher values will cause animations to lose precision. Generally, you want this to have a value of 1 or 2.
+
+animated_java.dialog.blueprint_settings.teleportation_duration.title: Teleport Duration
+animated_java.dialog.blueprint_settings.teleportation_duration.description: The duration of the teleportation between keyframes in ticks. This is the time over which the model will visually interpolate between it's old position to it's new position. Higher values will cause animations to lose precision.
+
+animated_java.dialog.blueprint_settings.use_storage_for_animation.title: Use Storage for Animation
+animated_java.dialog.blueprint_settings.use_storage_for_animation.description: Whether or not to use NBT storage to store animation data instead of functions. This will vastly reduce the number of functions in the generated Data Pack, but is 33% slower than the function method.
+
+## Bone Config Dialog
+animated_java.dialog.bone_config.title: Bone Config
+
+animated_java.dialog.bone_config.use_nbt.title: Use NBT
+animated_java.dialog.bone_config.use_nbt.description: Whether or not to use NBT to configure the bone instead of settings.
+animated_java.dialog.bone_config.use_nbt.use_nbt_warning: Using NBT will override all other settings, and any changes you make will not be visible in the editor. Only use this if you know what you're doing!
+
+animated_java.dialog.bone_config.inherit_settings.title: Inherit Settings
+animated_java.dialog.bone_config.inherit_settings.description: Whether or not to inherit the settings from the parent bone.
+
+animated_java.dialog.bone_config.glowing.title: Glowing
+animated_java.dialog.bone_config.glowing.description: Whether or not the bone should glow in-game.
+
+animated_java.dialog.bone_config.override_glow_color.title: Override Glow Color
+animated_java.dialog.bone_config.override_glow_color.description: Whether or not to override the default glow color.
+
+animated_java.dialog.bone_config.glow_color.title: Glow Color
+animated_java.dialog.bone_config.glow_color.description: The color of the glow.
+
+animated_java.dialog.bone_config.shadow_radius.title: Shadow Radius
+animated_java.dialog.bone_config.shadow_radius.description: The radius of the shadow.
+
+animated_java.dialog.bone_config.shadow_strength.title: Shadow Strength
+animated_java.dialog.bone_config.shadow_strength.description: The strength of the shadow.
+
+animated_java.dialog.bone_config.override_brightness.title: Override Brightness
+animated_java.dialog.bone_config.override_brightness.description: Whether or not to override the default brightness.
+
+animated_java.dialog.bone_config.brightness_override.title: Brightness
+animated_java.dialog.bone_config.brightness_override.description: The brightness of the bone. This should be a value between 0 and 15.
+
+animated_java.dialog.bone_config.enchanted.title: Enchanted
+animated_java.dialog.bone_config.enchanted.description: Whether or not the bone should be enchanted.
+
+animated_java.dialog.bone_config.invisible.title: Invisible
+animated_java.dialog.bone_config.invisible.description: Whether or not the bone should be invisible.
+
+animated_java.dialog.bone_config.nbt.title: NBT
+animated_java.dialog.bone_config.nbt.description: The NBT to apply to the bone.
+
+animated_java.dialog.bone_config.billboard.title: Billboard
+animated_java.dialog.bone_config.billboard.description: Controls if this bone should pivot to face player when rendered. It can be fixed (both vertical and horizontal angles are fixed), vertical (faces player around vertical axis), horizontal (pivots around horizontal axis), and center (pivots around center point).
+animated_java.dialog.bone_config.billboard.options.fixed: Fixed
+animated_java.dialog.bone_config.billboard.options.vertical: Vertical
+animated_java.dialog.bone_config.billboard.options.horizontal: Horizontal
+animated_java.dialog.bone_config.billboard.options.center: Center
+
+## Locator Config Dialog
+animated_java.dialog.locator_config.title: Locator Config
+
+animated_java.dialog.locator_config.plugin_mode_warning: |-
+ Plugin Mode is enabled! Locators have no configuration in Plugin Mode.
+ Instead, use the Plugin API to add custom functionality to your Locators.
+ For more information, see the Official Plugin API documentation for more information.
+
+animated_java.dialog.locator_config.use_entity.title: Use Entity
+animated_java.dialog.locator_config.use_entity.description: Whether or not to attach an entity to the Locator.
+
+animated_java.dialog.locator_config.entity_type.title: Entity Type
+animated_java.dialog.locator_config.entity_type.description: The type of entity to attach to the Locator.
+animated_java.dialog.locator_config.entity_type.error.empty: Entity Type cannot be empty!
+animated_java.dialog.locator_config.entity_type.warning.invalid: The selected entity type doesn't exist in Minecraft {0}
+
+animated_java.dialog.locator_config.summon_commands.title: On-Summon Commands
+animated_java.dialog.locator_config.summon_commands.description: Commands to run as the Locator's entity when summoned. Treat this text input as a normal mcfunction.
+
+animated_java.dialog.locator_config.ticking_commands.title: Ticking Commands
+animated_java.dialog.locator_config.ticking_commands.description: |-
+ Commands to run every tick at the Locator's position. Treat this text input as a normal mcfunction.
+ NOTE: These commands will only run while an animation is playing.
+
+## Text Display Config Dialog
+animated_java.dialog.text_display_config.title: Text Display Config
+
+animated_java.dialog.bone_config.vanilla_item_model.title: Vanilla Item Model
+animated_java.dialog.bone_config.vanilla_item_model.description: |-
+ If set, the bone will render as a vanilla item model.
+ This will overwrite the bone's existing cubes.
+
+animated_java.dialog.text_display_config.use_nbt.title: Use NBT
+animated_java.dialog.text_display_config.use_nbt.description: Whether or not to use NBT to configure the text display instead of settings.
+animated_java.dialog.text_display_config.use_nbt.use_nbt_warning: Using NBT will override all other settings, and any changes you make will not be visible in the editor. Only use this if you know what you're doing!
+
+animated_java.dialog.text_display_config.inherit_settings.title: Inherit Settings
+animated_java.dialog.text_display_config.inherit_settings.description: Whether or not to inherit the settings from the parent text display.
+
+animated_java.dialog.text_display_config.glowing.title: Glowing
+animated_java.dialog.text_display_config.glowing.description: Whether or not the text display should glow in-game.
+
+animated_java.dialog.text_display_config.override_glow_color.title: Override Glow Color
+animated_java.dialog.text_display_config.override_glow_color.description: Whether or not to override the default glow color.
+
+animated_java.dialog.text_display_config.glow_color.title: Glow Color
+animated_java.dialog.text_display_config.glow_color.description: The color of the glow.
+
+animated_java.dialog.text_display_config.shadow_radius.title: Shadow Radius
+animated_java.dialog.text_display_config.shadow_radius.description: The radius of the shadow.
+
+animated_java.dialog.text_display_config.shadow_strength.title: Shadow Strength
+animated_java.dialog.text_display_config.shadow_strength.description: The strength of the shadow.
+
+animated_java.dialog.text_display_config.override_brightness.title: Override Brightness
+animated_java.dialog.text_display_config.override_brightness.description: Whether or not to override the default brightness.
+
+animated_java.dialog.text_display_config.brightness_override.title: Brightness
+animated_java.dialog.text_display_config.brightness_override.description: The brightness of the text display. This should be a value between 0 and 15.
+
+animated_java.dialog.text_display_config.invisible.title: Invisible
+animated_java.dialog.text_display_config.invisible.description: Whether or not the bone should be invisible.
+
+animated_java.dialog.text_display_config.nbt.title: NBT
+animated_java.dialog.text_display_config.nbt.description: The NBT to apply to the text display.
+
+animated_java.dialog.text_display_config.billboard.title: Billboard
+animated_java.dialog.text_display_config.billboard.description: Controls if this text display should pivot to face player when rendered. It can be fixed (both vertical and horizontal angles are fixed), vertical (faces player around vertical axis), horizontal (pivots around horizontal axis), and center (pivots around center point).
+animated_java.dialog.text_display_config.billboard.options.fixed: Fixed
+animated_java.dialog.text_display_config.billboard.options.vertical: Vertical
+animated_java.dialog.text_display_config.billboard.options.horizontal: Horizontal
+animated_java.dialog.text_display_config.billboard.options.center: Center
+
+## Variant Config Dialog
+animated_java.dialog.variant_config.title: Variant Config
+
+animated_java.dialog.variant_config.variant_display_name: Display Name
+animated_java.dialog.variant_config.variant_display_name.description: Used to identify the Variant in the editor, and in error messages.
+
+animated_java.dialog.variant_config.generate_name_from_display_name: Generate Name From Display Name
+animated_java.dialog.variant_config.generate_name_from_display_name.description: Whether or not to generate the Name automatically from the Display Name.
+
+animated_java.dialog.variant_config.variant_name: Name
+animated_java.dialog.variant_config.variant_name.description: Used to identify the Variant in the exported Resource Pack and Data Pack.
+
+animated_java.dialog.variant_config.texture_map.title: Texture Map
+animated_java.dialog.variant_config.texture_map.description: A map of which textures to swap out when applying this Variant.
+animated_java.dialog.variant_config.texture_map.create_new_mapping: Create New Mapping
+animated_java.dialog.variant_config.texture_map.no_mappings: Variant has no mapped textures.
+
+animated_java.dialog.variant_config.bone_lists.description: A list of bones to include or exclude from the Variant. Only bones in the included list will be modified by the Variant, and bones in the excluded list will be ignored.
+animated_java.dialog.variant_config.excluded_bones.title: Excluded Bones
+animated_java.dialog.variant_config.excluded_bones.description: A list of bones to exclude from the Variant. These bones will not be modified by the Variant.
+animated_java.dialog.variant_config.included_bones.title: Included Bones
+animated_java.dialog.variant_config.included_bones.description: A list of bones to include in the Variant. Only these bones will be modified by the Variant.
+animated_java.dialog.variant_config.swap_columns_button.tooltip: Swap Lists
+
+## Old AJModel Loader Dialog
+animated_java.action.upgrade_old_aj_model_loader.name: Update .ajmodel
+animated_java.dialog.upgrade_old_aj_model_loader.title: Update .ajmodel
+animated_java.action.upgrade_old_aj_model_loader.select_file: Select .ajmodel File
+animated_java.action.upgrade_old_aj_model_loader.body: Upgrade your outdated .ajmodel files to the new .ajblueprint format.
+animated_java.action.upgrade_old_aj_model_loader.button: Select an .ajmodel File to Upgrade
+
+## Animation Properties Dialog
+animated_java.dialog.animation_properties.title: Animation Properties ({0})
+
+animated_java.dialog.animation_properties.animation_name.title: Animation Name
+animated_java.dialog.animation_properties.animation_name.description: The name of the animation.
+
+animated_java.dialog.animation_properties.loop_mode.title: Loop Mode
+animated_java.dialog.animation_properties.loop_mode.description: Determines how the animation should loop. Once - The animation will play once and stop. Hold - The animation will play once and hold the last frame. Loop - The animation will loop indefinitely.
+animated_java.dialog.animation_properties.loop_mode.options.once: Once
+animated_java.dialog.animation_properties.loop_mode.options.hold: Hold
+animated_java.dialog.animation_properties.loop_mode.options.loop: Loop
+animated_java.dialog.animation_properties.animation_name.error.empty: The animation name cannot be empty!
+animated_java.dialog.animation_properties.animation_name.error.invalid_characters: The animation name contains invalid characters! Animation names should only contain letters, numbers, underscores, and periods.
+
+animated_java.dialog.animation_properties.bone_lists.description: A list of bones to include or exclude from the animation. Only bones in the included list will be modified by the animation, and bones in the excluded list will be ignored.
+animated_java.dialog.animation_properties.excluded_bones.title: Excluded Bones
+animated_java.dialog.animation_properties.excluded_bones.description: A list of bones to exclude from the animation. These bones will not be modified by the animation.
+animated_java.dialog.animation_properties.included_bones.title: Included Bones
+animated_java.dialog.animation_properties.included_bones.description: A list of bones to include in the animation. Only these bones will be modified by the animation.
+animated_java.dialog.animation_properties.swap_columns_button.tooltip: Swap Lists
+
+## Export Progress Dialog
+animated_java.dialog.export_progress.title: Exporting...
+
+### Panels
+
+## Variants Panel
+animated_java.panel.variants.title: Variants
+animated_java.panel.variants.tool.create_new_variant: Create New Variant
+animated_java.panel.variants.tool.edit_variant: Edit Variant
+animated_java.panel.variants.tool.duplicate_selected_variant: Duplicate Selected Variant
+animated_java.panel.variants.tool.delete_selected_variant: Delete Selected Variant
+animated_java.panel.variants.tool.variant_visible: Variant Selected
+animated_java.panel.variants.tool.variant_not_visible: Variant Not Selected
+
+animated_java.action.variants.create: Create Variant
+animated_java.action.variants.duplicate: Duplicate Variant
+animated_java.action.variants.open_config: Open Variant Config
+animated_java.action.variants.delete: Delete Variant
+
+### Animator
+
+## Properties
+animated_java.animation.excluded_bones: Excluded Bones
+animated_java.animation.invert_excluded_bones: Invert Excluded Bones
+
+## Timeline
+animated_java.effect_animator.timeline.variant: Variant
+animated_java.effect_animator.timeline.commands: Commands
+
+## Keyframes
+animated_java.effect_animator.keyframes.variant: Variant
+animated_java.effect_animator.keyframes.commands: Commands
+animated_java.effect_animator.keyframes.execute_condition: Execute Condition
+
+# Keyframe Panel
+animated_java.panel.keyframe.keyframe_title: Keyframe ({0})
+
+animated_java.panel.keyframe.variant.title: Variant
+animated_java.panel.keyframe.variant.description: The Variant to apply to the keyframe.
+
+animated_java.panel.keyframe.commands.title: Commands
+animated_java.panel.keyframe.commands.description: Commands to run when the keyframe is reached. Treat this text input as a normal mcfunction.
+
+animated_java.panel.keyframe.execute_condition.title: Execute Condition
+animated_java.panel.keyframe.execute_condition.description: A condition that must be met for the keyframe to execute. Treat this text input as the sub-commands of an execute command.
+
+animated_java.panel.keyframe.repeat.title: Repeat?
+animated_java.panel.keyframe.repeat.description: |-
+ Whether or not to run the commands in this keyframe repeatedly.
+ If enabled, the commands will run every Repeat Frequency ticks.
+
+animated_java.panel.keyframe.repeat_frequency.title: Repeat Frequency
+animated_java.panel.keyframe.repeat_frequency.description: |-
+ How many ticks to wait before running the commands in this keyframe again.
+ Setting this to 1 will run the commands every tick.
+ NOTE: If the animation is not evenly divisible by this value, the interval may be slightly off when the animation loops.
+ If this is set to a value larger than the length of the animation, the commands will run as if repeat was disabled (Once when the keyframe is reached).
+
+# Text Display Panel
+animated_java.panel.text_display.title: Text Display
+
+animated_java.tool.text_display.line_width.title: Line Width
+animated_java.tool.text_display.line_width.description: The width of the text display in pixels.
+
+animated_java.tool.text_display.background_color.title: Background Color
+animated_java.tool.text_display.background_color.description: The color of the background of the text display.
+
+animated_java.tool.text_display.text_shadow.title: Text Shadow
+animated_java.tool.text_display.text_shadow.description: Whether or not to display a shadow behind the text.
+
+# Vanilla Item Display Panel
+animated_java.panel.vanilla_item_display.title: Displayed Item
+animated_java.panel.vanilla_item_display.description: The item to display.
+
+# Vanilla Block Display Panel
+animated_java.panel.vanilla_block_display.title: Displayed Block
+animated_java.panel.vanilla_block_display.description: The block to display. Supports block states!
+
+### Custom Elements
+## Vanilla Item Display
+animated_java.vanilla_item_display.title: Vanilla Item Display
+
+## Vanilla Block Display
+
+### Misc
+
+# Blueprint Setting Errors - Failed to Export Message Box
+animated_java.misc.failed_to_export.title: Failed to Export
+animated_java.misc.failed_to_export.custom_models.message: You have disabled resource pack exporting, but have custom models in your project! Please enable resource pack exporting or remove the custom models before exporting.
+animated_java.misc.failed_to_export.blueprint_settings.message: There are errors in your blueprint settings! Please fix them before exporting.
+animated_java.misc.failed_to_export.blueprint_settings.error_item: 'Found an issue with {0}:'
+animated_java.misc.failed_to_export.button: Ok
+
+# Format Category
+animated_java.format_category.animated_java: Animated Java
+
+# Model Manager Warnings
+animated_java.block_model_manager.mob_head_warning: Mob Heads are don't render in Block Displays. Use an Item Display instead.
diff --git a/src/lang/zh_cn.yaml b/src/lang/zh_cn.yaml
deleted file mode 100644
index 7f1f21b3..00000000
--- a/src/lang/zh_cn.yaml
+++ /dev/null
@@ -1,364 +0,0 @@
-animated_java.title: Animated Java
-
-animated_java.menubar.settings: Animated Java
-
-animated_java.menubar.items.about: 关于
-animated_java.menubar.items.settings: 设置
-animated_java.menubar.items.project_settings: 项目设置
-animated_java.menubar.items.documentation: 文档
-animated_java.menubar.items.export_project: 项目导出
-animated_java.menubar.items.bone_config: 骨骼配置
-animated_java.menubar.items.camera_config: 镜头配置
-animated_java.menubar.items.locator_config: 定位器配置
-
-animated_java.quickmessage.exported_successfully: 项目导出成功!
-
-### Dialogs
-animated_java.dialog.close_button: 确认
-
-### About
-animated_java.dialog.about.title: 关于 Animated Java
-
-### Export in Progress
-animated_java.dialog.export_in_progress.title: 项目导出中...
-
-### Settings
-animated_java.settings.accessability_options_group: 可访问性
-
-animated_java.dialog.settings.title: Animated Java 设置
-animated_java.settings.reduced_motion: 动画减弱
-animated_java.settings.reduced_motion.description: |-
- 关闭 Animated Java 的菜单动画。
- 将禁用可能引起眩晕的动画和其他特效。
-
-animated_java.settings.resource_pack_group: 资源包
-
-animated_java.settings.minify_output: 简化输出
-animated_java.settings.minify_output.description: |-
- 简化资源包的输出。
- 移除输出的备注和空格。
- 输出的文件会更小,但会降低可读性。
-
-### Documentation
-animated_java.dialog.documentation.title: Animated Java 文档
-
-animated_java.dialog.documentation.loading: 文档加载中…
-
-animated_java.dialog.documentation.error.failed_to_load.title: 文档加载失败! :(
-animated_java.dialog.documentation.error.failed_to_load.description: |-
- 确保你的网络连接正常!
-
-### Project Settings
-animated_java.dialog.project_settings.title: Animated Java 项目设置
-
-animated_java.dialog.project_settings.project_group: 项目
-
-animated_java.project_settings.project_namespace: 项目名称
-animated_java.project_settings.project_namespace.description: |-
- 项目的名称和命名空间。
-animated_java.project_settings.project_namespace.error.unset: |-
- 项目名称不能为空
-
-animated_java.project_settings.project_resolution: 纹理尺寸
-animated_java.project_settings.project_resolution.description: |-
- 项目的纹理分辨率。
- 应该与该项目中最大的纹理尺寸相同。
-
-animated_java.project_settings.target_minecraft_version: Minecraft版本
-animated_java.project_settings.target_minecraft_version.description: |-
- 生成的数据包所对应的Minecraft版本。
-
-animated_java.project_settings.resourcepack_group: 资源包
-
-animated_java.project_settings.rig_item: 骨骼物品
-animated_java.project_settings.rig_item.description: |-
- 显示模型时所使用的物品。
-animated_java.project_settings.rig_item.error.unset: |-
- 骨骼物品不能为空
-animated_java.project_settings.rig_item.error.space: |-
- 骨骼物品必须为有效的物品ID
- 物品ID不能包含空格。
-animated_java.project_settings.rig_item.error.invalid_namespace: |-
- 骨骼物品必须为有效的物品ID
- 物品ID必须包含命名空间。
-animated_java.project_settings.rig_item.warning.unknown_item: |-
- 非原版的骨骼物品
- 可能会在导出时出现问题。
- 如果为快照或模组的物品,请忽略该警告。
-
-animated_java.project_settings.enable_advanced_resource_pack_settings: 启用高级资源包设置
-animated_java.project_settings.enable_advanced_resource_pack_settings.description: |-
- 启用高级资源包设置。
- 可以手动设置骨骼模型所使用的物品和文件导出路径,而非AJ自动设置。
- 如果不清楚这是做什么的,请禁用该项。
-
-animated_java.project_settings.rig_item_model: 骨骼物品模型
-animated_java.project_settings.rig_item_model.description: |-
- 模型的导出路径。
- 该路径应位于有效的资源包的 'minecraft:models/item' 之下。
-animated_java.project_settings.rig_item_model.error.unset: |-
- 骨骼物品模型不能为空。
-animated_java.project_settings.rig_item_model.error.invalid_path: |-
- 骨骼物品模型必须保存于有效的资源包内
- 该文件必须位于 'resources/assets//models/' 之内。
-animated_java.project_settings.rig_item_model.error.item_does_not_match: |-
- 骨骼物品模型的名称必须与所使用的骨骼物品中定义的名称相同
- 当前的骨骼物品为 "%rigItem",但所使用的骨骼物品指向的目标为 "%pathItem"。
-animated_java.project_settings.rig_item_model.error.rig_item_unset: |-
- 未设置骨骼物品
- 请在设置骨骼物品后设置骨骼物品模型。
-
-animated_java.project_settings.rig_export_folder: 骨骼导出文件夹
-animated_java.project_settings.rig_export_folder.description: |-
- 生成的模型导出时所存放到的文件夹。
- 应位于有效的资源包之内。
-animated_java.project_settings.rig_export_folder.error.unset: |-
- 骨骼导出文件夹不能为空
-animated_java.project_settings.rig_export_folder.error.invalid_path: |-
- 骨骼所导出到的文件夹必须位于有效的资源包之内
- 指向的目标文件夹必须位于 'resources/assets//models/' 之内。
-
-animated_java.project_settings.texture_export_folder: 纹理导出文件夹
-animated_java.project_settings.texture_export_folder.description: |-
- 生成的纹理导出时所存放到的文件夹。
- 应位于有效的资源包之内。
-animated_java.project_settings.texture_export_folder.error.unset: |-
- 纹理导出文件夹不能为空
-animated_java.project_settings.texture_export_folder.error.invalid_path: |-
- 纹理所导出到的文件夹必须位于有效的资源包之内
- 指向的目标文件夹必须位于 'resources/assets//textures/' 之内。
-
-animated_java.project_settings.resource_pack_mcmeta: 资源包
-animated_java.project_settings.resource_pack_mcmeta.description: |-
- 存放骨骼模型的资源包。
- 该设置应指向有效的资源包中的 pack.mcmeta 文件。
-animated_java.project_settings.resource_pack_mcmeta.error.unset: |-
- 必须选择一个资源包。
-animated_java.project_settings.resource_pack_mcmeta.error.invalid_path: |-
- 所选的资源包无效!
- 确保选择的是 pack.mcmeta 文件,且资源包中存在 assets 文件夹。
-
-animated_java.project_settings.verbose: 详细
-animated_java.project_settings.verbose.description: |-
- 是否在运行AJ函数时在聊天栏输出详细信息。
- 有助于进行调试。
-
-animated_java.project_settings.exporter_settings: '%exporter Settings'
-
-animated_java.project_settings.exporter: 导出器
-animated_java.project_settings.exporter.description: |-
- 此项目所使用的导出器。
-
-### Bone Config
-animated_java.dialog.bone_config: Animated Java 骨骼配置
-animated_java.bone_config.nbt: 骨骼实体 NBT
-animated_java.bone_config.nbt.description: |-
- 应用到该骨骼实体的自定义NBT。
- 但该数据包所使用到的部分内部NBT标签不可被自定义。
-
-### Camera Config
-animated_java.dialog.camera_config: Animated Java 镜头配置
-
-animated_java.camera_config.entity_type: 实体类型
-animated_java.camera_config.entity_type.description: |-
- 生成镜头时所使用的实体类型。
- 比如,当设置为 minecraft:armor_stand 时,游戏中生成的相机即为一个盔甲架。
-animated_java.camera_config.entity_type.error.space: |-
- 实体ID不能包含空格。
-animated_java.camera_config.entity_type.error.invalid_namespace: |-
- 实体ID必须包含命名空间。
-animated_java.camera_config.entity_type.warning.unknown_entity: |-
- 非原版的实体ID
- 可能会在导出时出现问题。
- 如果为快照或模组的ID,请忽略该警告。
-
-animated_java.camera_config.nbt: NBT
-animated_java.camera_config.nbt.description: |-
- 应用到镜头实体的自定义NBT。
- 但该数据包所使用到的部分内部NBT标签不可被自定义。
-
-### Locator Config
-animated_java.dialog.locator_config: Animated Java 定位器配置
-
-animated_java.locator_config.entity_type: 实体类型
-animated_java.locator_config.entity_type.description: |-
- 生成定位器时所使用的实体类型。
- 比如,当设置为 minecraft:pig 时,游戏中生成的定位器即为一头猪。
-animated_java.locator_config.entity_type.error.space: |-
- 实体ID不能包含空格。
-animated_java.locator_config.entity_type.error.invalid_namespace: |-
- 实体ID必须包含命名空间。
-animated_java.locator_config.entity_type.warning.unknown_entity: |-
- 非原版的实体ID
- 可能会在导出时出现问题。
- 如果为快照或模组的ID,请忽略该警告。
-
-animated_java.locator_config.nbt: NBT
-animated_java.locator_config.nbt.description: |-
- 应用到定位实体的自定义NBT。
- 但该数据包所使用到的部分内部NBT标签不可被自定义。
-
-### Animation Config
-animated_java.dialog.animation_config.title: 动画属性
-
-animated_java.animation_config.animation_name: 名称
-animated_java.animation_config.animation_name.description: |-
- 动画的名称。
-animated_java.animation_config.animation_name.error.duplicate_name: |-
- 已存在名为 "%name" 的动画。
- 动画名称必须是唯一的。
-
-animated_java.animation_config.loop: 循环模式
-animated_java.animation_config.loop.description: |-
- 动画的循环模式。
- 单次:动画仅播放一次,并在播放完毕时立刻重置。
- 循环:动画将循环播放。
- 保持:动画将在播放完毕时保持在最后一帧。
-animated_java.animation_config.loop.options.once: 单次播放
-animated_java.animation_config.loop.options.loop: 无限循环
-animated_java.animation_config.loop.options.hold: 单次结尾
-
-animated_java.animation_config.loop_delay: 循环延迟
-animated_java.animation_config.loop_delay.description: |-
- 动画在下一次循环播放前的延时(以刻为单位)。
- 仅在循环模式为循环时适用。
-
-animated_java.animation_config.start_delay: 开始延迟
-animated_java.animation_config.start_delay.description: |-
- 动画在play函数运行多久后开始播放(以刻为单位)。
-
-animated_java.animation_config.affected_bones_is_a_whitelist: 白名单骨骼
-animated_java.animation_config.affected_bones_is_a_whitelist.description: |-
- 勾选时,该动画仅影响白名单中的骨骼。
- 未勾选,则仅会影响白名单以外的骨骼。
-
-animated_java.animation_config.affected_bones: 影响的骨骼
-animated_java.animation_config.affected_bones.add_new_item_message: 添加骨骼
-animated_java.animation_config.affected_bones.description: |-
- 将会被该动画影响到的骨骼。
- 当白名单被勾选时,该动画仅应用于名单中的骨骼。
- 当白名单未被勾选,该动画会应用于所有不在名单中的骨骼。
-
-### Variants Panel
-animated_java.panels.variants.name: 变体
-animated_java.panels.variants.items: 变体
-animated_java.panels.variants.delete_default_variant: 不允许删除默认变体
-animated_java.panels.variants.delete_variant: 删除变体
-animated_java.panels.variants.edit_variant: 编辑变体
-animated_java.panels.variants.default_variant: 默认变体
-animated_java.actions.add_variant.name: 添加变体
-animated_java.actions.add_variant.description: 创建新变体
-animated_java.actions.variant_properties.name: 变体属性
-animated_java.actions.variant_properties.description: 打开变体属性窗口。
-
-### Variants Properties Dialog
-animated_java.dialog.variant_properties.title: 变体属性
-
-animated_java.dialog.variant_properties.variant_name: 名称
-animated_java.dialog.variant_properties.variant_name.description: |-
- 该变体的名称。
-animated_java.variant_properties.variant_name.error.duplicate_name: |-
- 已存在名为 "%name" 的变体。
- 变体名称必须是唯一的。
-
-animated_java.dialog.variant_properties.affected_bones_is_a_whitelist: 白名单骨骼
-animated_java.dialog.variant_properties.affected_bones_is_a_whitelist.description: |-
- 受影响的骨骼为白名单还是黑名单。
- 勾选时,该变体仅影响白名单中的骨骼。
- 未勾选,则仅会影响白名单以外的骨骼。
-
-animated_java.dialog.variant_properties.affected_bones: 影响的骨骼
-animated_java.dialog.variant_properties.affected_bones.add_new_item_message: 添加骨骼
-animated_java.dialog.variant_properties.affected_bones.description: |-
- 将会被该变体影响到的骨骼。
- 当白名单被勾选时,该变体仅应用于名单中的骨骼。
- 当白名单未被勾选,该变体会应用于所有不在名单中的骨骼。
-animated_java.dialog.variant_properties.textureMap: 纹理贴图
-animated_java.dialog.variant_properties.textureMap.description: |-
- 该变体所使用的纹理贴图。
- 选择该变体适用时所替换的纹理。
-
-### Custom Keyframes
-animated_java.keyframe.animation: 动画
-animated_java.keyframe.animation.description: |-
- 到达该关键帧时所播放的动画。
-
-animated_java.keyframe.variant: 变体
-animated_java.keyframe.variant.description: |-
- 到达该关键帧时所应用的变体。
-
-animated_java.keyframe.commands: 命令
-animated_java.keyframe.commands.description: |-
- 到达该关键帧时所运行的(函数中的)命令。
-
-animated_java.keyframe.tweenDuration: 补间时长
-animated_java.keyframe.tweenDuration.description: |-
- 该帧到下一个动画之间的补间时长。(刻为单位)
-
-animated_java.keyframe.tweenMode: 补间模式
-animated_java.keyframe.tweenMode.description: |-
- 关键帧的补间模式。
- 播放:动画将会重新播放。
- 恢复:动画将从与此帧相同的 anim_time 开始播放。
-animated_java.keyframe.tweenMode.play: 播放
-animated_java.keyframe.tweenMode.resume: 恢复
-
-animated_java.keyframe.executeCondition: 执行条件
-animated_java.keyframe.executeCondition.description: |-
- 关键帧适用时所必须满足的 execute 命令条件。
- 此处填写 execute 子命令串。
- 示例:"if score @s example matches 1.."
- 提示:可通过换行分隔子命令!
-
-animated_java.timeline.animation: 动画
-animated_java.timeline.variant: 变体
-animated_java.timeline.commands: 命令
-
-### Popups
-animated_java.popup.close_button: 关闭
-animated_java.popup.confirm_button: 确认
-animated_java.popup.cancel_button: 好的
-
-# Unexpected Error
-animated_java.popup.unexpectedError.title: 未知错误
-animated_java.popup.unexpectedError.body: |-
- 发生了未知的错误!
- 请在我们的 Github 页面,或官方 Discord 的求助频道上进行汇报。
-
-# Confirm predicate file overwrite
-animated_java.popup.confirm_predicate_file_overwrite.title: 确认覆盖 Predicate 文件
-animated_java.popup.confirm_predicate_file_overwrite.body: |-
- 已存在名为 "%file" 的文件,且并非骨骼物品模型!
- 选择对其进行覆盖吗?
-
- 完整文件路径:
- %path
-
-# Invalid Texture Mapping(s)
-animated_java.popup.invalid_texture_mapping.title: 无效的纹理贴图
-animated_java.popup.invalid_texture_mapping.body: |-
- 变体 "%variant" 存在无效的纹理贴图!
- 项目中存在一个或以上丢失或无效的纹理。
- 无效的纹理和无效的原因如下:
-animated_java.popup.invalid_texture_mapping.reason: 无效的原因
-animated_java.popup.invalid_texture_mapping.reason.invalid_from_texture: |-
- 项目中并未找到 "from" 纹理。
-animated_java.popup.invalid_texture_mapping.reason.invalid_to_texture: |-
- 项目中并未找到 "to" 纹理。
-animated_java.popup.invalid_texture_mapping.footer: |-
- 变体中丢失或无效的纹理贴图将会被移除。
- 关闭该窗口后,请打开变体属性窗口并检测纹理贴图是否正确。
-
-# Invalid Cube(s)
-animated_java.popup.invalid_cubes.title: 无效方块
-animated_java.popup.invalid_cubes.body: |-
- 部分方块存在非法的旋转!
- 根据所处的骨骼排序,以下为无效的方块。
- 关闭该窗口后,它们将会在3D视图中被高亮显示。
-
-# Failed Project Export Readiness
-animated_java.popup.failed_project_export_readiness.title: 导出失败
-animated_java.popup.failed_project_export_readiness.body: |-
- 该项目尚未准备好导出!
- 找到原因如下:
-animated_java.popup.failed_project_export_readiness.issue: 项目设置 "%s" 存在以下错误
\ No newline at end of file
diff --git a/src/minecraft/entities.ts b/src/minecraft/entities.ts
deleted file mode 100644
index f01b59ef..00000000
--- a/src/minecraft/entities.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { registry } from './registryLoader'
-
-export class Entities {
- static list: string[] = []
-
- static isEntity(name: string) {
- return Entities.list.includes(name)
- }
-}
-
-registry
- .then(v => {
- Entities.list = v.entity_type.map(v => `minecraft:${v}`)
- })
- .catch(e => {
- console.error(e)
- })
diff --git a/src/minecraft/index.ts b/src/minecraft/index.ts
deleted file mode 100644
index 88525cac..00000000
--- a/src/minecraft/index.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-// @ts-ignore
-import fontUrl from '../assets/MinecraftFull.ttf'
-
-if ([...document.fonts.keys()].filter(v => v.family === 'MinecraftFull').length === 0) {
- void new FontFace('MinecraftFull', fontUrl as Buffer, {}).load().then(font => {
- document.fonts.add(font)
- })
-}
-
-export const PIXEL_FILTER =
- 'url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxmaWx0ZXIgaWQ9ImZpbHRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj48ZmVDb21wb25lbnRUcmFuc2Zlcj48ZmVGdW5jUiB0eXBlPSJpZGVudGl0eSIvPjxmZUZ1bmNHIHR5cGU9ImlkZW50aXR5Ii8+PGZlRnVuY0IgdHlwZT0iaWRlbnRpdHkiLz48ZmVGdW5jQSB0eXBlPSJkaXNjcmV0ZSIgdGFibGVWYWx1ZXM9IjAgMSIvPjwvZmVDb21wb25lbnRUcmFuc2Zlcj48L2ZpbHRlcj48L3N2Zz4=#filter)'
-
-export const FONT = '16px MinecraftFull'
-
-export * from './entities'
-export * from './items'
-export * from './util'
-export * from './jsonText'
-export * from './searchGenerator'
-// export * from './registryLoader'
diff --git a/src/minecraft/items.ts b/src/minecraft/items.ts
deleted file mode 100644
index a4b31436..00000000
--- a/src/minecraft/items.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { registry } from './registryLoader'
-
-export class Items {
- static list: string[] = []
-
- static isItem(name: string) {
- return Items.list.includes(name)
- }
-}
-
-registry
- .then(v => {
- Items.list = v.item.map(v => `minecraft:${v}`)
- })
- .catch(e => {
- console.error(e)
- })
diff --git a/src/minecraft/jsonText.ts b/src/minecraft/jsonText.ts
deleted file mode 100644
index b49465d3..00000000
--- a/src/minecraft/jsonText.ts
+++ /dev/null
@@ -1,246 +0,0 @@
-const PIXEL_FILTER =
- 'url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxmaWx0ZXIgaWQ9ImZpbHRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj48ZmVDb21wb25lbnRUcmFuc2Zlcj48ZmVGdW5jUiB0eXBlPSJpZGVudGl0eSIvPjxmZUZ1bmNHIHR5cGU9ImlkZW50aXR5Ii8+PGZlRnVuY0IgdHlwZT0iaWRlbnRpdHkiLz48ZmVGdW5jQSB0eXBlPSJkaXNjcmV0ZSIgdGFibGVWYWx1ZXM9IjAgMSIvPjwvZmVDb21wb25lbnRUcmFuc2Zlcj48L2ZpbHRlcj48L3N2Zz4=#filter)'
-
-const FONT = '16px MinecraftFull'
-
-type JsonTextColor =
- | 'dark_red'
- | 'red'
- | 'gold'
- | 'yellow'
- | 'dark_green'
- | 'green'
- | 'aqua'
- | 'dark_aqua'
- | 'dark_blue'
- | 'blue'
- | 'light_purple'
- | 'dark_purple'
- | 'white'
- | 'gray'
- | 'dark_gray'
- | 'black'
- | `#${string}`
-
-const COLOR_MAP: Record = {
- dark_red: '#AA0000',
- red: '#FF5555',
- gold: '#FFAA00',
- yellow: '#FFFF55',
- dark_green: '#00AA00',
- green: '#55FF55',
- aqua: '#55FFFF',
- dark_aqua: '#00AAAA',
- dark_blue: '#0000AA',
- blue: '#5555FF',
- light_purple: '#FF55FF',
- dark_purple: '#AA00AA',
- white: '#FFFFFF',
- gray: '#AAAAAA',
- dark_gray: '#555555',
- black: '#000000',
-}
-
-type JsonTextObject = {
- text?: string
- font?: string
- color?: JsonTextColor
- extra?: JsonTextArray
- bold?: true | false
- italic?: true | false
- underlined?: true | false
- strikethrough?: true | false
- obfuscated?: true | false
- insertion?: string
- clickEvent?: {
- action:
- | 'open_url'
- | 'open_file'
- | 'run_command'
- | 'suggest_command'
- | 'change_page'
- | 'copy_to_clipboard'
- value: string
- }
- hoverEvent?: {
- action: 'show_text' | 'show_item' | 'show_entity'
- contents:
- | JsonTextComponent
- | {
- type: string
- id: string
- name?: string
- }
- | {
- id: string
- count?: number
- tag?: string
- }
- }
- tl?: string
- with?: JsonTextArray
- score?: {
- name: string
- objective: string
- value?: number
- }
- selector?: string
- separator?: string
- keybind?: string
- nbt?: string
- block?: string
- entity?: string
- storage?: string
-}
-
-type JsonTextComponent = string | JsonTextArray | JsonTextObject
-
-type JsonTextArray = JsonTextComponent[] | string[]
-
-export class JsonText {
- private text: JsonTextComponent
- constructor(jsonText: JsonTextComponent) {
- this.text = jsonText
- }
-
- toString() {
- return JSON.stringify(this.text)
- }
-
- toJSON() {
- return this.text
- }
-
- renderToCanvas(): TextCanvas {
- const textCanvas = new TextCanvas()
-
- this._renderToCanvas(this.text, textCanvas)
- // textCanvas.canvas.width = textCanvas.width
- // textCanvas.canvas.height = textCanvas.height
- textCanvas.render()
-
- return textCanvas
- }
-
- private _renderToCanvas(comp: JsonTextComponent, textCanvas: TextCanvas) {
- if (comp instanceof Array) {
- console.log('array', comp)
- comp.forEach(c => this._renderToCanvas(c, textCanvas))
- } else if (typeof comp === 'string') {
- console.log('string', comp)
- } else {
- console.log('obj', comp)
- this._renderObj(comp, textCanvas)
- }
- }
-
- private _renderObj(obj: JsonTextObject, textCanvas: TextCanvas) {
- const writeOptions: Record = {}
- // canvas.ctx.font = FONT
- if (obj.color)
- writeOptions.fillStyle = obj.color[0] === '#' ? obj.color : COLOR_MAP[obj.color]
-
- if (obj.bold) writeOptions.font = 'bold ' + FONT
- if (obj.italic) writeOptions.font = 'italic ' + FONT
- // if (obj.underlined) canvas.canvas.style.textDecoration = 'underline'
- // if (obj.strikethrough) canvas.canvas.style.textDecoration = 'line-through'
- // if (obj.obfuscated) canvas.canvas.style.textDecoration = 'blink'
-
- if (obj.text) {
- textCanvas.write(obj.text, writeOptions)
- } else if (obj.score) {
- textCanvas.write(
- obj.score.value === undefined
- ? `(${obj.score.name} ${obj.score.objective})`
- : obj.score.value.toString(),
- writeOptions
- )
- } else if (obj.nbt) {
- let text = `${obj.nbt}`
- if (obj.block) text += ` ${obj.block}`
- else if (obj.entity) text += ` ${obj.entity}`
- else if (obj.storage) text += ` ${obj.storage}`
- textCanvas.write(text, writeOptions)
- } else if (obj.selector) {
- textCanvas.write(`(${obj.selector})`, writeOptions)
- }
- }
-}
-
-interface TextBit {
- canvas: CanvasFrame
- posX: number
- posY: number
-}
-
-class TextCanvas {
- canvasFrame: CanvasFrame
- canvas: HTMLCanvasElement
- ctx: CanvasRenderingContext2D
-
- private textBits: TextBit[] = []
-
- currentX = 0
- currentY = 0
-
- constructor() {
- this.canvasFrame = new CanvasFrame(1, 1)
- this.canvas = this.canvasFrame.canvas
- this.ctx = this.canvasFrame.ctx
- }
-
- get width() {
- return this.canvas.width
- }
-
- set width(width: number) {
- this.canvas.width = width
- }
-
- get height() {
- return this.canvas.height
- }
-
- set height(height: number) {
- this.canvas.height = height
- }
-
- write(text: string, writeOptions: Record = {}) {
- const measure = new CanvasFrame(1, 1)
- measure.ctx.font = FONT
- measure.ctx.filter = PIXEL_FILTER
- const metrics = measure.ctx.measureText(text)
- const width = Math.ceil(metrics.width)
- const height = Math.ceil(metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent)
-
- const canvas = new CanvasFrame(width, height)
- canvas.ctx.font = FONT
- canvas.ctx.filter = PIXEL_FILTER
- canvas.ctx.fillStyle = '#ffffff'
- canvas.ctx.textBaseline = 'top'
-
- Object.assign(canvas.ctx, writeOptions)
-
- canvas.ctx.fillText(text, 0, 0)
-
- this.textBits.push({ canvas, posX: this.currentX, posY: this.currentY })
-
- this.currentX += width
- // this.currentY += height
- }
-
- render() {
- console.log('rendering', this.textBits)
-
- const totalWidth = this.textBits.reduce((acc, bit) => acc + bit.canvas.width, 0)
- const totalHeight = this.textBits.reduce((acc, bit) => Math.max(acc, bit.canvas.height), 0)
-
- this.width = totalWidth
- this.height = totalHeight
-
- for (const bit of this.textBits) {
- console.log('drawing', bit)
- this.ctx.drawImage(bit.canvas.canvas, bit.posX, bit.posY)
- }
- }
-}
diff --git a/src/minecraft/registryLoader.ts b/src/minecraft/registryLoader.ts
deleted file mode 100644
index ab94f3c7..00000000
--- a/src/minecraft/registryLoader.ts
+++ /dev/null
@@ -1,164 +0,0 @@
-interface IRegistryJSON {
- activity: string[]
- advancement: string[]
- attribute: string[]
- block: string[]
- block_definition: string[]
- block_entity_type: string[]
- block_predicate_type: string[]
- chunk_status: string[]
- custom_stat: string[]
- dimension: string[]
- dimension_type: string[]
- enchantment: string[]
- entity_type: string[]
- float_provider_type: string[]
- fluid: string[]
- font: string[]
- function: string[]
- game_event: string[]
- height_provider_type: string[]
- int_provider_type: string[]
- item: string[]
- item_modifier: string[]
- loot_condition_type: string[]
- loot_function_type: string[]
- loot_nbt_provider_type: string[]
- loot_number_provider_type: string[]
- loot_pool_entry_type: string[]
- loot_score_provider_type: string[]
- loot_table: string[]
- memory_module_type: string[]
- menu: string[]
- mob_effect: string[]
- model: string[]
- motive: string[]
- particle_type: string[]
- point_of_interest_type: string[]
- pos_rule_test: string[]
- position_source_type: string[]
- potion: string[]
- predicate: string[]
- recipe: string[]
- recipe_serializer: string[]
- recipe_type: string[]
- rule_test: string[]
- schedule: string[]
- sensor_type: string[]
- sound_event: string[]
- stat_type: string[]
- structure: string[]
- 'tag/block': string[]
- 'tag/entity_type': string[]
- 'tag/fluid': string[]
- 'tag/game_event': string[]
- 'tag/item': string[]
- texture: string[]
- villager_profession: string[]
- villager_type: string[]
- 'worldgen/biome': string[]
- 'worldgen/biome_source': string[]
- 'worldgen/block_state_provider_type': string[]
- 'worldgen/carver': string[]
- 'worldgen/chunk_generator': string[]
- 'worldgen/configured_carver': string[]
- 'worldgen/configured_feature': string[]
- 'worldgen/configured_structure_feature': string[]
- 'worldgen/configured_surface_builder': string[]
- 'worldgen/feature': string[]
- 'worldgen/feature_size_type': string[]
- 'worldgen/foliage_placer_type': string[]
- 'worldgen/material_condition': string[]
- 'worldgen/material_rule': string[]
- 'worldgen/noise': string[]
- 'worldgen/noise_settings': string[]
- 'worldgen/placed_feature': string[]
- 'worldgen/placement_modifier_type': string[]
- 'worldgen/processor_list': string[]
- 'worldgen/structure_feature': string[]
- 'worldgen/structure_piece': string[]
- 'worldgen/structure_placement': string[]
- 'worldgen/structure_pool_element': string[]
- 'worldgen/structure_processor': string[]
- 'worldgen/template_pool': string[]
- 'worldgen/tree_decorator_type': string[]
- 'worldgen/trunk_placer_type': string[]
-}
-
-const TWO_DAYS = 172800000
-const MAX_RETRIES = 5
-
-export class RegistryLoader {
- key: string
- url: string
- constructor(key: string, url: string) {
- this.url = url
- this.key = key
- }
-
- get lastTime() {
- const local = localStorage.getItem(`${this.key}.lastTime`)
- if (local) return Number(local)
- return 0
- }
-
- get localValue() {
- const local = localStorage.getItem(this.key)
- if (local) {
- try {
- return JSON.parse(local) as IRegistryJSON
- } catch (err) {
- console.log('Failed to parse local registry', err)
- }
- }
- }
-
- async load(): Promise {
- const lastTime = this.lastTime
- const now = Date.now()
- if (lastTime && now - lastTime >= TWO_DAYS) {
- localStorage.setItem(`${this.key}.lastTime`, String(now))
- console.log(`Local registry for ${this.key} out of date, Updating...`)
- return await this.fetch()
- }
-
- const local = this.localValue
- if (!local) {
- console.log(`No local registry found for ${this.key}. Collecting...`)
- return await this.fetch()
- }
- console.log(`Local registry for ${this.key} found. Loading...`)
- return local
- }
-
- async fetch() {
- const url = this.url
- let retries = 0
- const json = await new Promise(function request(resolve, reject) {
- fetch(url)
- .then(r => {
- if (r) resolve(r.json())
- })
- .catch((err: Error) => {
- console.log(
- `Failed to get Minecraft registry (${err.message}). Retrying in 1 second...`
- )
- retries++
- if (retries > MAX_RETRIES)
- reject(
- 'Failed to download Minecraft Registry. Are you connected to the internet?'
- )
- setTimeout(request, 50)
- })
- })
-
- localStorage.setItem(this.key, JSON.stringify(json))
- return json
- }
-}
-
-const REGISTRY = new RegistryLoader(
- `animated-java`,
- 'https://raw.githubusercontent.com/misode/mcmeta/summary/registries/data.json'
-)
-export const registry = REGISTRY.load()
diff --git a/src/minecraft/searchGenerator.ts b/src/minecraft/searchGenerator.ts
deleted file mode 100644
index f88685a4..00000000
--- a/src/minecraft/searchGenerator.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-interface ITreeLeaf {
- type: 'leaf'
- scoreIndex: number
- item: T
-}
-
-interface ITreeBranch {
- type: 'branch'
- items?: Array | ITreeLeaf>
- minScoreIndex: number
- maxScoreIndex: number
-}
-
-export function generateSearchTree(
- items: T[],
- trimmer?: (item: ITreeBranch | ITreeLeaf) => boolean | void
-): ITreeBranch | ITreeLeaf | undefined {
- const depth = () => Math.floor(Math.log(items.length) / Math.log(8))
- const remainingItems = [...items]
- let scoreIndex = 0
- function recurse(myDepth = 0): ITreeBranch | ITreeLeaf | undefined {
- const minScoreIndex = scoreIndex
- let maxScoreIndex = null
- const tree: Array | ITreeLeaf> = []
- for (let i = 0; i < 8; i++) {
- if (remainingItems.length === 0) break
- if (myDepth < depth() && remainingItems.length >= 8 - i) {
- const item = recurse(myDepth + 1)
- if (trimmer && item) {
- if (trimmer(item)) tree.push(item)
- } else if (item) tree.push(item)
- } else {
- const item: ITreeLeaf = {
- type: 'leaf',
- item: remainingItems.shift()!,
- scoreIndex: scoreIndex,
- }
- if (trimmer && item) {
- if (trimmer(item)) tree.push(item)
- } else if (item) tree.push(item)
- scoreIndex++
- }
- }
- maxScoreIndex = scoreIndex - 1
- if (tree.length === 1) {
- if (trimmer && !trimmer(tree[0])) return
- return tree[0]
- }
- return { minScoreIndex, maxScoreIndex, items: tree, type: 'branch' } as ITreeBranch
- }
- return recurse()
-}
diff --git a/src/minecraft/util.ts b/src/minecraft/util.ts
deleted file mode 100644
index 6b38fd09..00000000
--- a/src/minecraft/util.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-export function safeFunctionName(name: string) {
- return name
- .replace(/[\s-]/g, '_')
- .replace(/[^a-zA-Z0-9_\\.]/g, '')
- .toLowerCase()
-}
-
-export function isValidResourcePackMcMeta(path: string) {
- const parsed = PathModule.parse(path)
- const assetsPath = PathModule.join(parsed.dir, 'assets')
- return parsed.base === 'pack.mcmeta' && fs.existsSync(path) && fs.existsSync(assetsPath)
-}
-
-export function isValidDataPackMcMeta(path: string) {
- const parsed = PathModule.parse(path)
- const dataPath = PathModule.join(parsed.dir, 'data')
- return parsed.base === 'pack.mcmeta' && fs.existsSync(path) && fs.existsSync(dataPath)
-}
-
-export function isValidResourcePackPath(path: string) {
- const parsed = parseResourcePackPath(path)
- return parsed && parsed.namespace && parsed.resourcePath
-}
-
-export function parseResourcePackPath(path: string) {
- path = path.replace(/[\\/]/g, PathModule.sep)
- const parts = path.split(PathModule.sep)
-
- const assetsIndex = parts.indexOf('assets')
- if (assetsIndex === -1) return false
-
- const resourcePackRoot = parts.slice(0, assetsIndex).join(PathModule.sep)
-
- const namespace = parts[assetsIndex + 1]
- if (namespace !== namespace.toLowerCase()) return false
-
- const resourcePath = parts.slice(assetsIndex + 3).join(PathModule.sep)
- if (resourcePath !== resourcePath.toLowerCase()) return false
-
- const fileName = parts[parts.length - 1]
- if (fileName !== fileName.toLowerCase()) return false
-
- let resourceLocation = namespace + ':' + resourcePath.replace(/\\/g, '/')
- const index = resourceLocation.lastIndexOf('.')
- if (index !== -1) resourceLocation = resourceLocation.substring(0, index)
-
- return {
- resourcePackRoot,
- namespace,
- resourcePath,
- resourceLocation,
- fileName,
- }
-}
-
-export function isValidDatapackName(name: string, type: string) {
- const safeName = safeFunctionName(name)
- if (name !== safeName)
- throw new Error(`Invalid ${type} name "${name}". Try "${safeName}" instead.`)
-}
diff --git a/src/modelDataFixerUpper.ts b/src/modelDataFixerUpper.ts
deleted file mode 100644
index 6a7625e5..00000000
--- a/src/modelDataFixerUpper.ts
+++ /dev/null
@@ -1,171 +0,0 @@
-import { FORMAT_VERSION, IAnimatedJavaModel } from './modelFormat'
-import { openUnexpectedErrorDialog } from './ui/popups/unexpectedError'
-
-export function process(model: any) {
- if (model.meta.model_format === 'animatedJava/ajmodel') {
- model.meta.model_format = 'animated_java/ajmodel'
- model.meta.format_version = '0.0'
- }
- console.log('Processing model', JSON.parse(JSON.stringify(model)))
-
- const needsUpgrade = compareVersions(FORMAT_VERSION, model.meta.format_version)
- if (!needsUpgrade) return
-
- console.log('Upgrading model from version', model.meta.format_version, 'to', FORMAT_VERSION)
-
- try {
- console.group('Upgrade process')
- if (model.meta.format_version.length === 3) {
- if (compareVersions('1.0', model.meta.format_version)) updateModelTo1_0(model)
- if (compareVersions('1.1', model.meta.format_version)) updateModelTo1_1(model)
- if (compareVersions('1.2', model.meta.format_version)) updateModelTo1_2(model)
- if (compareVersions('1.3', model.meta.format_version)) updateModelTo1_3(model)
- if (compareVersions('1.4', model.meta.format_version)) updateModelTo1_4(model)
- model.meta.format_version = '0.3.9'
- }
- // Versions below this are post 0.3.10. I changed the versioning system to use the AJ version instead of a unique format version.
- if (compareVersions('0.3.10', model.meta.format_version)) updateModelTo0_3_10(model)
- console.groupEnd()
- } catch (e) {
- console.error(e)
- openUnexpectedErrorDialog(e)
- void Project?.close(true)
- return
- }
-
- model.meta.format_version = FORMAT_VERSION
-
- console.log('Upgrade complete')
-}
-
-// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars
-function updateModelTo0_3_10(model: any) {
- console.log('Processing model for AJ 0.3.10', JSON.parse(JSON.stringify(model)))
-}
-
-// eslint-disable-next-line @typescript-eslint/naming-convention
-function updateModelTo1_4(model: any) {
- console.log('Processing model format 1.4', JSON.parse(JSON.stringify(model)))
- const exporter = model.animated_java.exporter_settings['animated_java:datapack_exporter']
- if (exporter && exporter.outdated_rig_warning !== undefined) {
- model.animated_java.exporter_settings[
- 'animated_java:datapack_exporter'
- ].enable_outdated_rig_warning =
- model.animated_java.exporter_settings[
- 'animated_java:datapack_exporter'
- ].outdated_rig_warning
- delete model.animated_java.exporter_settings['animated_java:datapack_exporter']
- .outdated_rig_warning
- }
-}
-
-// eslint-disable-next-line @typescript-eslint/naming-convention
-function updateModelTo1_3(model: any) {
- console.log('Processing model format 1.3', JSON.parse(JSON.stringify(model)))
- if (model.animated_java.settings.exporter === 'animated_java:animation_exporter') {
- model.animated_java.settings.exporter = 'animated_java:datapack_exporter'
- }
- if (model.animated_java.exporter_settings['animated_java:animation_exporter']) {
- model.animated_java.exporter_settings['animated_java:datapack_exporter'] =
- model.animated_java.exporter_settings['animated_java:animation_exporter']
- delete model.animated_java.exporter_settings['animated_java:animation_exporter']
- }
-}
-
-// eslint-disable-next-line @typescript-eslint/naming-convention
-function updateModelTo1_2(model: any) {
- console.log('Processing model format 1.2', JSON.parse(JSON.stringify(model)))
- for (const variant of model.animated_java.variants) {
- for (const [from, to] of Object.entries(variant.textureMap as Record)) {
- const fromUUID = from.split('::')[0]
- const toUUID = to.split('::')[0]
- variant.textureMap[fromUUID] = toUUID
- delete variant.textureMap[from]
- }
- }
-}
-
-// eslint-disable-next-line @typescript-eslint/naming-convention
-function updateModelTo1_1(model: any) {
- console.log('Processing model format 1.1', JSON.parse(JSON.stringify(model)))
- model.animated_java.settings.resource_pack_mcmeta =
- model.animated_java.settings.resource_pack_folder
- delete model.animated_java.settings.resource_pack_folder
- const animationExporterSettings =
- model.animated_java.exporter_settings['animated_java:animation_exporter']
- if (!animationExporterSettings) return
- animationExporterSettings.datapack_mcmeta = animationExporterSettings.datapack_folder
- delete animationExporterSettings.datapack_folder
-}
-
-// eslint-disable-next-line @typescript-eslint/naming-convention
-function updateModelTo1_0(model: any) {
- console.log('Processing model format 1.0', JSON.parse(JSON.stringify(model)))
- if (model.meta.settings) {
- console.log('Upgrading settings...')
- const animatedJava: IAnimatedJavaModel['animated_java'] = {
- settings: {
- project_name: model.meta.settings.animatedJava.projectName,
- verbose: model.meta.settings.animatedJava.verbose,
- rig_item: model.meta.settings.animatedJava.rigItem,
- rig_item_model: model.meta.settings.animatedJava.predicateFilePath,
- rig_export_folder: model.meta.settings.animatedJava.rigModelsExportFolder,
- },
- exporter_settings: {},
- variants: [],
- } as IAnimatedJavaModel['animated_java']
-
- model.animated_java = animatedJava
- }
-
- if (model.meta.variants) {
- console.log('Upgrading variants...')
- const variants: IAnimatedJavaModel['animated_java']['variants'] = []
-
- for (const [name, variant] of Object.entries(model.meta.variants as Record)) {
- variants.push({
- name,
- uuid: guid(),
- textureMap: variant,
- default: name === 'default',
- boneConfig: {},
- affectedBones: [],
- affectedBonesIsAWhitelist: false,
- })
- }
-
- model.animated_java.variants = variants
- }
-
- if (
- model.animations &&
- model.animations.find((a: any) =>
- Object.keys(a.animators as Record).find(name => name === 'effects')
- )
- ) {
- console.log('Upgrading effects...')
-
- for (const animation of model.animations) {
- const effects = animation.animators.effects
- if (!effects) continue
- for (const keyframe of effects.keyframes) {
- if (keyframe.channel !== 'timeline') continue
- for (const dataPoint of keyframe.data_points) {
- if (dataPoint.script) {
- dataPoint.commands = dataPoint.script
- delete dataPoint.script
- keyframe.channel = 'commands'
- }
- }
- }
- }
-
- console.log('Upgrading effects complete', model.animations)
- }
-
- model.meta.format_version = FORMAT_VERSION
-
- delete model.meta.variants
- delete model.meta.settings
- delete model.meta.uuid
-}
diff --git a/src/modelFormat.ts b/src/modelFormat.ts
deleted file mode 100644
index 1f87bc8d..00000000
--- a/src/modelFormat.ts
+++ /dev/null
@@ -1,762 +0,0 @@
-import * as fs from 'fs'
-import * as events from './events'
-import { AnimatedJavaExporter } from './exporter'
-import * as DFU from './modelDataFixerUpper'
-import { getDefaultProjectSettings } from './projectSettings'
-import * as AJSettings from './settings'
-import { injectStartScreen } from './ui/ajStartScreen'
-import { consoleGroup, consoleGroupCollapsed } from './util/console'
-import { createBlockbenchMod } from './util/moddingTools'
-import { IBoneConfig, TextureMap, Variant, VariantsContainer } from './variants'
-import * as PACKAGE from '../package.json'
-
-export const FORMAT_VERSION = PACKAGE.version
-
-function addProjectToRecentProjects(file: FileResult) {
- if (!Project || !file.path) return
- const name = pathToName(file.path, true)
- if (file.path && isApp && !file.no_file) {
- const project = Project
- Project.save_path = file.path
- Project.name = pathToName(name, false)
- addRecentProject({
- name,
- path: file.path,
- icon: ajModelFormat.icon,
- })
- setTimeout(() => {
- if (Project === project) void updateRecentProjectThumbnail()
- }, 200)
- }
-}
-
-export interface IAnimatedJavaModel {
- animated_java: {
- settings?: Record
- exporter_settings?: Record>
- variants?: Array<{
- name: string
- textureMap: TextureMap
- default?: boolean
- uuid: string
- boneConfig: Record
- affectedBones?: Array<{ name: string; value: string }>
- affectedBonesIsAWhitelist?: boolean
- }>
- }
-
- flag?: any
- meta?: any
- parent?: any
- history?: any
- display?: any
- textures?: any
- elements?: any
- overrides?: any
- animations?: Record
- outliner?: any[]
- resolution?: any
- history_index?: number
- animation_controllers?: any
- backgrounds?: Record
- editor_state?: Record
- animation_variable_placeholders?: any
-}
-
-const loadAnimatedJavaProjectSettings = consoleGroup(
- 'loadAnimatedJavaProjectSettings',
- (model: IAnimatedJavaModel) => {
- if (!Project) return
-
- const settings = getDefaultProjectSettings()
- for (const setting of Object.values(settings)) {
- setting._onInit()
- }
- if (!(model.animated_java && model.animated_java.settings)) return
-
- console.log('Loading Animated Java project settings...')
-
- for (const [name, setting] of Object.entries(settings)) {
- if (model.animated_java.settings[name] === undefined) continue
- setting._load(model.animated_java.settings[name])
- }
- Project.animated_java_settings = settings
- }
-)
-
-const loadAnimatedJavaExporterSettings = consoleGroup(
- 'loadAnimatedJavaExporterSettings',
- (model: IAnimatedJavaModel) => {
- if (!Project) return
- const settings: typeof Project.animated_java_exporter_settings = {}
- if (!model.animated_java.exporter_settings) return
-
- for (const exporter of AnimatedJavaExporter.all) {
- if (!exporter) continue
- console.log('Initializing settings for', exporter.id)
- settings[exporter.id] = exporter.getSettings()
- for (const setting of Object.values(settings[exporter.id])) {
- setting._onInit()
- }
-
- const savedSettings = model.animated_java.exporter_settings[exporter.id]
- if (!savedSettings) continue
-
- console.group(`Loading ${exporter.id} settings...`)
- for (const [settingId, settingValue] of Object.entries(savedSettings)) {
- if (model.animated_java.exporter_settings[exporter.id][settingId] === undefined)
- continue
- if (settings[exporter.id][settingId] === undefined) {
- console.warn('Setting', settingId, 'does not exist in exporter', exporter.id)
- continue
- }
- console.log('Loading value for', exporter.id, settingId, settingValue)
- settings[exporter.id][settingId]._load(settingValue)
- }
- console.groupEnd()
- }
- Project.animated_java_exporter_settings = settings
- }
-)
-
-const exportAnimatedJavaProjectSettings = consoleGroup('exportAnimatedJavaProjectSettings', () => {
- if (!Project?.animated_java_settings) return
- const exported: Record = {}
- for (const [name, setting] of Object.entries(Project.animated_java_settings)) {
- exported[name] = setting._save()
- }
-
- return exported
-})
-
-function exportAnimatedJavaExporterSettings() {
- if (!Project?.animated_java_exporter_settings) return
- const exported: Record = {}
- for (const [exporterId, exporterSettings] of Object.entries(
- Project.animated_java_exporter_settings
- )) {
- exported[exporterId] = {}
- for (const [settingId, setting] of Object.entries(exporterSettings)) {
- exported[exporterId][settingId] = setting._save()
- }
- }
-
- return exported
-}
-
-const loadAnimatedJavaVariants = consoleGroup(
- 'loadAnimatedJavaVariants',
- (model: IAnimatedJavaModel) => {
- if (!Project) return
-
- Project.animated_java_variants = new VariantsContainer()
- if (!(model.animated_java && model.animated_java.variants)) return
-
- console.log('Loading Animated Java variants...')
- for (const variant of model.animated_java.variants) {
- console.log('Loading variant', variant.name)
- if (!(variant.name && variant.textureMap && variant.uuid)) continue
- Project.animated_java_variants.addVariant(Variant.fromJSON(variant), variant.default)
- }
-
- Project.animated_java_variants.select()
- }
-)
-
-const exportAnimatedJavaVariants = consoleGroup('exportAnimatedJavaVariants', () => {
- if (!Project?.animated_java_variants) return
- const exported: IAnimatedJavaModel['animated_java']['variants'] = []
- for (const variant of Project.animated_java_variants.variants) {
- exported.push(variant.toJSON())
- }
-
- return exported
-})
-
-events.UPDATE_SELECTION.subscribe(() => {
- if (Format === ajModelFormat) {
- if (!Group.selected && Cube.selected.length > 0) {
- ajModelFormat.rotation_limit = true
- ajModelFormat.rotation_snap = true
- } else {
- ajModelFormat.rotation_limit = false
- ajModelFormat.rotation_snap = false
- }
- }
-})
-
-export const ajCodec = new Blockbench.Codec('ajmodel', {
- name: 'Animated Java Model',
- remember: true,
- extension: 'ajmodel',
- load_filter: {
- extensions: ['ajmodel', 'mcmodel'],
- type: 'json',
- },
-
- load: consoleGroupCollapsed('ajCodec:load', (model, file) => {
- setupProject(ajModelFormat)
- if (!Project || !ajCodec.parse) return
- Project.save_path = file.path
- Project.export_path = file.path
- addProjectToRecentProjects(file)
- ajCodec.parse(model, file.path)
- events.LOAD_PROJECT.dispatch()
- }),
-
- parse: consoleGroupCollapsed('ajCodec:parse', (model: IAnimatedJavaModel, path) => {
- if (!Project) throw new Error('No project to load model into...')
- console.log('Parsing Animated Java model...', model)
- if (!model.elements && !model.parent && !model.display && !model.textures) {
- Blockbench.showMessageBox({
- translateKey: 'invalid_model',
- icon: 'error',
- })
- return
- }
- ajCodec.dispatchEvent('parse', { model, path })
- DFU.process(model)
-
- Project.animated_java_uuid = model.meta.uuid || guid()
-
- if (model.resolution !== undefined) {
- Project.texture_width = model.resolution.width
- Project.texture_height = model.resolution.height
- }
-
- loadAnimatedJavaProjectSettings(model)
- loadAnimatedJavaExporterSettings(model)
-
- if (model.meta.box_uv !== undefined && Format.optional_box_uv) {
- Project.box_uv = model.meta.box_uv
- }
-
- for (const key in ModelProject.properties) {
- ModelProject.properties[key].merge(Project, model)
- }
-
- if (model.overrides) {
- Project.overrides = model.overrides
- }
- if (model.textures) {
- model.textures.forEach((tex: Texture) => {
- const texCopy = new Texture(tex, tex.uuid).add(false)
- if (isApp && tex.relative_path && Project.save_path) {
- const resolvedPath = PathModule.resolve(Project.save_path, tex.relative_path)
- if (fs.existsSync(resolvedPath)) {
- texCopy.fromPath(resolvedPath)
- return
- }
- }
- if (isApp && tex.path && fs.existsSync(tex.path) && !model.meta.backup) {
- texCopy.fromPath(tex.path)
- return
- }
- if (tex.source && tex.source.substr(0, 5) == 'data:') {
- texCopy.fromDataURL(tex.source)
- }
- })
- }
- loadAnimatedJavaVariants(model)
-
- if (model.elements) {
- const defaultTexture = Texture.getDefault()
- model.elements.forEach(function (element: any) {
- const copy = OutlinerElement.fromSave(element, true) as Cube
- for (const face in copy.faces) {
- if (!Format.single_texture && element.faces) {
- const texture =
- element.faces[face].texture !== null &&
- Texture.all[element.faces[face].texture]
- if (texture) {
- copy.faces[face].texture = texture.uuid
- }
- } else if (defaultTexture && copy.faces && copy.faces[face].texture !== null) {
- copy.faces[face].texture = defaultTexture.uuid
- }
- }
- copy.init()
- })
- }
- if (model.outliner) {
- parseGroups(model.outliner)
- }
- if (model.animations) {
- model.animations.forEach((anim: _Animation) => {
- const baseAnim = new Blockbench.Animation()
- baseAnim.uuid = anim.uuid
- baseAnim.extend(anim).add()
- if (isApp && Format.animation_files) {
- baseAnim.saved_name = baseAnim.name
- }
- })
- }
- // if (model.animation_controllers) {
- // model.animation_controllers.forEach((anim: _Animation) => {
- // var base_ani = new AnimationController()
- // base_ani.uuid = anim.uuid
- // base_ani.extend(anim).add()
- // if (isApp && Format.animation_files) {
- // base_ani.saved_name = base_ani.name
- // }
- // })
- // }
- if (model.animation_variable_placeholders) {
- Interface.Panels.variable_placeholders.inside_vue._data.text =
- model.animation_variable_placeholders
- }
- if (model.backgrounds) {
- for (const key in model.backgrounds) {
- if (Object.hasOwn(Project.backgrounds, key)) {
- const store = model.backgrounds[key]
- const real = Project.backgrounds[key]
-
- if (store.image !== undefined) {
- real.image = store.image
- }
- if (store.size !== undefined) {
- real.size = store.size
- }
- if (store.x !== undefined) {
- real.x = store.x
- }
- if (store.y !== undefined) {
- real.y = store.y
- }
- if (store.lock !== undefined) {
- real.lock = store.lock
- }
- }
- }
- Preview.all.forEach(p => {
- if (p.canvas.isConnected) {
- p.loadBackground()
- }
- })
- }
- if (model.history) {
- Undo.history = model.history.slice()
- Undo.index = model.history_index || 0
- }
- Canvas.updateAllBones()
- Canvas.updateAllPositions()
- Validator.validate()
- ajCodec.dispatchEvent('parsed', { model })
-
- if (model.editor_state) {
- const state = model.editor_state
- Merge.string(Project, state, 'save_path')
- Merge.string(Project, state, 'export_path')
- Merge.boolean(Project, state, 'saved')
- Merge.number(Project, state, 'added_models')
- Merge.string(Project, state, 'mode')
- Merge.string(Project, state, 'tool')
- Merge.string(Project, state, 'display_uv')
- Merge.boolean(Project, state, 'exploded_view')
- if (state.uv_viewport) {
- Merge.number(Project.uv_viewport, state.uv_viewport, 'zoom')
- Merge.arrayVector2((Project.uv_viewport = state.uv_viewport), 'offset')
- }
- if (state.previews) {
- for (const id in state.previews) {
- Project.previews[id] = state.previews[id]
- }
- }
- state.selected_elements.forEach((uuid: string) => {
- const el = Outliner.elements.find(el2 => el2.uuid == uuid)
- if (el) Project.selected_elements.push(el)
- })
- Group.selected =
- state.selected_group && Group.all.find(g => g.uuid == state.selected_group)
- for (const key in state.selected_vertices) {
- Project.mesh_selection[key] = state.mesh_selection[key]
- }
- Project.selected_faces.replace(state.selected_faces as any[])
- ;(
- state.selected_texture && Texture.all.find(t => t.uuid == state.selected_texture)
- )?.select()
-
- Project.loadEditorState()
- }
-
- // Verify and clean textures
- for (const texture of Project.textures) {
- texture.name = texture.name.replace(/\.png$/, '')
- }
- // ajCodec.dispatchEvent('parsed', { model })
- }),
-
- compile: consoleGroupCollapsed('ajCodec:compile', options => {
- if (!options) options = {}
- if (!Project) throw new Error('No project to compile...')
- console.log('Compiling Animated Java model...')
-
- const selectedVariant = Project.animated_java_variants!.selectedVariant
- Project.animated_java_variants!.select()
-
- const model: IAnimatedJavaModel = {
- meta: {
- format: ajCodec.format.id,
- format_version: FORMAT_VERSION,
- uuid: Project.animated_java_uuid || guid(),
- },
- animated_java: {
- settings: exportAnimatedJavaProjectSettings(),
- exporter_settings: exportAnimatedJavaExporterSettings(),
- variants: exportAnimatedJavaVariants(),
- },
- }
-
- for (const key in ModelProject.properties) {
- if (ModelProject.properties[key].export)
- ModelProject.properties[key].copy(Project, model)
- }
-
- if (Project.overrides) {
- model.overrides = Project.overrides
- }
- model.resolution = {
- width: Project.texture_width || 16,
- height: Project.texture_height || 16,
- }
-
- if (options.flag) {
- model.flag = options.flag
- }
-
- if (options.editor_state) {
- Project.saveEditorState()
- model.editor_state = {
- save_path: Project.save_path,
- export_path: Project.export_path,
- saved: Project.saved,
- added_models: Project.added_models,
- mode: Project.mode,
- tool: Project.tool,
- display_uv: Project.display_uv,
- exploded_view: Project.exploded_view,
- uv_viewport: Project.uv_viewport,
- previews: JSON.parse(JSON.stringify(Project.previews)),
-
- selected_elements: Project.selected_elements.map(e => e.uuid),
- selected_group: Project.selected_group?.uuid,
- mesh_selection: JSON.parse(JSON.stringify(Project.mesh_selection)),
- selected_faces: Project.selected_faces,
- selected_texture: Project.selected_texture?.uuid,
- }
- }
-
- model.elements = []
- elements.forEach(el => {
- const obj = el.getSaveCopy && el.getSaveCopy(!!model.meta)
- model.elements.push(obj)
- })
- model.outliner = compileGroups(true)
-
- model.textures = []
- Texture.all.forEach(tex => {
- const t = tex.getUndoCopy()
- delete t.selected
- if (isApp && Project.save_path && tex.path) {
- const relative = PathModule.relative(Project.save_path, tex.path)
- t.relative_path = relative.replace(/\\/g, '/')
- }
- if (Settings.get('embed_textures') || options.backup) {
- t.source = 'data:image/png;base64,' + tex.getBase64()
- t.mode = 'bitmap'
- // tex.saved = true
- }
- if (options.absolute_paths == false) delete t.path
- model.textures.push(t)
- })
-
- if (Blockbench.Animation.all.length) {
- model.animations = []
- Blockbench.Animation.all.forEach(a => {
- model.animations!.push(
- a.getUndoCopy &&
- a.getUndoCopy(
- { bone_names: true, absolute_paths: options.absolute_paths },
- true
- )
- )
- })
- }
- // if (AnimationController.all.length) {
- // model.animation_controllers = []
- // AnimationController.all.forEach(a => {
- // model.animation_controllers.push(a.getUndoCopy && a.getUndoCopy())
- // })
- // }
- if (Interface.Panels.variable_placeholders.inside_vue._data.text) {
- model.animation_variable_placeholders =
- Interface.Panels.variable_placeholders.inside_vue._data.text
- }
-
- if (!options.backup) {
- // Backgrounds
- const backgrounds: IAnimatedJavaModel['backgrounds'] = {}
-
- for (const key in Project.backgrounds) {
- const scene = Project.backgrounds[key]
- if (scene.image) {
- backgrounds[key] = scene.getSaveCopy()
- }
- }
- if (Object.keys(backgrounds).length) {
- model.backgrounds = backgrounds
- }
- }
-
- if (options.history) {
- model.history = []
- Undo.history.forEach(h => {
- const e = {
- before: omitKeys(h.before, ['aspects']),
- post: omitKeys(h.post, ['aspects']),
- action: h.action,
- time: h.time,
- }
- model.history.push(e)
- })
- model.history_index = Undo.index
- }
-
- const content = compileJSON(model)
- ajCodec.dispatchEvent('compile', { model, options })
- events.SAVE_PROJECT.dispatch()
-
- if (selectedVariant) Project.animated_java_variants!.select(selectedVariant)
-
- return options.raw ? model : content
- }),
-
- export: consoleGroupCollapsed('ajCodec:export', () => {
- console.log('Exporting Animated Java model...')
- Blockbench.export({
- resource_id: 'animated_java.export',
- type: 'json',
- extensions: [ajCodec.extension],
- content: ajCodec.compile(),
- // eslint-disable-next-line @typescript-eslint/naming-convention
- custom_writer: (content, path) => {
- ajCodec.write(content, path)
- },
- })
- }),
-
- fileName() {
- return Project?.animated_java_settings?.project_namespace.value || 'unnamed_project'
- },
-})
-
-// ANCHOR Model Conversion
-export function convertToAJModelFormat() {
- console.log('Converting to Animated Java model...')
- Project!.animated_java_settings = getDefaultProjectSettings()
- for (const setting of Object.values(Project!.animated_java_settings)) {
- setting._onInit()
- }
-
- Project!.animated_java_exporter_settings = {}
- for (const exporter of AnimatedJavaExporter.all) {
- if (!exporter) continue
- console.log('Initializing settings for', exporter.id)
- Project!.animated_java_exporter_settings[exporter.id] = exporter.getSettings()
- for (const setting of Object.values(
- Project!.animated_java_exporter_settings[exporter.id]
- )) {
- setting._onInit()
- }
- }
-
- Project!.animated_java_variants = new VariantsContainer()
- Project!.animated_java_variants.addVariant(new Variant('default'))
-
- const oldAnimations = Project!.animations
- Project!.animations = []
- for (const animation of oldAnimations) {
- const newAnimation = new Blockbench.Animation()
- Project!.animations.push(newAnimation.extend(animation))
- }
-
- // Verify and clean textures
- for (const texture of Project!.textures) {
- texture.name = texture.name.replace(/\.png$/, '')
- }
-
- events.CONVERT_PROJECT.dispatch()
-
- const project = Project!
- project.unselect()
- project.select()
-}
-
-export const ajModelFormat = new Blockbench.ModelFormat({
- id: 'animated_java/ajmodel',
- icon: 'icon-armor_stand',
- name: 'Animated Java Rig',
- category: 'minecraft',
- target: 'Minecraft: Java Edition',
- confidential: false,
- condition: () => true,
- show_on_start_screen: true,
- format_page: {
- component: {
- methods: {},
- created: () => {
- console.log('Loading Animated Java model format page...')
- injectStartScreen()
- },
- template: `
-
The Animated Java Model Format
-
Target : Minecraft: Java Edition
-
`,
- },
- },
-
- onSetup(project: ModelProject, newModel = true) {
- if (project.animated_java_settings) {
- // Project Settings
- project.animated_java_settings = getDefaultProjectSettings()
- for (const setting of Object.values(project.animated_java_settings)) {
- setting._onInit()
- }
- // Exporter Settings
- const settings: Record>> = {}
- for (const exporter of AnimatedJavaExporter.all) {
- if (!exporter) continue
- settings[exporter.id] = exporter.getSettings()
- for (const setting of Object.values(settings[exporter.id])) {
- setting._onInit()
- }
- }
- if (newModel) project.animated_java_uuid = guid()
- project.animated_java_exporter_settings = settings
- }
- Group.all.forEach(v => v.createUniqueName())
- },
-
- codec: ajCodec,
-
- box_uv: false,
- optional_box_uv: true,
- single_texture: false,
- model_identifier: false,
- parent_model_id: false,
- vertex_color_ambient_occlusion: true,
- animated_textures: true,
- bone_rig: true,
- centered_grid: true,
- rotate_cubes: true,
- integer_size: false,
- meshes: false,
- texture_meshes: false,
- locators: true,
- rotation_limit: false,
- uv_rotation: true,
- java_face_properties: true,
- select_texture_for_particles: false,
- bone_binding_expression: true,
- animation_files: false,
- texture_folder: false,
- edit_mode: true,
- paint_mode: true,
- display_mode: false,
- animation_mode: true,
- pose_mode: false,
-})
-ajCodec.format = ajModelFormat
-
-createBlockbenchMod(
- 'animated_java:save_project',
- {
- action: BarItems.save_project as Action,
- originalClick: (BarItems.save_project as Action).click,
- },
- context => {
- context.action.click = (event: Event) => {
- if (Project && Format === ajModelFormat) {
- ajCodec.write(ajCodec.compile(), Project.save_path)
- } else {
- context.originalClick.call(context.action, event)
- }
- }
- return context
- },
- context => {
- context.action.click = context.originalClick
- }
-)
-
-createBlockbenchMod(
- 'animated_java:save_project_as',
- {
- action: BarItems.save_project_as as Action,
- originalClick: (BarItems.save_project_as as Action).click,
- },
- context => {
- context.action.click = (event: Event) => {
- if (Project && Format === ajModelFormat) {
- ajCodec.export()
- } else {
- context.originalClick.call(context.action, event)
- }
- }
- return context
- },
- context => {
- context.action.click = context.originalClick
- }
-)
-
-createBlockbenchMod(
- 'animated_java:export_over',
- {
- action: BarItems.export_over as Action,
- originalClick: (BarItems.export_over as Action).click,
- },
- context => {
- context.action.click = (event: Event) => {
- if (Project && Format === ajModelFormat) {
- if (Format) {
- // saveTextures()
- if (Project.export_path) {
- ajCodec.write(ajCodec.compile(), Project.export_path)
- } else if (!Project.save_path) {
- ajCodec.export()
- }
- }
- if (
- Blockbench.Animation.all.length &&
- BarItems.save_all_animations instanceof Action
- ) {
- BarItems.save_all_animations.trigger()
- }
- } else {
- context.originalClick.call(context.action, event)
- }
- }
- return context
- },
- context => {
- context.action.click = context.originalClick
- }
-)
-
-createBlockbenchMod(
- 'animated_java:events.preSelectProject',
- {
- original: ModelProject.prototype.select,
- },
- context => {
- ModelProject.prototype.select = function (this: ModelProject) {
- if (Project !== this) events.PRE_SELECT_PROJECT.dispatch(this)
- return context.original.call(this)
- }
- return context
- },
- context => {
- ModelProject.prototype.select = context.original
- }
-)
diff --git a/src/mods/animationControllerMod.ts b/src/mods/animationControllerMod.ts
new file mode 100644
index 00000000..91cc42be
--- /dev/null
+++ b/src/mods/animationControllerMod.ts
@@ -0,0 +1,20 @@
+import { BLUEPRINT_FORMAT } from '../blueprintFormat'
+import { PACKAGE } from '../constants'
+import { createBlockbenchMod, createPropertySubscribable } from '../util/moddingTools'
+
+createBlockbenchMod(
+ `${PACKAGE.name}:animationControllerMod`,
+ undefined,
+ () => {
+ const [, set] = createPropertySubscribable(AnimationController.prototype, 'saved')
+ const unsubSet = set.subscribe(({ storage }) => {
+ if (Format.id === BLUEPRINT_FORMAT.id) {
+ storage.value = true
+ }
+ })
+ return { unsubSet }
+ },
+ context => {
+ context.unsubSet()
+ }
+)
diff --git a/src/mods/animationMod.ts b/src/mods/animationMod.ts
deleted file mode 100644
index 23514d7c..00000000
--- a/src/mods/animationMod.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { ajModelFormat } from '../modelFormat'
-import { roundToN } from '../util/misc'
-import { createBlockbenchMod, createPropertySubscribable } from '../util/moddingTools'
-
-createBlockbenchMod(
- 'animated_java:animation/affected_bones',
- {
- extend: Blockbench.Animation.prototype.extend,
- setLength: Blockbench.Animation.prototype.setLength,
- compileBedrockAnimation: Blockbench.Animation.prototype.compileBedrockAnimation,
- propertyAffectedBones: undefined as Property<'array'> | undefined,
- propertyAffectedBonesIsAWhitelist: undefined as Property<'boolean'> | undefined,
- },
- context => {
- context.propertyAffectedBones = new Property(
- Blockbench.Animation,
- 'array',
- 'affected_bones',
- {
- condition: () => Format === ajModelFormat,
- }
- )
-
- context.propertyAffectedBonesIsAWhitelist = new Property(
- Blockbench.Animation,
- 'boolean',
- 'affected_bones_is_a_whitelist',
- {
- condition: () => Format === ajModelFormat,
- }
- )
-
- Blockbench.Animation.prototype.setLength = function (len?: number) {
- if (Format === ajModelFormat) {
- len = Math.max(len === undefined ? this.length : len, 0.05)
- }
- return context.setLength.call(this, len)
- }
-
- Blockbench.Animation.prototype.extend = function (
- this: _Animation,
- data: AnimationOptions
- ) {
- context.extend.call(this, data)
- if (Format === ajModelFormat) {
- // console.log('extend', this, data)
- this.snapping = 20
- // Round keyframes to the nearest tick (0.05 seconds)
- for (const animator of Object.values(this.animators)) {
- if (!animator) continue
- let lastTime = -Infinity
- for (const keyframe of animator.keyframes) {
- let rounded = roundToN(keyframe.time, 20)
- if (rounded === keyframe.time) continue
- if (rounded === lastTime) rounded += 0.05
- keyframe.time = rounded
- lastTime = rounded
- }
- }
- }
- return this
- }
-
- Blockbench.Animation.prototype.compileBedrockAnimation = function (this: _Animation): any {
- const tag = context.compileBedrockAnimation.call(this)
- if (Format === ajModelFormat) {
- // console.log('compileBedrockAnimation', this)
- tag.affected_bones = this.affected_bones
- tag.affected_bones_is_a_whitelist = this.affected_bones_is_a_whitelist
- }
- return tag
- }
-
- return context
- },
- context => {
- context.propertyAffectedBones?.delete()
- context.propertyAffectedBonesIsAWhitelist?.delete()
- Blockbench.Animation.prototype.extend = context.extend
- Blockbench.Animation.prototype.setLength = context.setLength
- Blockbench.Animation.prototype.compileBedrockAnimation = context.compileBedrockAnimation
- }
-)
diff --git a/src/mods/animationPropertiesAction.ts b/src/mods/animationPropertiesAction.ts
new file mode 100644
index 00000000..a5a1878c
--- /dev/null
+++ b/src/mods/animationPropertiesAction.ts
@@ -0,0 +1,28 @@
+import { isCurrentFormat } from '../blueprintFormat'
+import { PACKAGE } from '../constants'
+import { openAnimationPropertiesDialog } from '../interface/animationPropertiesDialog'
+import { createBlockbenchMod } from '../util/moddingTools'
+
+createBlockbenchMod(
+ `${PACKAGE.name}:animationPropertiesAction`,
+ {
+ originalOpen: Blockbench.Animation.prototype.propertiesDialog,
+ },
+ context => {
+ Blockbench.Animation.prototype.propertiesDialog = function (this: _Animation) {
+ if (isCurrentFormat()) {
+ if (!Blockbench.Animation.selected) {
+ Blockbench.showQuickMessage('No animation selected')
+ return
+ }
+ openAnimationPropertiesDialog(Blockbench.Animation.selected)
+ } else {
+ context.originalOpen.call(this)
+ }
+ }
+ return context
+ },
+ context => {
+ Blockbench.Animation.prototype.propertiesDialog = context.originalOpen
+ }
+)
diff --git a/src/mods/animationPropertiesMod.ts b/src/mods/animationPropertiesMod.ts
new file mode 100644
index 00000000..e7fa95b8
--- /dev/null
+++ b/src/mods/animationPropertiesMod.ts
@@ -0,0 +1,76 @@
+import { isCurrentFormat } from '../blueprintFormat'
+import { PACKAGE } from '../constants'
+import { roundToNth } from '../util/misc'
+import { ContextProperty, createBlockbenchMod } from '../util/moddingTools'
+import { translate } from '../util/translation'
+
+export const DEFAULT_SNAPPING_VALUE = 20
+export const MINIMUM_ANIMATION_LENGTH = 0.05
+
+createBlockbenchMod(
+ `${PACKAGE.name}:animationDefaultPropertiesMod`,
+ {
+ originalExtend: Blockbench.Animation.prototype.extend,
+ originalSetLength: Blockbench.Animation.prototype.setLength,
+ },
+ context => {
+ Blockbench.Animation.prototype.extend = function (
+ this: _Animation,
+ data: AnimationOptions
+ ) {
+ context.originalExtend.call(this, data)
+ this.snapping = DEFAULT_SNAPPING_VALUE
+ if (isCurrentFormat()) {
+ this.length = Math.max(this.length, MINIMUM_ANIMATION_LENGTH)
+ for (const animator of Object.values(this.animators)) {
+ if (!animator) continue
+ let lastTime = -Infinity
+ for (const kf of animator.keyframes) {
+ let rounded = roundToNth(kf.time, DEFAULT_SNAPPING_VALUE)
+ if (rounded === kf.time) continue
+ if (rounded === lastTime) rounded += 0.05
+ kf.time = rounded
+ lastTime = rounded
+ }
+ }
+ }
+ return this
+ }
+
+ Blockbench.Animation.prototype.setLength = function (this: _Animation, length?: number) {
+ if (isCurrentFormat()) {
+ length = Math.max(length || this.length, MINIMUM_ANIMATION_LENGTH)
+ }
+ return context.originalSetLength.call(this, length)
+ }
+
+ return context
+ },
+ context => {
+ Blockbench.Animation.prototype.extend = context.originalExtend
+ Blockbench.Animation.prototype.setLength = context.originalSetLength
+ }
+)
+
+createBlockbenchMod(
+ `${PACKAGE.name}:animationPropertiesMod`,
+ {
+ excludedBonesProperty: undefined as ContextProperty<'array'>,
+ },
+ context => {
+ context.excludedBonesProperty = new Property(
+ Blockbench.Animation,
+ 'array',
+ 'excluded_bones',
+ {
+ condition: () => isCurrentFormat(),
+ label: translate('animation.excluded_bones'),
+ default: [],
+ }
+ )
+ return context
+ },
+ context => {
+ context.excludedBonesProperty?.delete()
+ }
+)
diff --git a/src/mods/boneAnimatorMod.ts b/src/mods/boneAnimatorMod.ts
deleted file mode 100644
index e6795c4d..00000000
--- a/src/mods/boneAnimatorMod.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { ajModelFormat } from '../modelFormat'
-import { createBlockbenchMod } from '../util/moddingTools'
-// Cube.all[0].mesh.material = Canvas.wireframeMaterial
-
-createBlockbenchMod(
- 'animated_java:animation/affected_bones',
- {
- displayFrame: BoneAnimator.prototype.displayFrame,
- },
- context => {
- BoneAnimator.prototype.displayFrame = function (this: BoneAnimator, multiplier: number) {
- // if (Format === ajModelFormat) {
- // const includes = this.animation.affected_bones.find(v => v.value === this.uuid)
- // if (
- // (!includes && this.animation.affected_bones_is_a_whitelist) ||
- // (includes && !this.animation.affected_bones_is_a_whitelist)
- // ) {
- // const bone = this.getGroup()
- // if (bone) {
- // // console.log(this.uuid, 'not included')
- // bone.children.forEach(function recurse(child) {
- // if (child instanceof Group) child.children.forEach(recurse)
- // else if (child instanceof Cube)
- // child.mesh.material = Canvas.wireframeMaterial
- // })
- // }
- // return
- // }
- // }
- return context.displayFrame.call(this, multiplier)
- }
-
- return context
- },
- context => {
- BoneAnimator.prototype.displayFrame = context.displayFrame
- }
-)
diff --git a/src/mods/bonePropertiesMod.ts b/src/mods/bonePropertiesMod.ts
new file mode 100644
index 00000000..8288edcf
--- /dev/null
+++ b/src/mods/bonePropertiesMod.ts
@@ -0,0 +1,21 @@
+import { isCurrentFormat as condition } from '../blueprintFormat'
+import { PACKAGE } from '../constants'
+import { type ContextProperty, createBlockbenchMod } from '../util/moddingTools'
+
+createBlockbenchMod(
+ `${PACKAGE.name}:boneProperties`,
+ {
+ configs: undefined as ContextProperty<'instance'>,
+ },
+ context => {
+ context.configs = new Property(Group, 'instance', 'configs', {
+ condition,
+ default: { default: undefined, variants: {} },
+ })
+
+ return context
+ },
+ context => {
+ context.configs?.delete()
+ }
+)
diff --git a/src/mods/cameraPluginMod.ts b/src/mods/cameraPluginMod.ts
deleted file mode 100644
index 67533a51..00000000
--- a/src/mods/cameraPluginMod.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { clearInterval } from 'timers'
-import { CAMERA_CONFIG_ACTION } from '../ui/ajCameraConfig'
-import { createBlockbenchMod } from '../util/moddingTools'
-
-createBlockbenchMod(
- 'animated_java:cameraPluginMod',
- {
- installed: false,
- interval: undefined as NodeJS.Timeout | undefined,
- subContext: {} as any,
- },
- context => {
- context.interval = setInterval(() => {
- if (OutlinerElement.types.camera) {
- context.subContext = inject()
- context.installed = true
- }
- }, 50)
- return context
- },
- context => {
- if (context.installed && OutlinerElement.types.camera) {
- extract(context.subContext)
- context.installed = false
- }
- clearInterval(context.interval)
- }
-)
-
-function inject() {
- const camera = OutlinerElement.types.camera
- const context = {
- structure: [...camera.prototype.menu!.structure],
- entity_type: undefined as Property | undefined,
- nbt: undefined as Property | undefined,
- }
- camera.prototype.menu!.structure.splice(1, 0, CAMERA_CONFIG_ACTION)
- context.entity_type = new Property(camera, 'string', 'entity_type', {
- default: 'minecraft:armor_stand',
- })
- context.nbt = new Property(camera, 'string', 'nbt', {
- default: '{}',
- })
- return context
-}
-
-function extract(context: any) {
- const camera = OutlinerElement.types.camera
- camera.prototype.menu!.structure = context.structure
- context.entity_type.delete()
- context.nbt.delete()
-}
diff --git a/src/mods/cubeFaceMod.ts b/src/mods/cubeFaceMod.ts
deleted file mode 100644
index 32064621..00000000
--- a/src/mods/cubeFaceMod.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { ajModelFormat } from '../modelFormat'
-import { createBlockbenchMod } from '../util/moddingTools'
-
-createBlockbenchMod(
- 'animated_java:cube_face/variants',
- {
- originalGetTexture: CubeFace.prototype.getTexture,
- },
- context => {
- CubeFace.prototype.getTexture = function (this: CubeFace) {
- if (
- Format === ajModelFormat &&
- this.texture !== undefined &&
- this.texture !== null &&
- this.texture !== false
- ) {
- const variant = Project!.animated_java_variants?.selectedVariant
- if (variant) {
- const uuid = variant.getMappedUuid(this.texture)
- if (uuid && this.cube.parent instanceof Group) {
- const included = variant.affectedBones.find(
- v => v.value === (this.cube.parent as Group).uuid
- )
- if (
- (included && variant.affectedBonesIsAWhitelist) ||
- (!included && !variant.affectedBonesIsAWhitelist)
- )
- if (uuid) return Texture.all.find(t => t.uuid === uuid) || null
- }
- }
- }
- return context.originalGetTexture.call(this)
- }
- return context
- },
- context => {
- CubeFace.prototype.getTexture = context.originalGetTexture
- }
-)
diff --git a/src/mods/cubeMod.ts b/src/mods/cubeMod.ts
deleted file mode 100644
index 690dd5b8..00000000
--- a/src/mods/cubeMod.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-import { ajModelFormat } from '../modelFormat'
-import { createBlockbenchMod, createPropertySubscribable } from '../util/moddingTools'
-
-const ERROR_OUTLINE_MATERIAL = Canvas.outlineMaterial.clone()
-ERROR_OUTLINE_MATERIAL.color = new THREE.Color(1, 0, 0)
-
-let toast: Deletable | undefined
-function showInvalidCubeToast() {
- if (!toast)
- toast = Blockbench.showToastNotification({
- text: 'Some Cubes have invalid rotations!',
- color: 'var(--color-error)',
- icon: 'error',
- click: () => {
- validateAllCubes()
- return false
- },
- })
-}
-
-function validateCube(cube: Cube) {
- // Validate a cube by checking if the rotation is in [-45, -22.5, 0, 22.5, 45] and that it's only rotated on a single axis
- if (
- !(
- (cube.rotation.allAre(i => i === 0) ||
- cube.rotation.filter(v => v !== 0).length === 1) &&
- cube.rotation.every(v => [-45, -22.5, 0, 22.5, 45].includes(v))
- )
- ) {
- cube.mesh.outline.material = ERROR_OUTLINE_MATERIAL
- cube.forceVisible = true
- showInvalidCubeToast()
- return false
- } else if (cube.mesh.outline.material === ERROR_OUTLINE_MATERIAL) {
- cube.mesh.outline.material = Canvas.outlineMaterial
- cube.forceVisible = false
- }
- return true
-}
-
-function validateAllCubes() {
- const cubes = Cube.all.map(cube => validateCube(cube))
- if (toast && cubes.every(v => v)) {
- toast.delete()
- toast = undefined
- }
-}
-
-export function getInvalidCubes() {
- return Cube.all.filter(cube => !validateCube(cube))
-}
-
-export function createChaos(chaosLevel = 1000) {
- for (let i = 0; i < chaosLevel; i++) {
- setTimeout(() => {
- const cube = Cube.all[Math.floor(Math.random() * Cube.all.length)]
- cube.rotation = cube.rotation.map(() => Math.random() * 360) as ArrayVector3
- Canvas.updateAll()
- }, i * 10)
- }
- return new Promise(resolve => setTimeout(resolve, chaosLevel * 10))
-}
-
-createBlockbenchMod(
- 'animated_java:cube_verifier',
- {},
- () => {
- return setInterval(() => {
- if (Format === ajModelFormat) validateAllCubes()
- }, 1000)
- },
- context => {
- clearInterval(context)
- }
-)
-
-createBlockbenchMod(
- 'animated_java:cube',
- {
- originalInit: Cube.prototype.init,
- },
- context => {
- Cube.prototype.init = function (this: Cube) {
- const result = context.originalInit.call(this)
-
- this.forceVisible = false
-
- const [visibleGetter] = createPropertySubscribable(this.mesh.outline, 'visible')
- visibleGetter.subscribe(({ storage }) => {
- if (Format === ajModelFormat) storage.value = this.forceVisible || storage.value
- })
-
- return result
- }
- return context
- },
- context => {
- Cube.prototype.init = context.originalInit
- }
-)
-
-// createBlockbenchMod(
-// 'animated_java:cube.preview_controller',
-// {
-// updateVisibility: NodePreviewController.prototype.updateVisibility,
-// updateTransform: NodePreviewController.prototype.updateTransform,
-// },
-// context => {
-// NodePreviewController.prototype.updateTransform = function (
-// this: NodePreviewController,
-// element: OutlinerNode
-// ) {
-// context.updateTransform.call(this, element)
-// if (Format === ajModelFormat && element instanceof Cube) validateCube(element)
-// }
-
-// return context
-// },
-// context => {
-// NodePreviewController.prototype.updateVisibility = context.updateVisibility
-// NodePreviewController.prototype.updateTransform = context.updateTransform
-// }
-// )
diff --git a/src/mods/cubeOutlineMod.ts b/src/mods/cubeOutlineMod.ts
new file mode 100644
index 00000000..c66ff727
--- /dev/null
+++ b/src/mods/cubeOutlineMod.ts
@@ -0,0 +1,77 @@
+import { isCurrentFormat } from '../blueprintFormat'
+import { PACKAGE } from '../constants'
+import { createBlockbenchMod, createPropertySubscribable } from '../util/moddingTools'
+
+const ERROR_OUTLINE_MATERIAL = Canvas.outlineMaterial.clone()
+ERROR_OUTLINE_MATERIAL.color.set('#ff0000')
+
+function isCubeValid(cube: Cube) {
+ // Cube is automatically valid if it has no rotation
+ if (cube.rotation[0] === 0 && cube.rotation[1] === 0 && cube.rotation[2] === 0) {
+ return true
+ }
+ const rotation = cube.rotation[0] + cube.rotation[1] + cube.rotation[2]
+ // prettier-ignore
+ if (
+ // Make sure the cube is rotated in only one axis by adding all the rotations together, and checking if the sum is equal to one of the rotations.
+ (
+ rotation === cube.rotation[0] ||
+ rotation === cube.rotation[1] ||
+ rotation === cube.rotation[2]
+ )
+ &&
+ // Make sure the cube is rotated in one of the allowed 22.5 degree increments
+ (
+ rotation === -45 ||
+ rotation === -22.5 ||
+ rotation === 0 ||
+ rotation === 22.5 ||
+ rotation === 45
+ )
+ ) {
+ return true
+ }
+ return false
+}
+
+createBlockbenchMod(
+ `${PACKAGE.name}:cubeOutlineMod`,
+ {
+ originalUpdateTransform: Cube.preview_controller.updateTransform,
+ originalInit: Cube.prototype.init,
+ },
+ context => {
+ Cube.preview_controller.updateTransform = function (cube: Cube) {
+ if (isCurrentFormat()) {
+ const isValid = isCubeValid(cube)
+ if (cube.rotationInvalid && isValid) {
+ cube.mesh.outline.material = Canvas.outlineMaterial
+ cube.rotationInvalid = false
+ } else if (!cube.rotationInvalid && !isValid) {
+ cube.mesh.outline.material = ERROR_OUTLINE_MATERIAL
+ cube.rotationInvalid = true
+ }
+ }
+ context.originalUpdateTransform.call(this, cube)
+ }
+
+ Cube.prototype.init = function (this: Cube) {
+ const cube = context.originalInit.call(this)
+
+ cube.rotationInvalid = false
+
+ const [get] = createPropertySubscribable(this.mesh.outline, 'visible')
+ get.subscribe(({ storage }) => {
+ if (isCurrentFormat()) storage.value = this.rotationInvalid || storage.value
+ })
+
+ return cube
+ }
+
+ return context
+ },
+ context => {
+ Cube.preview_controller.updateTransform = context.originalUpdateTransform
+ Cube.prototype.init = context.originalInit
+ }
+)
diff --git a/src/mods/customKeyframesMod.ts b/src/mods/customKeyframesMod.ts
new file mode 100644
index 00000000..1d67b99d
--- /dev/null
+++ b/src/mods/customKeyframesMod.ts
@@ -0,0 +1,211 @@
+import { BLUEPRINT_FORMAT, isCurrentFormat } from '../blueprintFormat'
+import { events } from '../util/events'
+import { translate } from '../util/translation'
+import { Variant } from '../variants'
+
+const DEFAULT_CHANNELS = { ...EffectAnimator.prototype.channels }
+const DEFAULT_EFFECT_DISPLAY_FRAME = EffectAnimator.prototype.displayFrame
+// const DEFAULT_BONE_DISPLAY_FRAME = BoneAnimator.prototype.displayFrame
+export const CUSTOM_CHANNELS = ['variant', 'commands']
+
+let installed = false
+
+function injectCustomKeyframes() {
+ if (installed) return
+
+ // Add custom keyframe channels
+ EffectAnimator.addChannel('variant', {
+ name: translate('effect_animator.timeline.variant'),
+ mutable: true,
+ max_data_points: 1,
+ })
+ // EffectAnimator.addChannel('commands', {
+ // name: translate('effect_animator.timeline.commands'),
+ // mutable: true,
+ // max_data_points: 1,
+ // })
+ // BoneAnimator.addChannel('commands', {
+ // name: translate('effect_animator.timeline.commands'),
+ // mutable: true,
+ // transform: true,
+ // max_data_points: 1,
+ // })
+
+ // Add custom keyframe properties to the KeyframeDataPoint class
+ const variantKeyframeDataPoint = new Property(KeyframeDataPoint, 'string', 'variant', {
+ label: translate('effect_animator.keyframe.variant'),
+ condition: datapoint => datapoint.keyframe.channel === 'variant',
+ exposed: false,
+ })
+ Object.defineProperty(variantKeyframeDataPoint, 'default', {
+ get() {
+ return Variant.getDefault().uuid
+ },
+ })
+ new Property(KeyframeDataPoint, 'string', 'commands', {
+ label: translate('effect_animator.timeline.commands'),
+ default: '',
+ condition: datapoint => datapoint.keyframe.channel === 'commands',
+ exposed: false,
+ })
+ new Property(KeyframeDataPoint, 'string', 'execute_condition', {
+ label: translate('effect_animator.timeline.execute_condition'),
+ default: '',
+ condition: datapoint => CUSTOM_CHANNELS.includes(datapoint.keyframe.channel as string),
+ exposed: false,
+ })
+ new Property(KeyframeDataPoint, 'boolean', 'repeat', {
+ label: translate('effect_animator.timeline.repeat'),
+ default: false,
+ condition: datapoint => CUSTOM_CHANNELS.includes(datapoint.keyframe.channel as string),
+ exposed: false,
+ })
+ new Property(KeyframeDataPoint, 'number', 'repeat_frequency', {
+ label: translate('effect_animator.timeline.repeat_frequency'),
+ default: 1,
+ condition: datapoint => CUSTOM_CHANNELS.includes(datapoint.keyframe.channel as string),
+ exposed: false,
+ })
+
+ // Remove default keyframe channels (except sound)
+ for (const channel of Object.keys(DEFAULT_CHANNELS)) {
+ if (channel === 'sound') continue
+ delete EffectAnimator.prototype.channels[channel]
+ }
+
+ // Modify the displayFrame method to handle custom keyframes
+ EffectAnimator.prototype.displayFrame = function (this: EffectAnimator, inLoop: boolean) {
+ // Default Blockbench Sound keyframe handling
+ if (inLoop && !this.muted.sound) {
+ this.sound.forEach((kf: _Keyframe) => {
+ const diff = this.animation.time - kf.time
+ if (diff < 0) return
+
+ let media = Timeline.playing_sounds.find(s => s.keyframe_id == kf.uuid)
+ if (diff >= 0 && diff < (1 / 60) * (Timeline.playback_speed / 100) && !media) {
+ if (kf.data_points[0].file && !kf.cooldown) {
+ media = new Audio(kf.data_points[0].file as string)
+ media.keyframe_id = kf.uuid
+ media.playbackRate = Math.clamp(Timeline.playback_speed / 100, 0.1, 4.0)
+ media.volume = Math.clamp(settings.volume.value / 100, 0, 1)
+ media.play().catch(() => {
+ /**/
+ })
+ Timeline.playing_sounds.push(media)
+ media.onended = function () {
+ Timeline.playing_sounds.remove(media)
+ }
+
+ kf.cooldown = true
+ setTimeout(() => {
+ delete kf.cooldown
+ }, 400)
+ }
+ } else if (diff > 0) {
+ media = Timeline.playing_sounds.find(s => s.keyframe_id == kf.uuid)
+ if (media && Math.abs(media.currentTime - diff) > 0.08) {
+ // Resync
+ media.currentTime = diff
+ media.playbackRate = Math.clamp(Timeline.playback_speed / 100, 0.1, 4.0)
+ }
+ }
+ })
+ }
+
+ if (!(Project && isCurrentFormat())) return
+ if (!this.muted.variant) {
+ let after, before, result: _Keyframe | undefined
+
+ for (const kf of this.variant as _Keyframe[]) {
+ if (kf.time < this.animation.time) {
+ if (!before || kf.time > before.time) {
+ before = kf
+ }
+ } else {
+ if (!after || kf.time < after.time) {
+ after = kf
+ }
+ }
+ }
+
+ if (after && after.time === this.animation.time) {
+ result = after
+ } else if (before) {
+ result = before
+ } else if (after) {
+ result = this.variant.at(-1)
+ }
+
+ if (result) {
+ const variant = Variant.all.find(v => v.uuid === result.data_points[0].variant)
+ if (!variant) console.error('Variant', result.data_points[0].variant, 'not found.')
+ variant?.select()
+ }
+ }
+
+ this.last_displayed_time = this.animation.time
+ }
+
+ installed = true
+}
+
+function extractCustomKeyframes() {
+ if (!installed) return
+
+ for (const channel of Object.keys(DEFAULT_CHANNELS)) {
+ if (channel === 'sound') continue
+ EffectAnimator.prototype.channels[channel] = DEFAULT_CHANNELS[channel]
+ }
+
+ KeyframeDataPoint.properties.variant?.delete()
+ KeyframeDataPoint.properties.commands?.delete()
+ KeyframeDataPoint.properties.execute_condition?.delete()
+ KeyframeDataPoint.properties.repeat?.delete()
+ KeyframeDataPoint.properties.repeat_frequency?.delete()
+
+ delete EffectAnimator.prototype.channels.variant
+ delete EffectAnimator.prototype.variant
+ // delete EffectAnimator.prototype.channels.commands
+ // delete EffectAnimator.prototype.commands
+ delete BoneAnimator.prototype.channels.commands
+ delete BoneAnimator.prototype.commands
+
+ EffectAnimator.prototype.displayFrame = DEFAULT_EFFECT_DISPLAY_FRAME
+ // BoneAnimator.prototype.displayFrame = DEFAULT_BONE_DISPLAY_FRAME
+
+ installed = false
+}
+
+events.PRE_SELECT_PROJECT.subscribe(project => {
+ if (project.format.id === BLUEPRINT_FORMAT.id) {
+ injectCustomKeyframes()
+ } else {
+ extractCustomKeyframes()
+ }
+})
+
+function keyframeValueSetterFactory(channel: string) {
+ return function (kf: _Keyframe, value: ValueType) {
+ if (kf.data_points.length === 0) {
+ kf.data_points.push(new KeyframeDataPoint(kf))
+ }
+ kf.data_points[0][channel] = value
+ }
+}
+
+function keyframeValueGetterFactory(channel: string) {
+ return function (kf: _Keyframe) {
+ return kf.data_points.at(0)?.[channel] as ValueType | undefined
+ }
+}
+
+export const setKeyframeVariant = keyframeValueSetterFactory('variant')
+export const getKeyframeVariant = keyframeValueGetterFactory('variant')
+export const setKeyframeCommands = keyframeValueSetterFactory('commands')
+export const getKeyframeCommands = keyframeValueGetterFactory('commands')
+export const setKeyframeExecuteCondition = keyframeValueSetterFactory('execute_condition')
+export const getKeyframeExecuteCondition = keyframeValueGetterFactory('execute_condition')
+export const setKeyframeRepeat = keyframeValueSetterFactory('repeat')
+export const getKeyframeRepeat = keyframeValueGetterFactory('repeat')
+export const setKeyframeRepeatFrequency = keyframeValueSetterFactory('repeat_frequency')
+export const getKeyframeRepeatFrequency = keyframeValueGetterFactory('repeat_frequency')
diff --git a/src/mods/elementToolbarsMod.ts b/src/mods/elementToolbarsMod.ts
new file mode 100644
index 00000000..1f973840
--- /dev/null
+++ b/src/mods/elementToolbarsMod.ts
@@ -0,0 +1,20 @@
+import { PACKAGE } from '../constants'
+import { TextDisplay } from '../outliner/textDisplay'
+import { createBlockbenchMod } from '../util/moddingTools'
+
+createBlockbenchMod(
+ `${PACKAGE.name}:element_toolbars`,
+ {
+ originalCondition: Toolbars.element_origin.condition,
+ },
+ context => {
+ Toolbars.element_origin.condition = function () {
+ if (TextDisplay.selected) return false
+ return context.originalCondition()
+ }
+ return context
+ },
+ context => {
+ Toolbars.element_origin.condition = context.originalCondition
+ }
+)
diff --git a/src/mods/exportOverActionMod.ts b/src/mods/exportOverActionMod.ts
new file mode 100644
index 00000000..3ee3b3f1
--- /dev/null
+++ b/src/mods/exportOverActionMod.ts
@@ -0,0 +1,32 @@
+import { BLUEPRINT_CODEC, BLUEPRINT_FORMAT } from '../blueprintFormat'
+import { PACKAGE } from '../constants'
+import { createBlockbenchMod } from '../util/moddingTools'
+
+createBlockbenchMod(
+ `${PACKAGE.name}:exportOverAction`,
+ {
+ action: BarItems.export_over as Action,
+ originalClick: (BarItems.export_over as Action).click,
+ },
+ context => {
+ context.action.click = (event: Event) => {
+ if (!Project || !Format) return
+ if (Format.id === BLUEPRINT_FORMAT.id) {
+ if (Project.save_path || Project.export_path) {
+ BLUEPRINT_CODEC.write(
+ BLUEPRINT_CODEC.compile(),
+ Project.save_path || Project.export_path
+ )
+ } else {
+ BLUEPRINT_CODEC.export()
+ }
+ } else {
+ context.originalClick.call(context.action, event)
+ }
+ }
+ return context
+ },
+ context => {
+ context.action.click = context.originalClick
+ }
+)
diff --git a/src/mods/functions.molang b/src/mods/functions.molang
new file mode 100644
index 00000000..3bab9c8d
--- /dev/null
+++ b/src/mods/functions.molang
@@ -0,0 +1,165 @@
+smoothmin(a, b, k): |-
+ v.h = math.clamp(0.5 + (0.5 * (a - b) / k), 0, 1);
+ return math.lerp(a, b, v.h) - (k * v.h * (1 - v.h));
+
+smoothclamp(value, min, max, k): |-
+ return lunar.smoothmin(lunar.smoothmin(value, min, -k), max, k);
+
+lopsided_wave(value, lopside_mag): |-
+ return math.sin(value + math.cos(value) * lopside_mag);
+
+easeinsine(progress): |-
+ return 1 - math.cos((progress * 180) / 2);
+
+easeoutsine(progress): |-
+ return math.sin((progress * 180) / 2);
+
+easeinoutsine(progress): |-
+ return(math.cos(180 * progress) - 1) * -1 / 2;
+
+easeinquad(progress): |-
+ return progress * progress;
+
+easeoutquad(progress): |-
+ return 1 - (1 - progress) * (1 - progress);
+
+easeinoutquad(progress): |-
+ return progress < 0.5
+ ? 2 * progress * progress
+ : 1 - math.pow(-2 * progress + 2, 2) / 2;
+
+easeincubic(progress): |-
+ return progress * progress * progress;
+
+easeoutcubic(progress): |-
+ return 1 - math.pow(1 - progress, 3);
+
+easeinoutcubic(progress): |-
+ return progress < 0.5
+ ? 4 * progress * progress * progress
+ : 1 - math.pow(-2 * progress + 2, 3) / 2;
+
+easeinquart(progress): |-
+ return progress * progress * progress * progress;
+
+easeoutquart(progress): |-
+ return 1 - math.pow(1 - progress, 4);
+
+easeinoutquart(progress): |-
+ return progress < 0.5
+ ? 8 * progress * progress * progress * progress
+ : 1 - math.pow(-2 * progress + 2, 4) / 2;
+
+easeinquint(progress): |-
+ return progress * progress * progress * progress * progress;
+
+easeoutquint(progress): |-
+ return 1 - math.pow(1 - progress, 5);
+
+easeinoutquint(progress): |-
+ return progress < 0.5
+ ? 16 * progress * progress * progress * progress * progress
+ : 1 - math.pow(-2 * progress + 2, 5) / 2;
+
+easeinexpo(progress): |-
+ return progress == 0
+ ? 0
+ : math.pow(2, 10 * progress - 10);
+
+easeoutexpo(progress): |-
+ return progress == 1
+ ? 1
+ : 1 - math.pow(2, -10 * progress);
+
+easeinoutexpo(progress): |-
+ return progress == 0
+ ? 0
+ : progress == 1
+ ? 1
+ : progress < 0.5
+ ? math.pow(2, 20 * progress - 10) / 2
+ : (2 - math.pow(2, -20 * progress + 10)) / 2;
+
+easeincirc(progress): |-
+ return math.sqrt(1 - math.pow(progress - 1, 2));
+
+easeoutcirc(progress): |-
+ return math.sqrt(1 - math.pow(progress - 1, 2));
+
+easeinoutcirc(progress): |-
+ return progress < 0.5
+ ? (1 - math.sqrt(1 - math.pow(2 * progress, 2))) / 2
+ : (math.sqrt(1 - math.pow(-2 * progress + 2, 2)) + 1) / 2;
+
+easeinback(progress, overshoot): |-
+ t.overshoot=1.70158 * (overshoot ?? 1);
+ return (t.overshoot + 1) * progress * progress * progress - t.overshoot * progress * progress;
+
+easeoutback(progress, overshoot): |-
+ t.overshoot=1.70158 * (overshoot ?? 1);
+ return 1 + (t.overshoot + 1) * math.pow(progress - 1, 3) + t.overshoot * math.pow(progress - 1, 2);
+
+easeinoutback(progress, overshoot): |-
+ t.overshoot=1.70158 * (overshoot ?? 1);
+ t.c2 = t.overshoot + 1;
+ return progress < 0.5
+ ? (math.pow(2 * progress, 2) * ((t.c2 + 1) * 2 * progress - t.c2)) / 2
+ : (math.pow(2 * progress - 2, 2) * ((t.c2 + 1) * (progress * 2 - 2) + t.c2) + 2) / 2;
+
+easeinelastic(progress): |-
+ return progress == 0
+ ? 0
+ : progress == 1
+ ? 1
+ : -math.pow(2, 10 * progress - 10) * math.sin((progress * 10 - 10.75) * 90);
+
+easeoutelastic(progress): |-
+ return progress == 0
+ ? 0
+ : progress == 1
+ ? 1
+ : math.pow(2, -10 * progress) * math.sin((progress * 10 - 0.75) * 90) + 1;
+
+easeinoutelastic(progress): |-
+ return progress == 0
+ ? 0
+ : progress == 1
+ ? 1
+ : progress < 0.5
+ ? (math.pow(2, 20 * progress - 10) * math.sin((20 * progress - 11.125) * 90) * -1) / 2
+ : (math.pow(2, -20 * progress + 10) * math.sin((20 * progress - 11.125) * 90)) / 2 + 1;
+
+easeinbounce(progress): |-
+ return 1 - easeoutbounce(1 - progress);
+
+easeoutbounce(progress): |-
+ t.n1=7.5625;
+ t.d1=2.75;
+ return progress < 1 / t.d1
+ ? (t.n1 * progress * progress)
+ : progress < 2 / t.d1
+ ? {
+ progress = progress - 1.5 / t.d1;
+ t.n1 * progress * progress + 0.75;
+ }
+ : progress < 2.5 / t.d1
+ ? {
+ progress = progress - 2.25 / t.d1;
+ t.n1 * progress * progress + 0.9375;
+ }
+ : {
+ progress = progress - 2.625 / t.d1;
+ t.n1 * progress * progress + 0.984375;
+ };
+
+easeinoutbounce(progress): |-
+ return progress < 0.5
+ ? 1-easeinbounce(1-2 * progress) / 2
+ : 1 + easeoutbounce(2 * progress - 1) / 2;
+
+linear_wave(progress, hang): |-
+ t.progress = progress * (math.pi / 180) + math.pi * 600;
+ t.hang = hang * (math.pi / 180);
+ return math.mod(math.abs(t.progress), 2 * math.pi + t.hang * 2) > math.pi + t.hang
+ ? math.clamp(-math.mod(math.abs(t.progress), math.pi + t.hang) / math.pi + 1, 0, 1)
+ : math.clamp(math.mod(math.abs(t.progress), math.pi + t.hang) / math.pi, 0, 1);
\ No newline at end of file
diff --git a/src/mods/groupMod.ts b/src/mods/groupContextMenuMod.ts
similarity index 51%
rename from src/mods/groupMod.ts
rename to src/mods/groupContextMenuMod.ts
index b32c8e6b..3ba80590 100644
--- a/src/mods/groupMod.ts
+++ b/src/mods/groupContextMenuMod.ts
@@ -1,23 +1,20 @@
-import { BONE_CONFIG_ACTION } from '../ui/ajBoneConfig'
+import { PACKAGE } from '../constants'
+import { BONE_CONFIG_ACTION } from '../interface/boneConfigDialog'
import { createBlockbenchMod } from '../util/moddingTools'
createBlockbenchMod(
- 'animated_java:group/force_valid_function_name',
+ `${PACKAGE.name}:groupContextMenu`,
{
menuStructure: Group.prototype.menu!.structure,
- nbtProperty: undefined as Property<'string'> | undefined,
},
context => {
const structure = [...context.menuStructure]
- structure.splice(5, 0, BONE_CONFIG_ACTION)
+ structure.splice(6, 0, BONE_CONFIG_ACTION)
Group.prototype.menu!.structure = structure
- context.nbtProperty = new Property(Group, 'string', 'nbt', { default: '{}' })
-
return context
},
context => {
- context.nbtProperty?.delete()
Group.prototype.menu!.structure = context.menuStructure
}
)
diff --git a/src/mods/groupNameMod.ts b/src/mods/groupNameMod.ts
new file mode 100644
index 00000000..4cbfc66f
--- /dev/null
+++ b/src/mods/groupNameMod.ts
@@ -0,0 +1,20 @@
+import { PACKAGE } from '../constants'
+import { toSafeFuntionName } from '../util/minecraftUtil'
+import { createBlockbenchMod } from '../util/moddingTools'
+
+createBlockbenchMod(
+ `${PACKAGE.name}:groupNameMod`,
+ {
+ originalRename: Group.prototype.saveName,
+ },
+ context => {
+ Group.prototype.saveName = function (this: Group, save?: boolean) {
+ this.name = toSafeFuntionName(this.name)
+ return context.originalRename.call(this, save)
+ }
+ return context
+ },
+ context => {
+ Group.prototype.rename = context.originalRename
+ }
+)
diff --git a/src/mods/keyframeMod.ts b/src/mods/keyframeMod.ts
index 3ae386ea..1eceee90 100644
--- a/src/mods/keyframeMod.ts
+++ b/src/mods/keyframeMod.ts
@@ -1,206 +1,21 @@
-import { ajModelFormat } from '../modelFormat'
-import * as events from '../events'
-import { translate } from '../util/translation'
-// import { applyModelVariant } from '../variants'
-
-const oldEffectAnimatorDisplayFrame = EffectAnimator.prototype.displayFrame
-// const oldEffectAnimatorStartPreviousSounds = EffectAnimator.prototype.startPreviousSounds
-const OLD_CHANNELS = { ...EffectAnimator.prototype.channels }
-
-let installed = false
-
-export function injectCustomKeyframes() {
- if (installed) return
- // Add custom channels to Bone Animator
- // BoneAnimator.addChannel('commands', {
- // name: translate('animated_java.timeline.commands'),
- // mutable: false,
- // max_data_points: 2,
- // })
-
- // Add custom channels to Effect Animator
- // EffectAnimator.addChannel('animationStates', {
- // name: translate('animated_java.timeline.animation'),
- // mutable: false,
- // max_data_points: 2,
- // })
-
- EffectAnimator.addChannel('variants', {
- name: translate('animated_java.timeline.variant'),
- mutable: true,
- max_data_points: 2,
- })
-
- EffectAnimator.addChannel('commands', {
- name: translate('animated_java.timeline.commands'),
- mutable: false,
- max_data_points: 2,
- })
-
- // Add new KeyframeDataPoint properties
- new Property(KeyframeDataPoint, 'string', 'variant', {
- label: translate('animated_java.keyframe.variant'),
- default: 'default',
- condition: point => {
- return point.keyframe.channel === 'variants'
- },
- exposed: false,
- })
-
- new Property(KeyframeDataPoint, 'string', 'commands', {
- label: translate('animated_java.keyframe.commands'),
- condition: point => {
- return point.keyframe.channel === 'commands'
- },
- exposed: false,
- })
-
- // new Property(KeyframeDataPoint, 'string', 'animationState', {
- // label: translate('animated_java.keyframe.animationState'),
- // condition: point => {
- // return point.keyframe.channel === 'animationStates'
- // },
- // exposed: false,
- // })
-
- new Property(KeyframeDataPoint, 'string', 'executeCondition', {
- label: translate('animated_java.keyframe.executeCondition'),
- condition: point => {
- return ['animationStates', 'variants', 'commands'].includes(
- point.keyframe.channel as string
- )
- },
- exposed: false,
- })
-
- for (const channel of Object.keys(OLD_CHANNELS)) {
- if (channel === 'sound') continue
- delete EffectAnimator.prototype.channels[channel]
- }
-
- // Modify keyframe functionality
- EffectAnimator.prototype.displayFrame = function (this: EffectAnimator, inLoop: boolean) {
- // Default Blockbench Sound keyframe handling
- if (inLoop && !this.muted.sound) {
- this.sound.forEach((kf: _Keyframe) => {
- const diff = kf.time - this.animation.time
- if (diff >= 0 && diff < (1 / 60) * (Timeline.playback_speed / 100)) {
- if (kf.data_points[0].file && !kf.cooldown) {
- const media = new Audio(kf.data_points[0].file as string)
- media.playbackRate = Math.clamp(Timeline.playback_speed / 100, 0.1, 4.0)
- media.volume = Math.clamp(settings.volume.value / 100, 0, 1)
- media.play().catch(() => null)
- Timeline.playing_sounds.push(media)
- media.onended = function () {
- Timeline.playing_sounds.remove(media)
- }
- kf.cooldown = true
- setTimeout(() => {
- delete kf.cooldown
- }, 400)
- }
- }
- })
- }
-
- if (!Project || !Project.animated_java_variants) return
- if (!this.muted.variants) {
- let after, before, result: _Keyframe | undefined
-
- for (const kf of this.variants as _Keyframe[]) {
- if (kf.time < this.animation.time) {
- if (!before || kf.time > before.time) {
- before = kf
- }
- } else {
- if (!after || kf.time < after.time) {
- after = kf
- }
- }
- }
-
- if (after && after.time === this.animation.time) {
- result = after
- } else if (before) {
- result = before
- } else if (after) {
- result = this.variants.at(-1)
- }
-
- if (result) {
- const variant = Project.animated_java_variants.variants.find(
- v => result && v.uuid === result.data_points[0].variant
- )
- Project.animated_java_variants.select(variant)
- }
+import { PACKAGE } from '../constants'
+import { events } from '../util/events'
+import { createBlockbenchMod } from '../util/moddingTools'
+
+createBlockbenchMod(
+ `${PACKAGE.name}:keyframeSelectEventMod`,
+ {
+ originalKeyframeSelect: Blockbench.Keyframe.prototype.select,
+ },
+ context => {
+ Blockbench.Keyframe.prototype.select = function (this: _Keyframe, event: any) {
+ const kf = context.originalKeyframeSelect.call(this, event)
+ events.SELECT_KEYFRAME.dispatch(kf)
+ return kf
}
-
- this.last_displayed_time = this.animation.time
- }
-
- // EffectAnimator.prototype.startPreviousSounds = function (this: EffectAnimator) {
- // // Do nothing. Blockbench throws an error if this isn't overwritten.
- // }
-
- installed = true
-}
-
-export function extractCustomKeyframes() {
- if (!installed) return
- EffectAnimator.prototype.displayFrame = oldEffectAnimatorDisplayFrame
- // EffectAnimator.prototype.startPreviousSounds = oldEffectAnimatorStartPreviousSounds
-
- for (const channel of Object.keys(OLD_CHANNELS)) {
- if (channel === 'sound') continue
- EffectAnimator.prototype.channels[channel] = OLD_CHANNELS[channel]
- }
-
- KeyframeDataPoint.properties.variant?.delete()
- KeyframeDataPoint.properties.commands?.delete()
- // KeyframeDataPoint.properties.animationState?.delete()
- KeyframeDataPoint.properties.executeCondition?.delete()
-
- // delete BoneAnimator.prototype.channels.commands
- // delete BoneAnimator.prototype.commands
-
- delete EffectAnimator.prototype.channels.variants
- delete EffectAnimator.prototype.variants
- delete EffectAnimator.prototype.channels.commands
- delete EffectAnimator.prototype.commands
- // delete EffectAnimator.prototype.channels.animationStates
- // delete EffectAnimator.prototype.animationStates
-
- installed = false
-}
-
-events.EXTRACT_MODS.subscribe(() => extractCustomKeyframes())
-
-events.PRE_SELECT_PROJECT.subscribe(project => {
- if (project.format.id === ajModelFormat.id) {
- if (!installed) injectCustomKeyframes()
- } else {
- if (installed) extractCustomKeyframes()
+ return context
+ },
+ context => {
+ Blockbench.Keyframe.prototype.select = context.originalKeyframeSelect
}
-})
-
-function keyframeSetterFactory(channel: string) {
- return function (kf: _Keyframe, data: any) {
- const dataPoint = kf.data_points.at(0)
- if (dataPoint) dataPoint[channel] = data
- }
-}
-
-function keyframeGetterFactory(channel: string) {
- return function (kf: _Keyframe) {
- return kf.data_points.at(0)?.[channel] as string | undefined
- }
-}
-
-export const getKeyframeVariant = keyframeGetterFactory('variant')
-export const setKeyframeVariant = keyframeSetterFactory('variant')
-export const getKeyframeCommands = keyframeGetterFactory('commands')
-export const setKeyframeCommands = keyframeSetterFactory('commands')
-// export const getKeyframeAnimationState = keyframeGetterFactory('animationState')
-// export const setKeyframeAnimationState = keyframeSetterFactory('animationState')
-export const getKeyframeCondition = keyframeGetterFactory('executeCondition')
-export const setKeyframeCondition = keyframeSetterFactory('executeCondition')
+)
diff --git a/src/mods/locatorAnimatorMod.ts b/src/mods/locatorAnimatorMod.ts
new file mode 100644
index 00000000..94891074
--- /dev/null
+++ b/src/mods/locatorAnimatorMod.ts
@@ -0,0 +1,166 @@
+import { BLUEPRINT_FORMAT } from '../blueprintFormat'
+import { events } from '../util/events'
+import { translate } from '../util/translation'
+
+const DEFAULT_SHOW_MOTION_TRAIL = Animator.showMotionTrail
+const DEFAULT_PREVIEW = Animator.preview
+const DEFAULT_UPDATE_SELECTION = globalThis.updateSelection
+const DEFAULT_SELECT = Locator.prototype.select
+
+export class LocatorAnimator extends BoneAnimator {
+ private _name: string
+
+ public uuid: string
+ public element: Locator | undefined
+
+ constructor(uuid: string, animation: _Animation, name: string) {
+ super(uuid, animation, name)
+ this.uuid = uuid
+ this._name = name
+ }
+
+ getElement() {
+ this.element = OutlinerNode.uuids[this.uuid] as Locator
+ return this.element
+ }
+
+ select() {
+ this.getElement()
+ if (!this.element) {
+ unselectAllElements()
+ return this
+ }
+
+ if (this.element.locked) {
+ return this
+ }
+
+ if (this.element.selected !== true && this.element) {
+ this.element.select()
+ }
+ GeneralAnimator.prototype.select.call(this)
+
+ if (
+ this[Toolbox.selected.animation_channel] &&
+ ((Timeline.selected && Timeline.selected.length === 0) ||
+ (Timeline.selected && (Timeline.selected[0].animator as any)) !== this)
+ ) {
+ let nearest: _Keyframe | undefined
+ this[Toolbox.selected.animation_channel].forEach((kf: _Keyframe) => {
+ if (Math.abs(kf.time - Timeline.time) < 0.002) {
+ nearest = kf
+ }
+ })
+ if (nearest) {
+ nearest.select()
+ }
+ }
+
+ if (this.element && this.element.parent && this.element.parent !== 'root') {
+ this.element.parent.openUp()
+ }
+
+ return this
+ }
+
+ doRender() {
+ this.getElement()
+ return !!(this.element && this.element.mesh)
+ }
+
+ displayPosition(arr?: ArrayVector3, multiplier = 1) {
+ const bone = this.element!.mesh
+ if (arr) {
+ bone.position.x -= arr[0] * multiplier
+ bone.position.y += arr[1] * multiplier
+ bone.position.z += arr[2] * multiplier
+ }
+ return this
+ }
+
+ interpolate(): ArrayVector3 {
+ return [0, 0, 0]
+ }
+
+ displayFrame() {
+ if (!this.doRender()) return
+ this.getElement()
+ }
+
+ showMotionTrail() {
+ return
+ }
+}
+LocatorAnimator.prototype.type = 'locator'
+LocatorAnimator.prototype.channels = {
+ commands: {
+ name: translate('effect_animator.timeline.commands'),
+ mutable: true,
+ transform: true,
+ max_data_points: 1,
+ },
+}
+
+let installed = false
+
+function inject() {
+ if (installed) return
+
+ Locator.animator = LocatorAnimator as any
+
+ Animator.showMotionTrail = function (target?: Group) {
+ if (!target || target instanceof Locator) return
+ DEFAULT_SHOW_MOTION_TRAIL(target)
+ }
+ Animator.preview = function (inLoop?: boolean) {
+ DEFAULT_PREVIEW(inLoop)
+ if (
+ Mode.selected.id === Modes.options.animate.id &&
+ Outliner.selected[0] instanceof Locator
+ ) {
+ // @ts-ignore
+ Canvas.gizmos[0].visible = false
+ Transformer.visible = false
+ }
+ }
+ globalThis.updateSelection = function () {
+ DEFAULT_UPDATE_SELECTION()
+ if (
+ Mode.selected.id === Modes.options.animate.id &&
+ Outliner.selected[0] instanceof Locator
+ ) {
+ // @ts-ignore
+ Canvas.gizmos[0].visible = false
+ Transformer.visible = false
+ }
+ }
+ Locator.prototype.select = function (this: Locator, event?: any, isOutlinerClick?: boolean) {
+ const result = DEFAULT_SELECT.call(this, event, isOutlinerClick)
+ if (Animator.open && Blockbench.Animation.selected) {
+ Blockbench.Animation.selected.getBoneAnimator().select()
+ }
+ return result
+ }
+
+ installed = true
+}
+
+function extract() {
+ if (!installed) return
+ Locator.animator = undefined
+
+ Animator.showMotionTrail = DEFAULT_SHOW_MOTION_TRAIL
+ Animator.preview = DEFAULT_PREVIEW
+ globalThis.updateSelection = DEFAULT_UPDATE_SELECTION
+ Locator.prototype.select = DEFAULT_SELECT
+
+ installed = false
+}
+
+events.PRE_SELECT_PROJECT.subscribe(project => {
+ if (project.format.id === BLUEPRINT_FORMAT.id) {
+ inject()
+ } else {
+ extract()
+ }
+})
diff --git a/src/mods/locatorContextMenuMod.ts b/src/mods/locatorContextMenuMod.ts
new file mode 100644
index 00000000..9ac417a7
--- /dev/null
+++ b/src/mods/locatorContextMenuMod.ts
@@ -0,0 +1,20 @@
+import { PACKAGE } from '../constants'
+import { LOCATOR_CONFIG_ACTION } from '../interface/locatorConfigDialog'
+import { createBlockbenchMod } from '../util/moddingTools'
+
+createBlockbenchMod(
+ `${PACKAGE.name}:groupContextMenu`,
+ {
+ menuStructure: Locator.prototype.menu!.structure,
+ },
+ context => {
+ const structure = [...context.menuStructure]
+ structure.splice(6, 0, LOCATOR_CONFIG_ACTION)
+ Locator.prototype.menu!.structure = structure
+
+ return context
+ },
+ context => {
+ Locator.prototype.menu!.structure = context.menuStructure
+ }
+)
diff --git a/src/mods/locatorMod.ts b/src/mods/locatorMod.ts
deleted file mode 100644
index 55fd0b54..00000000
--- a/src/mods/locatorMod.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { LOCATOR_CONFIG_ACTION } from '../ui/ajLocatorConfig'
-import { createBlockbenchMod } from '../util/moddingTools'
-
-createBlockbenchMod(
- 'animated_java:locator/force_valid_function_name',
- {
- menuStructure: Locator.prototype.menu!.structure,
- entity_type: undefined as Property<'string'> | undefined,
- nbt: undefined as Property<'string'> | undefined,
- },
- context => {
- const structure = [...context.menuStructure]
- structure.splice(1, 0, LOCATOR_CONFIG_ACTION)
- Locator.prototype.menu!.structure = structure
-
- context.entity_type = new Property(Locator, 'string', 'entity_type', {
- default: 'minecraft:pig',
- })
- context.nbt = new Property(Locator, 'string', 'nbt', {
- default: '{}',
- })
-
- return context
- },
- context => {
- context.entity_type?.delete()
- context.nbt?.delete()
- Locator.prototype.menu!.structure = context.menuStructure
- }
-)
diff --git a/src/mods/locatorPropertiesMod.ts b/src/mods/locatorPropertiesMod.ts
new file mode 100644
index 00000000..21806c88
--- /dev/null
+++ b/src/mods/locatorPropertiesMod.ts
@@ -0,0 +1,20 @@
+import { isCurrentFormat as condition } from '../blueprintFormat'
+import { PACKAGE } from '../constants'
+import { type ContextProperty, createBlockbenchMod } from '../util/moddingTools'
+
+createBlockbenchMod(
+ `${PACKAGE.name}:locatorProperties`,
+ {
+ config: undefined as ContextProperty<'instance'>,
+ },
+ context => {
+ context.config = new Property(Locator, 'instance', 'config', {
+ condition,
+ default: undefined,
+ })
+ return context
+ },
+ context => {
+ context.config?.delete()
+ }
+)
diff --git a/src/mods/modeMod.ts b/src/mods/modeMod.ts
deleted file mode 100644
index f2ff135e..00000000
--- a/src/mods/modeMod.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import { ajModelFormat } from '../modelFormat'
-import { createBlockbenchMod } from '../util/moddingTools'
-import { Variant } from '../variants'
-import * as events from '../events'
-
-createBlockbenchMod(
- 'animated_java:modes.edit/variants',
- {
- originalEditSelect: Modes.options.edit.onSelect,
- originalEditUnselect: Modes.options.edit.onUnselect,
- },
- context => {
- let selectedVariant: Variant
- Modes.options.edit.onSelect = function (this: Mode) {
- if (
- Project &&
- Format === ajModelFormat &&
- Project.animated_java_variants &&
- Project.animated_java_variants.selectedVariant
- ) {
- if (selectedVariant) Project.animated_java_variants.select(selectedVariant)
- events.UPDATE_SELECTION.dispatch()
- }
- return context.originalEditSelect?.call(this)
- }
- Modes.options.edit.onUnselect = function () {
- if (
- Project &&
- Format === ajModelFormat &&
- Project.animated_java_variants &&
- Project.animated_java_variants.selectedVariant
- ) {
- selectedVariant = Project.animated_java_variants.selectedVariant
- Project.animated_java_variants.select()
- events.UPDATE_SELECTION.dispatch()
- }
- return context.originalEditUnselect?.call(this)
- }
- return context
- },
- context => {
- Modes.options.edit.onSelect = context.originalEditSelect
- Modes.options.edit.onUnselect = context.originalEditUnselect
- }
-)
-
-createBlockbenchMod(
- 'animated_java:modes.paint/variants',
- {
- originalSelect: Modes.options.paint.onSelect,
- originalUnselect: Modes.options.paint.onUnselect,
- },
- context => {
- let selectedVariant: Variant | undefined
- Modes.options.paint.onSelect = function (this: Mode) {
- if (Project && Format === ajModelFormat) {
- requestAnimationFrame(() => {
- selectedVariant = Project.animated_java_variants?.selectedVariant
- Project.animated_java_variants?.select()
- // console.log(Project.animated_java_variants?.selectedVariant?.name)
- })
- }
- return context.originalSelect?.call(this)
- }
- Modes.options.paint.onUnselect = function () {
- if (Project && Format === ajModelFormat && Project.animated_java_variants) {
- Project.animated_java_variants.select(selectedVariant)
- // console.log(Project.animated_java_variants.selectedVariant?.name)
- }
- return context.originalUnselect?.call(this)
- }
- return context
- },
- context => {
- Modes.options.paint.onSelect = context.originalSelect
- Modes.options.paint.onUnselect = context.originalUnselect
- }
-)
diff --git a/src/mods/modelFormatConvertToMod.ts b/src/mods/modelFormatConvertToMod.ts
new file mode 100644
index 00000000..6b27cc5b
--- /dev/null
+++ b/src/mods/modelFormatConvertToMod.ts
@@ -0,0 +1,21 @@
+import { BLUEPRINT_FORMAT, convertToBlueprint } from '../blueprintFormat'
+import { PACKAGE } from '../constants'
+import { createBlockbenchMod } from '../util/moddingTools'
+
+createBlockbenchMod(
+ `${PACKAGE.name}:modelFormatConvertToMod`,
+ {
+ original: ModelFormat.prototype.convertTo,
+ },
+ context => {
+ ModelFormat.prototype.convertTo = function (this: ModelFormat) {
+ const result = context.original.call(this)
+ if (this === BLUEPRINT_FORMAT) convertToBlueprint()
+ return result
+ }
+ return context
+ },
+ context => {
+ ModelFormat.prototype.convertTo = context.original
+ }
+)
diff --git a/src/mods/modelFormatMod.ts b/src/mods/modelFormatMod.ts
index 1eda942e..9dd42ec4 100644
--- a/src/mods/modelFormatMod.ts
+++ b/src/mods/modelFormatMod.ts
@@ -1,24 +1,23 @@
-import { ajModelFormat, convertToAJModelFormat } from '../modelFormat'
+import { BLUEPRINT_FORMAT } from '../blueprintFormat'
+import { PACKAGE } from '../constants'
+import { events } from '../util/events'
import { createBlockbenchMod } from '../util/moddingTools'
-// Cube.all[0].mesh.material = Canvas.wireframeMaterial
createBlockbenchMod(
- 'animated_java:modelFormat',
+ `${PACKAGE.name}:modelFormatPreSelectProjectEvent`,
{
- convertTo: ModelFormat.prototype.convertTo,
+ originalSelect: ModelProject.prototype.select,
},
context => {
- ModelFormat.prototype.convertTo = function (this: ModelFormat) {
- const result = context.convertTo.call(this)
- if (this === ajModelFormat) {
- convertToAJModelFormat()
+ ModelProject.prototype.select = function (this: ModelProject) {
+ if (this.format.id === BLUEPRINT_FORMAT.id) {
+ events.PRE_SELECT_PROJECT.dispatch(this)
}
- return result
+ return context.originalSelect.call(this)
}
-
return context
},
context => {
- ModelFormat.prototype.convertTo = context.convertTo
+ ModelProject.prototype.select = context.originalSelect
}
)
diff --git a/src/mods/molangMod.ts b/src/mods/molangMod.ts
new file mode 100644
index 00000000..81c227f8
--- /dev/null
+++ b/src/mods/molangMod.ts
@@ -0,0 +1,482 @@
+import { PACKAGE } from '../constants'
+import { events } from '../util/events'
+import { createBlockbenchMod } from '../util/moddingTools'
+import MolangFunctionFile from './functions.molang'
+
+const GLOBAL_VARIABLES = Animator.MolangParser.global_variables
+
+const ROOT_TOKENS = [
+ 'true',
+ 'false',
+ 'math.',
+ 'query.', //'q.',
+ 'variable.', //'v.',
+ 'temp.', //'t.',
+ 'context.', //'c.',
+ 'this',
+ 'loop()',
+ 'return',
+ 'break',
+ 'continue',
+]
+const MOLANG_QUERIES = [
+ // common
+ 'all_animations_finished',
+ 'any_animation_finished',
+ 'anim_time',
+ 'life_time',
+ 'yaw_speed',
+ 'ground_speed',
+ 'vertical_speed',
+ 'property',
+ 'has_property()',
+ 'variant',
+ 'mark_variant',
+ 'skin_id',
+
+ 'above_top_solid',
+ 'actor_count',
+ 'all()',
+ 'all_tags',
+ 'anger_level',
+ 'any()',
+ 'any_tag',
+ 'approx_eq()',
+ 'armor_color_slot',
+ 'armor_material_slot',
+ 'armor_texture_slot',
+ 'average_frame_time',
+ 'blocking',
+ 'body_x_rotation',
+ 'body_y_rotation',
+ 'bone_aabb',
+ 'bone_origin',
+ 'bone_rotation',
+ 'camera_distance_range_lerp',
+ 'camera_rotation()',
+ 'can_climb',
+ 'can_damage_nearby_mobs',
+ 'can_dash',
+ 'can_fly',
+ 'can_power_jump',
+ 'can_swim',
+ 'can_walk',
+ 'cape_flap_amount',
+ 'cardinal_facing',
+ 'cardinal_facing_2d',
+ 'cardinal_player_facing',
+ 'combine_entities()',
+ 'count',
+ 'current_squish_value',
+ 'dash_cooldown_progress',
+ 'day',
+ 'death_ticks',
+ 'debug_output',
+ 'delta_time',
+ 'distance_from_camera',
+ 'effect_emitter_count',
+ 'effect_particle_count',
+ 'equipment_count',
+ 'equipped_item_all_tags',
+ 'equipped_item_any_tag()',
+ 'equipped_item_is_attachable',
+ 'eye_target_x_rotation',
+ 'eye_target_y_rotation',
+ 'facing_target_to_range_attack',
+ 'frame_alpha',
+ 'get_actor_info_id',
+ 'get_animation_frame',
+ 'get_default_bone_pivot',
+ 'get_locator_offset',
+ 'get_root_locator_offset',
+ 'had_component_group()',
+ 'has_any_family()',
+ 'has_armor_slot',
+ 'has_biome_tag',
+ 'has_block_property',
+ 'has_cape',
+ 'has_collision',
+ 'has_dash_cooldown',
+ 'has_gravity',
+ 'has_owner',
+ 'has_rider',
+ 'has_target',
+ 'head_roll_angle',
+ 'head_x_rotation',
+ 'head_y_rotation',
+ 'health',
+ 'heartbeat_interval',
+ 'heartbeat_phase',
+ 'heightmap',
+ 'hurt_direction',
+ 'hurt_time',
+ 'in_range()',
+ 'invulnerable_ticks',
+ 'is_admiring',
+ 'is_alive',
+ 'is_angry',
+ 'is_attached_to_entity',
+ 'is_avoiding_block',
+ 'is_avoiding_mobs',
+ 'is_baby',
+ 'is_breathing',
+ 'is_bribed',
+ 'is_carrying_block',
+ 'is_casting',
+ 'is_celebrating',
+ 'is_celebrating_special',
+ 'is_charged',
+ 'is_charging',
+ 'is_chested',
+ 'is_critical',
+ 'is_croaking',
+ 'is_dancing',
+ 'is_delayed_attacking',
+ 'is_digging',
+ 'is_eating',
+ 'is_eating_mob',
+ 'is_elder',
+ 'is_emerging',
+ 'is_emoting',
+ 'is_enchanted',
+ 'is_fire_immune',
+ 'is_first_person',
+ 'is_ghost',
+ 'is_gliding',
+ 'is_grazing',
+ 'is_idling',
+ 'is_ignited',
+ 'is_illager_captain',
+ 'is_in_contact_with_water',
+ 'is_in_love',
+ 'is_in_ui',
+ 'is_in_water',
+ 'is_in_water_or_rain',
+ 'is_interested',
+ 'is_invisible',
+ 'is_item_equipped',
+ 'is_item_name_any()',
+ 'is_jump_goal_jumping',
+ 'is_jumping',
+ 'is_laying_down',
+ 'is_laying_egg',
+ 'is_leashed',
+ 'is_levitating',
+ 'is_lingering',
+ 'is_moving',
+ 'is_name_any()',
+ 'is_on_fire',
+ 'is_on_ground',
+ 'is_on_screen',
+ 'is_onfire',
+ 'is_orphaned',
+ 'is_owner_identifier_any()',
+ 'is_persona_or_premium_skin',
+ 'is_playing_dead',
+ 'is_powered',
+ 'is_pregnant',
+ 'is_ram_attacking',
+ 'is_resting',
+ 'is_riding',
+ 'is_roaring',
+ 'is_rolling',
+ 'is_saddled',
+ 'is_scared',
+ 'is_selected_item',
+ 'is_shaking',
+ 'is_shaking_wetness',
+ 'is_sheared',
+ 'is_shield_powered',
+ 'is_silent',
+ 'is_sitting',
+ 'is_sleeping',
+ 'is_sneaking',
+ 'is_sneezing',
+ 'is_sniffing',
+ 'is_sonic_boom',
+ 'is_spectator',
+ 'is_sprinting',
+ 'is_stackable',
+ 'is_stalking',
+ 'is_standing',
+ 'is_stunned',
+ 'is_swimming',
+ 'is_tamed',
+ 'is_transforming',
+ 'is_using_item',
+ 'is_wall_climbing',
+ 'item_in_use_duration',
+ 'item_is_charged',
+ 'item_max_use_duration',
+ 'item_remaining_use_duration',
+ 'item_slot_to_bone_name()',
+ 'key_frame_lerp_time',
+ 'last_frame_time',
+ 'last_hit_by_player',
+ 'lie_amount',
+ 'life_span',
+ 'lod_index',
+ 'log',
+ 'main_hand_item_max_duration',
+ 'main_hand_item_use_duration',
+ 'max_durability',
+ 'max_health',
+ 'max_trade_tier',
+ 'maximum_frame_time',
+ 'minimum_frame_time',
+ 'model_scale',
+ 'modified_distance_moved',
+ 'modified_move_speed',
+ 'moon_brightness',
+ 'moon_phase',
+ 'movement_direction',
+ 'noise',
+ 'on_fire_time',
+ 'out_of_control',
+ 'player_level',
+ 'position()',
+ 'position_delta()',
+ 'previous_squish_value',
+ 'remaining_durability',
+ 'roll_counter',
+ 'rotation_to_camera()',
+ 'shake_angle',
+ 'shake_time',
+ 'shield_blocking_bob',
+ 'show_bottom',
+ 'sit_amount',
+ 'sleep_rotation',
+ 'sneeze_counter',
+ 'spellcolor',
+ 'standing_scale',
+ 'structural_integrity',
+ 'surface_particle_color',
+ 'surface_particle_texture_coordinate',
+ 'surface_particle_texture_size',
+ 'swell_amount',
+ 'swelling_dir',
+ 'swim_amount',
+ 'tail_angle',
+ 'target_x_rotation',
+ 'target_y_rotation',
+ 'texture_frame_index',
+ 'time_of_day',
+ 'time_since_last_vibration_detection',
+ 'time_stamp',
+ 'total_emitter_count',
+ 'total_particle_count',
+ 'trade_tier',
+ 'unhappy_counter',
+ 'walk_distance',
+ 'wing_flap_position',
+ 'wing_flap_speed',
+]
+const MOLANG_QUERY_LABELS = {
+ 'in_range()': 'in_range( value, min, max )',
+ 'all()': 'in_range( value, values... )',
+ 'any()': 'in_range( value, values... )',
+ 'approx_eq()': 'in_range( value, values... )',
+}
+const DEFAULT_CONTEXT = [
+ 'item_slot',
+ 'block_face',
+ 'cardinal_block_face_placed_on',
+ 'is_first_person',
+ 'owning_entity',
+ 'player_offhand_arm_height',
+ 'other',
+ 'count',
+]
+const DEFAULT_VARIABLES = ['attack_time', 'is_first_person']
+const MATH_FUNCTIONS = [
+ 'sin()',
+ 'cos()',
+ 'abs()',
+ 'clamp()',
+ 'pow()',
+ 'sqrt()',
+ 'random()',
+ 'ceil()',
+ 'round()',
+ 'trunc()',
+ 'floor()',
+ 'mod()',
+ 'min()',
+ 'max()',
+ 'exp()',
+ 'ln()',
+ 'lerp()',
+ 'lerprotate()',
+ 'pi',
+ 'asin()',
+ 'acos()',
+ 'atan()',
+ 'atan2()',
+ 'die_roll()',
+ 'die_roll_integer()',
+ 'hermite_blend()',
+ 'random_integer()',
+]
+const MATH_FUNCTION_LABELS = {
+ 'clamp()': 'clamp( value, min, max )',
+ 'pow()': 'pow( base, exponent )',
+ 'random()': 'random( low, high )',
+ 'mod()': 'mod( value, denominator )',
+ 'min()': 'min( A, B )',
+ 'max()': 'max( A, B )',
+ 'lerp()': 'lerp( start, end, 0_to_1 )',
+ 'lerprotate()': 'lerprotate( start, end, 0_to_1 )',
+ 'atan2()': 'atan2( y, x )',
+ 'die_roll()': 'die_roll( num, low, high )',
+ 'die_roll_integer()': 'die_roll_integer( num, low, high )',
+ 'random_integer()': 'random_integer( low, high )',
+ 'hermite_blend()': 'hermite_blend( 0_to_1 )',
+}
+
+const CUSTOM_FUNCTIONS: Record number> = {}
+const CUSTOM_FUNCTION_LABELS: Record = {}
+
+for (const [call, body] of Object.entries(MolangFunctionFile)) {
+ const match = call.match(/^(.+?)\((.*?)\)$/)
+ if (!match) continue
+ const name = match[1]
+ const argList = match[2].split(',').map(v => v.trim())
+ CUSTOM_FUNCTIONS[name] = (...args: number[]) => {
+ const variables: Record = {}
+ for (let i = 0; i < args.length; i++) {
+ variables[argList[i]] = args[i] || 0
+ }
+ return Animator.MolangParser.parse(body, variables)
+ }
+ CUSTOM_FUNCTION_LABELS[name] = name + '( ' + argList.join(', ') + ' )'
+}
+
+function getProjectVariables(current: string) {
+ const set = new Set()
+ const expressions = getAllMolangExpressions()
+ expressions.forEach((exp: MolangExpression) => {
+ if (!exp.value) return
+ const matches = exp.value.match(/(v|variable)\.\w+/gi)
+ if (!matches) return
+ matches.forEach(match => {
+ const name = match.substring(match.indexOf('.') + 1)
+ if (name !== current) set.add(name)
+ })
+ })
+ return set
+}
+
+function filterAndSortList(
+ list: string[],
+ match: string,
+ blacklist: string[] | false | undefined = false,
+ labels: Record | undefined = undefined
+) {
+ const result = list.filter(f => f.startsWith(match) && f.length != match.length)
+ list.forEach(f => {
+ if (!result.includes(f) && f.includes(match) && f.length != match.length) result.push(f)
+ })
+ if (blacklist) blacklist.forEach(black => result.remove(black))
+ return result.map(text => {
+ return { text, label: labels && labels[text], overlap: match.length }
+ })
+}
+
+createBlockbenchMod(
+ `${PACKAGE.name}:molangMod`,
+ {
+ originalAutocompleteMolang: Animator.autocompleteMolang,
+ unsubscribeSelectAjProject: undefined as (() => void) | undefined,
+ unsuscribeUnselectAjProject: undefined as (() => void) | undefined,
+ },
+ context => {
+ context.unsubscribeSelectAjProject = events.SELECT_AJ_PROJECT.subscribe(() => {
+ Object.assign(GLOBAL_VARIABLES, CUSTOM_FUNCTIONS)
+
+ Animator.autocompleteMolang = function (text, position, type) {
+ let beginning = text
+ .substring(0, position)
+ .split(/[^a-zA-Z_.]\.*/g)
+ .last()
+ if (!beginning) return []
+
+ beginning = beginning.toLowerCase()
+ if (beginning.includes('.')) {
+ const [namespace, dir] = beginning.split('.')
+ if (namespace == 'math') {
+ return filterAndSortList(
+ MATH_FUNCTIONS,
+ dir,
+ undefined,
+ MATH_FUNCTION_LABELS
+ )
+ }
+ if (namespace == 'query' || namespace == 'q') {
+ return filterAndSortList(
+ MOLANG_QUERIES,
+ dir,
+ type !== 'controller' && [
+ 'all_animations_finished',
+ 'any_animation_finished',
+ ],
+ MOLANG_QUERY_LABELS
+ )
+ }
+ if (namespace == 'temp' || namespace == 't') {
+ const temps = text.match(/([^a-z]|^)t(emp)?\.\w+/gi)
+ if (temps) {
+ const temps2 = temps.map(t => t.split('.')[1])
+ const temps3 = temps2.filter(
+ (t, i) => t !== dir && temps2.indexOf(t) === i
+ )
+ return filterAndSortList(temps3, dir)
+ }
+ }
+ if (namespace == 'context' || namespace == 'c') {
+ return filterAndSortList([...DEFAULT_CONTEXT], dir)
+ }
+ if (namespace == 'variable' || namespace == 'v') {
+ const options = [...getProjectVariables(dir)]
+ options.safePush(...DEFAULT_VARIABLES)
+ return filterAndSortList(options, dir)
+ }
+ } else {
+ const root_tokens = ROOT_TOKENS.slice()
+ let labels = {}
+ if (type === 'placeholders') {
+ labels = {
+ 'toggle()': 'toggle( name )',
+ 'slider()': 'slider( name, step?, min?, max? )',
+ 'impulse()': 'impulse( name, duration )',
+ }
+ root_tokens.push(...Object.keys(labels))
+ }
+ return filterAndSortList(
+ [...root_tokens, ...Object.keys(CUSTOM_FUNCTION_LABELS)],
+ beginning,
+ undefined,
+ { ...labels, ...CUSTOM_FUNCTION_LABELS }
+ )
+ }
+ return []
+ }
+ })
+
+ context.unsuscribeUnselectAjProject = events.UNSELECT_AJ_PROJECT.subscribe(() => {
+ for (const key of Object.keys(CUSTOM_FUNCTIONS)) {
+ delete GLOBAL_VARIABLES[key]
+ }
+ Animator.autocompleteMolang = context.originalAutocompleteMolang
+ })
+
+ return context
+ },
+ context => {
+ for (const key of Object.keys(CUSTOM_FUNCTIONS)) {
+ delete GLOBAL_VARIABLES[key]
+ }
+ Animator.autocompleteMolang = context.originalAutocompleteMolang
+ context.unsubscribeSelectAjProject?.()
+ context.unsuscribeUnselectAjProject?.()
+ }
+)
diff --git a/src/mods/outlinerNodeMod.ts b/src/mods/outlinerNodeMod.ts
deleted file mode 100644
index 878b5505..00000000
--- a/src/mods/outlinerNodeMod.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { safeFunctionName } from '../minecraft'
-import { ajModelFormat } from '../modelFormat'
-import { createBlockbenchMod } from '../util/moddingTools'
-
-createBlockbenchMod(
- 'animated_java:outlinerNode/force_valid_function_name',
- {
- createUniqueName: OutlinerNode.prototype.createUniqueName,
- },
- context => {
- OutlinerNode.prototype.createUniqueName = function (
- this: OutlinerNode,
- others?: OutlinerNode[]
- ) {
- if (Format === ajModelFormat) {
- this.name = safeFunctionName(this.name)
- }
- return context.createUniqueName.call(this, others)
- }
- return context
- },
- context => {
- OutlinerNode.prototype.createUniqueName = context.createUniqueName
- }
-)
diff --git a/src/mods/panelMod.ts b/src/mods/panelMod.ts
new file mode 100644
index 00000000..eb008868
--- /dev/null
+++ b/src/mods/panelMod.ts
@@ -0,0 +1,35 @@
+import { BLUEPRINT_FORMAT } from '../blueprintFormat'
+import { PACKAGE } from '../constants'
+import { createBlockbenchMod } from '../util/moddingTools'
+
+createBlockbenchMod(
+ `${PACKAGE.name}:panelMod`,
+ {
+ panel: Interface.Panels.animations,
+ },
+ context => {
+ const originalFilesFunction = context.panel.inside_vue.$options.computed.files
+
+ context.panel.inside_vue.$options.computed.files = function (this: Vue) {
+ if (Format.id === BLUEPRINT_FORMAT.id) {
+ return {
+ '': {
+ animations: [
+ ...Blockbench.Animation.all,
+ ...Blockbench.AnimationController.all,
+ ],
+ name: '',
+ hide_head: true,
+ },
+ }
+ }
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+ return originalFilesFunction.call(this)
+ }
+
+ return { ...context, originalFilesFunction }
+ },
+ context => {
+ context.panel.inside_vue.$options.computed.files = context.originalFilesFunction
+ }
+)
diff --git a/src/mods/previewRaycastMod.ts b/src/mods/previewRaycastMod.ts
new file mode 100644
index 00000000..8080a348
--- /dev/null
+++ b/src/mods/previewRaycastMod.ts
@@ -0,0 +1,153 @@
+import { PACKAGE } from '../constants'
+import { TextDisplay } from '../outliner/textDisplay'
+import { VanillaBlockDisplay } from '../outliner/vanillaBlockDisplay'
+import { VanillaItemDisplay } from '../outliner/vanillaItemDisplay'
+import { createBlockbenchMod } from '../util/moddingTools'
+
+const ITEM_IMAGE_CACHE = new Map()
+
+function getImageData(image: HTMLImageElement) {
+ if (ITEM_IMAGE_CACHE.has(image)) {
+ return ITEM_IMAGE_CACHE.get(image)!
+ }
+ const canvas = document.createElement('canvas')
+ const context = canvas.getContext(`2d`)!
+ const { naturalWidth: w, naturalHeight: h } = image
+ canvas.width = w
+ canvas.height = h
+ context.drawImage(image, 0, 0, w, h)
+ const imageData = context.getImageData(0, 0, w, h)
+ ITEM_IMAGE_CACHE.set(image, imageData)
+ return imageData
+}
+
+createBlockbenchMod(
+ `${PACKAGE.name}:previewRaycast`,
+ {
+ originalRaycast: Preview.prototype.raycast,
+ },
+ context => {
+ Preview.prototype.raycast = function (this: Preview, event: MouseEvent) {
+ const isClick = event.type === 'mousedown'
+ const isHover = event.type === 'mousemove'
+ const raycast = context.originalRaycast.bind(this)
+ if (!isClick && !isHover) return raycast(event)
+ if (Transformer.dragging) return raycast(event)
+
+ convertTouchEvent(event)
+ const canvasOffset = $(this.canvas).offset()!
+ this.mouse.x = ((event.clientX - canvasOffset.left) / this.width) * 2 - 1
+ this.mouse.y = -((event.clientY - canvasOffset.top) / this.height) * 2 + 1
+ this.raycaster.setFromCamera(this.mouse, this.camera)
+
+ const animatedJavaModels = new Map<
+ THREE.Object3D,
+ VanillaItemDisplay | TextDisplay | VanillaBlockDisplay
+ >()
+ const objects: THREE.Object3D[] = []
+ for (const element of OutlinerElement.all) {
+ // @ts-expect-error
+ if (element.visibility === false) continue
+ if (element instanceof TextDisplay) {
+ const mesh = element.mesh.children.find(el => el.isTextDisplayText)
+ if (mesh) {
+ // Add the background mesh to the raycast to avoid raycasting through the complex text mesh
+ objects.push(mesh.children[0])
+ animatedJavaModels.set(mesh, element)
+ }
+ } else if (element instanceof VanillaItemDisplay) {
+ if (!element.mesh) continue // Weird edge case.
+ const mesh = element.mesh.children.at(0)
+ if (!mesh) continue
+ animatedJavaModels.set(mesh, element)
+ for (const child of mesh.children) {
+ objects.push(child)
+ }
+ } else if (element instanceof VanillaBlockDisplay) {
+ if (!element.mesh) continue // Weird edge case.
+ const mesh = element.mesh.children.at(0)
+ if (!mesh) continue
+ animatedJavaModels.set(mesh, element)
+ for (const child of mesh.children) {
+ objects.push(child)
+ }
+ } else if (
+ element.mesh instanceof THREE.Mesh &&
+ element.mesh.geometry &&
+ // @ts-expect-error
+ element.visibility &&
+ !element.locked
+ ) {
+ objects.push(element.mesh)
+ } else if (element instanceof Locator) {
+ // @ts-expect-error
+ objects.push(element.mesh.sprite as THREE.Sprite)
+ }
+ }
+
+ const intersects = this.raycaster.intersectObjects(objects, false)
+ // const i = intersects.at(0) as THREE.Intersection
+ for (const i of intersects) {
+ if (
+ i &&
+ i.uv &&
+ i.object instanceof THREE.Mesh &&
+ i.object.parent?.isVanillaItemModel
+ ) {
+ const image = i.object.material.map!.image as HTMLImageElement
+ const { width, height } = image
+ const imageData = getImageData(image)
+ const x = Math.ceil(i.uv.x * width) - 1
+ const y = height - Math.ceil(i.uv.y * height)
+ const I = (x + y * width) * 4
+ if (imageData.data[I + 3] <= 140) continue
+
+ const element = animatedJavaModels.get(i.object.parent)
+ if (element) {
+ if (isHover) {
+ element.preview_controller.updateHighlight(element, true)
+ return { element }
+ } else {
+ element.select()
+ // @ts-expect-error
+ this.selection.click_target = element
+ }
+ return false
+ }
+ }
+ if (i && i.object.parent?.isTextDisplayText) {
+ const element = animatedJavaModels.get(i.object.parent)
+ if (element) {
+ if (isClick) {
+ element.select()
+ // @ts-expect-error
+ this.selection.click_target = element
+ }
+ return false
+ }
+ } else if (i && i.object.parent?.isVanillaBlockModel) {
+ const element = animatedJavaModels.get(i.object.parent)
+ if (element) {
+ if (isHover) {
+ element.preview_controller.updateHighlight(element, true)
+ return { element }
+ } else {
+ element.select()
+ // @ts-expect-error
+ this.selection.click_target = element
+ }
+ return false
+ }
+ } else if (i && i.object.isElement) {
+ return raycast(event)
+ }
+ }
+ return raycast(event)
+ }
+
+ return context
+ },
+ context => {
+ Preview.prototype.raycast = context.originalRaycast
+ }
+)
diff --git a/src/mods/projectSettingsActionOverride.ts b/src/mods/projectSettingsActionOverride.ts
new file mode 100644
index 00000000..2e40f856
--- /dev/null
+++ b/src/mods/projectSettingsActionOverride.ts
@@ -0,0 +1,25 @@
+import PACKAGE from '../../package.json'
+import { BLUEPRINT_FORMAT } from '../blueprintFormat'
+import { openBlueprintSettingsDialog } from '../interface/blueprintSettingsDialog'
+import { createBlockbenchMod } from '../util/moddingTools'
+
+createBlockbenchMod(
+ `${PACKAGE.name}:projectSettingsActionOverride`,
+ {
+ action: BarItems.project_window as Action,
+ oldClick: (BarItems.project_window as Action).click,
+ },
+ context => {
+ context.action.click = function (this, event: Event) {
+ if (Format.id === BLUEPRINT_FORMAT.id) {
+ openBlueprintSettingsDialog()
+ } else {
+ context.oldClick.call(this, event)
+ }
+ }
+ return context
+ },
+ context => {
+ context.action.click = context.oldClick
+ }
+)
diff --git a/src/mods/saveAllAnimationsActionMod.ts b/src/mods/saveAllAnimationsActionMod.ts
new file mode 100644
index 00000000..eb0b2c44
--- /dev/null
+++ b/src/mods/saveAllAnimationsActionMod.ts
@@ -0,0 +1,23 @@
+import { BLUEPRINT_FORMAT } from '../blueprintFormat'
+import { PACKAGE } from '../constants'
+import { createBlockbenchMod } from '../util/moddingTools'
+
+createBlockbenchMod(
+ `${PACKAGE.name}:saveAllAnimationsActionMod`,
+ {
+ action: BarItems.save_all_animations as Action,
+ },
+ context => {
+ const originalCondition = context.action.condition!
+ context.action.condition = function (this: Action) {
+ if (Format.id === BLUEPRINT_FORMAT.id) {
+ return false
+ }
+ return originalCondition.call(this)
+ }
+ return { ...context, originalCondition }
+ },
+ context => {
+ context.action.condition = context.originalCondition
+ }
+)
diff --git a/src/mods/saveProjectActionMod.ts b/src/mods/saveProjectActionMod.ts
new file mode 100644
index 00000000..016ae13a
--- /dev/null
+++ b/src/mods/saveProjectActionMod.ts
@@ -0,0 +1,25 @@
+import { BLUEPRINT_FORMAT, saveBlueprint } from '../blueprintFormat'
+import { PACKAGE } from '../constants'
+import { createBlockbenchMod } from '../util/moddingTools'
+
+createBlockbenchMod(
+ `${PACKAGE.name}:save_project`,
+ {
+ action: BarItems.save_project as Action,
+ originalClick: (BarItems.save_project as Action).click,
+ },
+ context => {
+ context.action.click = (event: Event) => {
+ if (!Project || !Format) return
+ if (Format === BLUEPRINT_FORMAT) {
+ saveBlueprint()
+ } else {
+ context.originalClick.call(context.action, event)
+ }
+ }
+ return context
+ },
+ context => {
+ context.action.click = context.originalClick
+ }
+)
diff --git a/src/mods/saveProjectAsActionMod.ts b/src/mods/saveProjectAsActionMod.ts
new file mode 100644
index 00000000..67c24faa
--- /dev/null
+++ b/src/mods/saveProjectAsActionMod.ts
@@ -0,0 +1,25 @@
+import { BLUEPRINT_CODEC, BLUEPRINT_FORMAT } from '../blueprintFormat'
+import { PACKAGE } from '../constants'
+import { createBlockbenchMod } from '../util/moddingTools'
+
+createBlockbenchMod(
+ `${PACKAGE.name}:save_project_as`,
+ {
+ action: BarItems.save_project_as as Action,
+ originalClick: (BarItems.save_project_as as Action).click,
+ },
+ context => {
+ context.action.click = (event: Event) => {
+ if (!Project || !Format) return
+ if (Format === BLUEPRINT_FORMAT) {
+ BLUEPRINT_CODEC.export()
+ } else {
+ context.originalClick.call(context.action, event)
+ }
+ }
+ return context
+ },
+ context => {
+ context.action.click = context.originalClick
+ }
+)
diff --git a/src/mods/showDefaultPoseMod.ts b/src/mods/showDefaultPoseMod.ts
new file mode 100644
index 00000000..536061f4
--- /dev/null
+++ b/src/mods/showDefaultPoseMod.ts
@@ -0,0 +1,39 @@
+import { isCurrentFormat } from '../blueprintFormat'
+import { PACKAGE } from '../constants'
+import { createBlockbenchMod } from '../util/moddingTools'
+
+createBlockbenchMod(
+ `${PACKAGE.name}:showDefaultPose`,
+ {
+ original: Animator.showDefaultPose,
+ },
+ context => {
+ Animator.showDefaultPose = function (noMatrixUpdate?: boolean) {
+ if (!isCurrentFormat()) return context.original(noMatrixUpdate)
+
+ const nodes = [...Group.all, ...Outliner.elements]
+ for (const node of nodes) {
+ // @ts-expect-error
+ if (!node.constructor.animator) continue
+ const mesh = node.mesh
+ if (mesh.fix_rotation) mesh.rotation.copy(mesh.fix_rotation as THREE.Euler)
+ if (mesh.fix_position) mesh.position.copy(mesh.fix_position as THREE.Vector3)
+ if (mesh.fix_scale) mesh.scale.copy(mesh.fix_scale)
+ else if (
+ // @ts-expect-error
+ node.constructor.animator.prototype.channels &&
+ // @ts-expect-error
+ node.constructor.animator.prototype.channels.scale
+ ) {
+ mesh.scale.x = mesh.scale.y = mesh.scale.z = 1
+ }
+ }
+ if (!noMatrixUpdate) scene.updateMatrixWorld()
+ }
+
+ return context
+ },
+ context => {
+ Animator.showDefaultPose = context.original
+ }
+)
diff --git a/src/mods/textureMod.ts b/src/mods/textureMod.ts
deleted file mode 100644
index 032b89be..00000000
--- a/src/mods/textureMod.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { ajModelFormat } from '../modelFormat'
-import { createBlockbenchMod } from '../util/moddingTools'
-
-createBlockbenchMod(
- 'animated_java:texture',
- {
- remove: Texture.prototype.remove,
- },
- context => {
- Texture.prototype.remove = function (this: Texture) {
- const x = context.remove.call(this)
- // Remove all texture mappings that use this texture
- if (Format === ajModelFormat) {
- Project!.animated_java_variants!.verifyTextures(true)
- }
- return x
- }
- // I'm lazy 🤪
- // const interval = setInterval(() => {
- // if (Format === ajModelFormat) {
- // const maxTextureWidth = Texture.all.reduce(
- // (max, texture) => Math.max(max, texture.width),
- // 0
- // )
- // Project!.texture_height = maxTextureWidth
- // Project!.texture_width = maxTextureWidth
- // }
- // }, 1000)
- return {
- // interval,
- ...context,
- }
- },
- context => {
- // clearInterval(context.interval)
- Texture.prototype.remove = context.remove
- }
-)
diff --git a/src/mods/variantPreviewCubeFaceMod.ts b/src/mods/variantPreviewCubeFaceMod.ts
new file mode 100644
index 00000000..fa151e31
--- /dev/null
+++ b/src/mods/variantPreviewCubeFaceMod.ts
@@ -0,0 +1,41 @@
+import { isCurrentFormat } from '../blueprintFormat'
+import { PACKAGE } from '../constants'
+import { createBlockbenchMod } from '../util/moddingTools'
+import { Variant } from '../variants'
+
+createBlockbenchMod(
+ `${PACKAGE.name}:variantPreviewCubeFace`,
+ {
+ originalGetTexture: CubeFace.prototype.getTexture,
+ },
+ context => {
+ CubeFace.prototype.getTexture = function (this: CubeFace): Texture | undefined {
+ if (isCurrentFormat() && this.texture) {
+ const variant = Variant.selected
+ if (
+ variant &&
+ this.cube.parent instanceof Group &&
+ !variant.excludedBones.find(
+ v => v.value === (this.cube.parent as Group).uuid
+ ) &&
+ variant.textureMap.has(this.texture)
+ ) {
+ this.lastVariant = variant
+ return variant.textureMap.getMappedTexture(this.texture)
+ } else if (
+ Mode.selected.id === Modes.options.animate.id &&
+ this.lastVariant &&
+ !variant?.isDefault
+ ) {
+ return this.lastVariant.textureMap.getMappedTexture(this.texture)
+ }
+ }
+ this.lastVariant = undefined
+ return context.originalGetTexture.call(this)
+ }
+ return context
+ },
+ context => {
+ CubeFace.prototype.getTexture = context.originalGetTexture
+ }
+)
diff --git a/src/nodeConfigs.ts b/src/nodeConfigs.ts
new file mode 100644
index 00000000..78a49d93
--- /dev/null
+++ b/src/nodeConfigs.ts
@@ -0,0 +1,713 @@
+import { NbtByte, NbtCompound, NbtFloat, NbtInt, NbtString, NbtTag } from 'deepslate'
+import {
+ IBlueprintBoneConfigJSON,
+ IBlueprintCameraConfigJSON,
+ IBlueprintLocatorConfigJSON,
+ IBlueprintTextDisplayConfigJSON,
+} from './blueprintFormat'
+
+export type BillboardMode = 'fixed' | 'vertical' | 'horizontal' | 'center'
+
+// TODO: Refactor these configs to inherit from a base class
+export class BoneConfig {
+ private _vanillaModel?: string
+ private _billboard?: BillboardMode
+ private _overrideBrightness?: boolean
+ private _brightnessOverride?: number
+ private _enchanted?: boolean
+ private _glowing?: boolean
+ private _overrideGlowColor?: boolean
+ private _glowColor?: string
+ private _inheritSettings?: boolean
+ private _invisible?: boolean
+ private _nbt?: string
+ private _shadowRadius?: number
+ private _shadowStrength?: number
+ private _useNBT?: boolean
+
+ static getDefault(): BoneConfig {
+ return BoneConfig.fromJSON({
+ billboard: 'fixed',
+ override_brightness: false,
+ brightness_override: 0,
+ enchanted: false,
+ glowing: false,
+ override_glow_color: false,
+ glow_color: '#ffffff',
+ inherit_settings: true,
+ invisible: false,
+ nbt: '{}',
+ shadow_radius: 0,
+ shadow_strength: 1,
+ use_nbt: false,
+ })
+ }
+
+ get billboard(): NonNullable {
+ if (this._billboard !== undefined) return this._billboard
+ const defaultConfig = BoneConfig.getDefault()
+ return defaultConfig.billboard
+ }
+ set billboard(value: BoneConfig['_billboard']) {
+ this._billboard = value
+ }
+
+ get overrideBrightness(): NonNullable {
+ if (this._overrideBrightness !== undefined) return this._overrideBrightness
+ const defaultConfig = BoneConfig.getDefault()
+ return defaultConfig.overrideBrightness
+ }
+ set overrideBrightness(value: BoneConfig['_overrideBrightness']) {
+ this._overrideBrightness = value
+ }
+
+ get brightnessOverride(): NonNullable {
+ if (this._brightnessOverride !== undefined) return this._brightnessOverride
+ const defaultConfig = BoneConfig.getDefault()
+ return defaultConfig.brightnessOverride
+ }
+ set brightnessOverride(value: BoneConfig['_brightnessOverride']) {
+ this._brightnessOverride = value
+ }
+
+ get enchanted(): NonNullable {
+ if (this._enchanted !== undefined) return this._enchanted
+ const defaultConfig = BoneConfig.getDefault()
+ return defaultConfig.enchanted
+ }
+ set enchanted(value: BoneConfig['_enchanted']) {
+ this._enchanted = value
+ }
+
+ get glowing(): NonNullable {
+ if (this._glowing !== undefined) return this._glowing
+ const defaultConfig = BoneConfig.getDefault()
+ return defaultConfig.glowing
+ }
+ set glowing(value: BoneConfig['_glowing']) {
+ this._glowing = value
+ }
+
+ get overrideGlowColor(): NonNullable {
+ if (this._overrideGlowColor !== undefined) return this._overrideGlowColor
+ const defaultConfig = BoneConfig.getDefault()
+ return defaultConfig.overrideGlowColor
+ }
+ set overrideGlowColor(value: BoneConfig['_overrideGlowColor']) {
+ this._overrideGlowColor = value
+ }
+
+ get glowColor(): NonNullable {
+ if (this._glowColor !== undefined) return this._glowColor
+ const defaultConfig = BoneConfig.getDefault()
+ return defaultConfig.glowColor
+ }
+ set glowColor(value: BoneConfig['_glowColor']) {
+ this._glowColor = value
+ }
+
+ get inheritSettings(): NonNullable {
+ if (this._inheritSettings !== undefined) return this._inheritSettings
+ const defaultConfig = BoneConfig.getDefault()
+ return defaultConfig.inheritSettings
+ }
+ set inheritSettings(value: BoneConfig['_inheritSettings']) {
+ this._inheritSettings = value
+ }
+
+ get invisible(): NonNullable {
+ if (this._invisible !== undefined) return this._invisible
+ const defaultConfig = BoneConfig.getDefault()
+ return defaultConfig.invisible
+ }
+ set invisible(value: BoneConfig['_invisible']) {
+ this._invisible = value
+ }
+
+ get nbt(): NonNullable {
+ if (this._nbt !== undefined) return this._nbt
+ const defaultConfig = BoneConfig.getDefault()
+ return defaultConfig.nbt
+ }
+ set nbt(value: BoneConfig['_nbt']) {
+ this._nbt = value
+ }
+
+ get shadowRadius(): NonNullable {
+ if (this._shadowRadius !== undefined) return this._shadowRadius
+ const defaultConfig = BoneConfig.getDefault()
+ return defaultConfig.shadowRadius
+ }
+ set shadowRadius(value: BoneConfig['_shadowRadius']) {
+ this._shadowRadius = value
+ }
+
+ get shadowStrength(): NonNullable {
+ if (this._shadowStrength !== undefined) return this._shadowStrength
+ const defaultConfig = BoneConfig.getDefault()
+ return defaultConfig.shadowStrength
+ }
+ set shadowStrength(value: BoneConfig['_shadowStrength']) {
+ this._shadowStrength = value
+ }
+
+ get useNBT(): NonNullable {
+ if (this._useNBT !== undefined) return this._useNBT
+ const defaultConfig = BoneConfig.getDefault()
+ return defaultConfig.useNBT
+ }
+ set useNBT(value: BoneConfig['_useNBT']) {
+ this._useNBT = value
+ }
+
+ public checkIfEqual(other: BoneConfig) {
+ return (
+ this._vanillaModel === other._vanillaModel &&
+ this._billboard === other._billboard &&
+ this._overrideBrightness === other._overrideBrightness &&
+ this._brightnessOverride === other._brightnessOverride &&
+ this._enchanted === other._enchanted &&
+ this._glowing === other._glowing &&
+ this._overrideGlowColor === other._overrideGlowColor &&
+ this._glowColor === other._glowColor &&
+ this._inheritSettings === other._inheritSettings &&
+ this._invisible === other._invisible &&
+ this._nbt === other._nbt &&
+ this._shadowRadius === other._shadowRadius &&
+ this._shadowStrength === other._shadowStrength &&
+ this._useNBT === other._useNBT
+ )
+ }
+
+ public isDefault(): boolean {
+ return this.checkIfEqual(BoneConfig.getDefault())
+ }
+
+ public toJSON(): IBlueprintBoneConfigJSON {
+ return {
+ billboard: this._billboard,
+ override_brightness: this._overrideBrightness,
+ brightness_override: this._brightnessOverride,
+ enchanted: this._enchanted,
+ glowing: this._glowing,
+ override_glow_color: this._overrideGlowColor,
+ glow_color: this._glowColor,
+ inherit_settings: this._inheritSettings,
+ invisible: this._invisible,
+ nbt: this._nbt,
+ shadow_radius: this._shadowRadius,
+ shadow_strength: this._shadowStrength,
+ use_nbt: this._useNBT,
+ }
+ }
+
+ inheritFrom(other: BoneConfig) {
+ if (other._billboard !== undefined) this.billboard = other.billboard
+ if (other._overrideBrightness !== undefined)
+ this.overrideBrightness = other.overrideBrightness
+ if (other._brightnessOverride !== undefined)
+ this.brightnessOverride = other.brightnessOverride
+ if (other._enchanted !== undefined) this.enchanted = other.enchanted
+ if (other._glowing !== undefined) this.glowing = other.glowing
+ if (other._overrideGlowColor !== undefined) this.overrideGlowColor = other.overrideGlowColor
+ if (other._glowColor !== undefined) this.glowColor = other.glowColor
+ if (other._inheritSettings !== undefined) this.inheritSettings = other.inheritSettings
+ if (other._invisible !== undefined) this.invisible = other.invisible
+ if (other._nbt !== undefined) this.nbt = other.nbt
+ if (other._shadowRadius !== undefined) this.shadowRadius = other.shadowRadius
+ if (other._shadowStrength !== undefined) this.shadowStrength = other.shadowStrength
+ if (other._useNBT !== undefined) this.useNBT = other.useNBT
+ }
+
+ public static fromJSON(json: IBlueprintBoneConfigJSON): BoneConfig {
+ const config = new BoneConfig()
+ if (json.billboard !== undefined) config._billboard = json.billboard
+ if (json.override_brightness !== undefined)
+ config._overrideBrightness = json.override_brightness
+ if (json.brightness_override !== undefined)
+ config._brightnessOverride = json.brightness_override
+ if (json.enchanted !== undefined) config._enchanted = json.enchanted
+ if (json.glowing !== undefined) config._glowing = json.glowing
+ if (json.override_glow_color !== undefined)
+ config._overrideGlowColor = json.override_glow_color
+ if (json.glow_color !== undefined) config._glowColor = json.glow_color
+ if (json.inherit_settings !== undefined) config._inheritSettings = json.inherit_settings
+ if (json.invisible !== undefined) config._invisible = json.invisible
+ if (json.nbt !== undefined) config._nbt = json.nbt
+ if (json.shadow_radius !== undefined) config._shadowRadius = json.shadow_radius
+ if (json.shadow_strength !== undefined) config._shadowStrength = json.shadow_strength
+ if (json.use_nbt !== undefined) config._useNBT = json.use_nbt
+ return config
+ }
+
+ public toNBT(compound: NbtCompound = new NbtCompound()): NbtCompound {
+ // if (this.vanillaItemModel) {
+ // const item = (compound.get('item') as NbtCompound) || new NbtCompound()
+ // compound.set('item', item.set('id', new NbtString(this.vanillaItemModel)))
+ // compound.set('item_display', new NbtString('none'))
+ // }
+
+ if (this.useNBT) {
+ const newData = NbtTag.fromString(this.nbt) as NbtCompound
+ for (const key of newData.keys()) {
+ compound.set(key, newData.get(key)!)
+ }
+ return compound
+ }
+
+ if (this._billboard) {
+ compound.set('billboard', new NbtString(this.billboard))
+ }
+
+ if (this.overrideBrightness) {
+ compound.set(
+ 'brightness',
+ new NbtCompound()
+ .set('block', new NbtFloat(this.brightnessOverride))
+ .set('sky', new NbtFloat(this.brightnessOverride))
+ )
+ }
+
+ if (this.enchanted) {
+ const item = (compound.get('item') as NbtCompound) || new NbtCompound()
+ compound.set(
+ 'item',
+ item.set(
+ 'components',
+ new NbtCompound().set(
+ 'minecraft:enchantments',
+ new NbtCompound().set(
+ 'levels',
+ new NbtCompound().set('minecraft:infinity', new NbtInt(1))
+ )
+ )
+ )
+ )
+ }
+
+ if (this.glowing) {
+ compound.set('Glowing', new NbtByte(Number(this.glowing)))
+ }
+ if (this.overrideGlowColor) {
+ compound.set(
+ 'glow_color_override',
+ new NbtInt(Number(this.glowColor.replace('#', '0x')))
+ )
+ }
+
+ // TODO Figure out a good solution for toggling a bone's visibility...
+ // if (force || config.invisible !== defaultConfig.invisible) {
+ // compound.set('invisible', new NbtByte(1))
+ // }
+
+ if (this._shadowRadius) {
+ compound.set('shadow_radius', new NbtFloat(this.shadowRadius))
+ }
+
+ if (this._shadowStrength) {
+ compound.set('shadow_strength', new NbtFloat(this.shadowStrength))
+ }
+
+ return compound
+ }
+}
+
+export class LocatorConfig {
+ private _useEntity?: boolean
+ private _entityType?: string
+ private _summonCommands?: string
+ private _tickingCommands?: string
+
+ getDefault(): LocatorConfig {
+ return LocatorConfig.fromJSON({
+ use_entity: false,
+ entity_type: 'minecraft:pig',
+ summon_commands: '',
+ ticking_commands: '',
+ })
+ }
+
+ get useEntity(): NonNullable {
+ if (this._useEntity !== undefined) return this._useEntity
+ const defaultConfig = this.getDefault()
+ return defaultConfig.useEntity
+ }
+ set useEntity(value: NonNullable) {
+ this._useEntity = value
+ }
+
+ get entityType(): NonNullable {
+ if (this._entityType !== undefined) return this._entityType
+ const defaultConfig = this.getDefault()
+ return defaultConfig.entityType
+ }
+ set entityType(value: NonNullable) {
+ this._entityType = value
+ }
+
+ get summonCommands(): NonNullable {
+ if (this._summonCommands !== undefined) return this._summonCommands
+ const defaultConfig = this.getDefault()
+ return defaultConfig.summonCommands
+ }
+ set summonCommands(value: NonNullable) {
+ this._summonCommands = value
+ }
+
+ get tickingCommands(): NonNullable {
+ if (this._tickingCommands !== undefined) return this._tickingCommands
+ const defaultConfig = this.getDefault()
+ return defaultConfig.tickingCommands
+ }
+ set tickingCommands(value: NonNullable) {
+ this._tickingCommands = value
+ }
+
+ public toJSON(): IBlueprintLocatorConfigJSON {
+ return {
+ use_entity: this._useEntity,
+ entity_type: this._entityType,
+ summon_commands: this._summonCommands,
+ ticking_commands: this._tickingCommands,
+ }
+ }
+
+ public static fromJSON(json: IBlueprintLocatorConfigJSON): LocatorConfig {
+ const config = new LocatorConfig()
+ if (json.use_entity !== undefined) config._useEntity = json.use_entity
+ if (json.entity_type !== undefined) config._entityType = json.entity_type
+ if (json.summon_commands !== undefined) config._summonCommands = json.summon_commands
+ if (json.ticking_commands !== undefined) config._tickingCommands = json.ticking_commands
+ return config
+ }
+
+ public isDefault(): boolean {
+ return this.checkIfEqual(new LocatorConfig())
+ }
+
+ public checkIfEqual(other: LocatorConfig) {
+ return (
+ this.useEntity === other.useEntity &&
+ this.entityType === other.entityType &&
+ this.summonCommands === other.summonCommands &&
+ this.tickingCommands === other.tickingCommands
+ )
+ }
+}
+
+export class CameraConfig {
+ private _entityType?: string
+ private _nbt?: string
+ private _tickingCommands?: string
+
+ getDefault(): CameraConfig {
+ return CameraConfig.fromJSON({
+ entity_type: 'minecraft:item_display',
+ nbt: '{}',
+ ticking_commands: '',
+ })
+ }
+
+ get entityType(): NonNullable {
+ if (this._entityType !== undefined) return this._entityType
+ const defaultConfig = this.getDefault()
+ return defaultConfig.entityType
+ }
+ set entityType(value: NonNullable) {
+ this._entityType = value
+ }
+
+ get nbt(): NonNullable {
+ if (this._nbt !== undefined) return this._nbt
+ const defaultConfig = this.getDefault()
+ return defaultConfig.nbt
+ }
+ set nbt(value: NonNullable) {
+ this._nbt = value
+ }
+
+ get tickingCommands(): NonNullable {
+ if (this._tickingCommands !== undefined) return this._tickingCommands
+ const defaultConfig = this.getDefault()
+ return defaultConfig.tickingCommands
+ }
+ set tickingCommands(value: NonNullable) {
+ this._tickingCommands = value
+ }
+
+ public toJSON(): IBlueprintCameraConfigJSON {
+ return {
+ entity_type: this.entityType,
+ nbt: this.nbt,
+ ticking_commands: this.tickingCommands,
+ }
+ }
+
+ public static fromJSON(json: IBlueprintCameraConfigJSON): CameraConfig {
+ const config = new CameraConfig()
+ if (json.entity_type != undefined) config.entityType = json.entity_type
+ if (json.nbt != undefined) config.nbt = json.nbt
+ if (json.ticking_commands != undefined) config.tickingCommands = json.ticking_commands
+ return config
+ }
+
+ public isDefault(): boolean {
+ return this.checkIfEqual(new CameraConfig())
+ }
+
+ public checkIfEqual(other: CameraConfig) {
+ return (
+ this.entityType === other.entityType &&
+ this.nbt === other.nbt &&
+ this.tickingCommands === other.tickingCommands
+ )
+ }
+}
+
+export class TextDisplayConfig {
+ private _billboard?: BillboardMode
+ private _overrideBrightness?: boolean
+ private _brightnessOverride?: number
+ private _glowing?: boolean
+ private _overrideGlowColor?: boolean
+ private _glowColor?: string
+ private _invisible?: boolean
+ private _shadowRadius?: number
+ private _shadowStrength?: number
+ private _useNBT?: boolean
+ private _nbt?: string
+
+ static getDefault(): TextDisplayConfig {
+ return TextDisplayConfig.fromJSON({
+ billboard: 'fixed',
+ override_brightness: false,
+ brightness_override: 0,
+ glowing: false,
+ override_glow_color: false,
+ glow_color: '#ffffff',
+ invisible: false,
+ nbt: '{}',
+ shadow_radius: 0,
+ shadow_strength: 1,
+ use_nbt: false,
+ })
+ }
+
+ get billboard(): NonNullable {
+ if (this._billboard !== undefined) return this._billboard
+ const defaultConfig = BoneConfig.getDefault()
+ return defaultConfig.billboard
+ }
+ set billboard(value: BoneConfig['_billboard']) {
+ this._billboard = value
+ }
+
+ get overrideBrightness(): NonNullable {
+ if (this._overrideBrightness !== undefined) return this._overrideBrightness
+ const defaultConfig = BoneConfig.getDefault()
+ return defaultConfig.overrideBrightness
+ }
+ set overrideBrightness(value: BoneConfig['_overrideBrightness']) {
+ this._overrideBrightness = value
+ }
+
+ get brightnessOverride(): NonNullable {
+ if (this._brightnessOverride !== undefined) return this._brightnessOverride
+ const defaultConfig = BoneConfig.getDefault()
+ return defaultConfig.brightnessOverride
+ }
+ set brightnessOverride(value: BoneConfig['_brightnessOverride']) {
+ this._brightnessOverride = value
+ }
+
+ get glowing(): NonNullable {
+ if (this._glowing !== undefined) return this._glowing
+ const defaultConfig = BoneConfig.getDefault()
+ return defaultConfig.glowing
+ }
+ set glowing(value: BoneConfig['_glowing']) {
+ this._glowing = value
+ }
+
+ get overrideGlowColor(): NonNullable {
+ if (this._overrideGlowColor !== undefined) return this._overrideGlowColor
+ const defaultConfig = BoneConfig.getDefault()
+ return defaultConfig.overrideGlowColor
+ }
+ set overrideGlowColor(value: BoneConfig['_overrideGlowColor']) {
+ this._overrideGlowColor = value
+ }
+
+ get glowColor(): NonNullable {
+ if (this._glowColor !== undefined) return this._glowColor
+ const defaultConfig = BoneConfig.getDefault()
+ return defaultConfig.glowColor
+ }
+ set glowColor(value: BoneConfig['_glowColor']) {
+ this._glowColor = value
+ }
+
+ get invisible(): NonNullable {
+ if (this._invisible !== undefined) return this._invisible
+ const defaultConfig = BoneConfig.getDefault()
+ return defaultConfig.invisible
+ }
+ set invisible(value: BoneConfig['_invisible']) {
+ this._invisible = value
+ }
+
+ get nbt(): NonNullable {
+ if (this._nbt !== undefined) return this._nbt
+ const defaultConfig = BoneConfig.getDefault()
+ return defaultConfig.nbt
+ }
+ set nbt(value: BoneConfig['_nbt']) {
+ this._nbt = value
+ }
+
+ get shadowRadius(): NonNullable {
+ if (this._shadowRadius !== undefined) return this._shadowRadius
+ const defaultConfig = BoneConfig.getDefault()
+ return defaultConfig.shadowRadius
+ }
+ set shadowRadius(value: BoneConfig['_shadowRadius']) {
+ this._shadowRadius = value
+ }
+
+ get shadowStrength(): NonNullable {
+ if (this._shadowStrength !== undefined) return this._shadowStrength
+ const defaultConfig = BoneConfig.getDefault()
+ return defaultConfig.shadowStrength
+ }
+ set shadowStrength(value: BoneConfig['_shadowStrength']) {
+ this._shadowStrength = value
+ }
+
+ get useNBT(): NonNullable {
+ if (this._useNBT !== undefined) return this._useNBT
+ const defaultConfig = BoneConfig.getDefault()
+ return defaultConfig.useNBT
+ }
+ set useNBT(value: BoneConfig['_useNBT']) {
+ this._useNBT = value
+ }
+
+ getDefault(): TextDisplayConfig {
+ return TextDisplayConfig.fromJSON({
+ billboard: 'center',
+ })
+ }
+
+ get tickingCommands(): NonNullable {
+ if (this._billboard !== undefined) return this._billboard
+ const defaultConfig = this.getDefault()
+ return defaultConfig.tickingCommands
+ }
+ set tickingCommands(value: NonNullable) {
+ this._billboard = value
+ }
+
+ public toJSON(): IBlueprintTextDisplayConfigJSON {
+ return {
+ billboard: this._billboard,
+ override_brightness: this._overrideBrightness,
+ brightness_override: this._brightnessOverride,
+ glowing: this._glowing,
+ override_glow_color: this._overrideGlowColor,
+ glow_color: this._glowColor,
+ invisible: this._invisible,
+ nbt: this._nbt,
+ shadow_radius: this._shadowRadius,
+ shadow_strength: this._shadowStrength,
+ use_nbt: this._useNBT,
+ }
+ }
+
+ public static fromJSON(json: IBlueprintTextDisplayConfigJSON): TextDisplayConfig {
+ const config = new TextDisplayConfig()
+ if (json.billboard !== undefined) config._billboard = json.billboard
+ if (json.override_brightness !== undefined)
+ config._overrideBrightness = json.override_brightness
+ if (json.brightness_override !== undefined)
+ config._brightnessOverride = json.brightness_override
+ if (json.glowing !== undefined) config._glowing = json.glowing
+ if (json.override_glow_color !== undefined)
+ config._overrideGlowColor = json.override_glow_color
+ if (json.glow_color !== undefined) config._glowColor = json.glow_color
+ if (json.invisible !== undefined) config._invisible = json.invisible
+ if (json.nbt !== undefined) config._nbt = json.nbt
+ if (json.shadow_radius !== undefined) config._shadowRadius = json.shadow_radius
+ if (json.shadow_strength !== undefined) config._shadowStrength = json.shadow_strength
+ if (json.use_nbt !== undefined) config._useNBT = json.use_nbt
+
+ return config
+ }
+
+ public toNBT(compound = new NbtCompound()) {
+ if (this.useNBT) {
+ const newData = NbtTag.fromString(this.nbt) as NbtCompound
+ for (const key of newData.keys()) {
+ compound.set(key, newData.get(key)!)
+ }
+ return compound
+ }
+
+ if (this._billboard) {
+ compound.set('billboard', new NbtString(this.billboard))
+ }
+
+ if (this.overrideBrightness) {
+ compound.set(
+ 'brightness',
+ new NbtCompound()
+ .set('block', new NbtFloat(this.brightnessOverride))
+ .set('sky', new NbtFloat(this.brightnessOverride))
+ )
+ }
+
+ if (this.glowing) {
+ compound.set('Glowing', new NbtByte(Number(this.glowing)))
+ }
+ if (this.overrideGlowColor) {
+ compound.set(
+ 'glow_color_override',
+ new NbtInt(Number(this.glowColor.replace('#', '0x')))
+ )
+ }
+
+ // TODO Figure out a good solution for toggling a bone's visibility...
+ // if (force || config.invisible !== defaultConfig.invisible) {
+ // compound.set('invisible', new NbtByte(1))
+ // }
+
+ if (this._shadowRadius) {
+ compound.set('shadow_radius', new NbtFloat(this.shadowRadius))
+ }
+
+ if (this._shadowStrength) {
+ compound.set('shadow_strength', new NbtFloat(this.shadowStrength))
+ }
+
+ return compound
+ }
+
+ public isDefault(): boolean {
+ return this.checkIfEqual(new TextDisplayConfig())
+ }
+
+ public checkIfEqual(other: TextDisplayConfig) {
+ return (
+ this._billboard === other._billboard &&
+ this._overrideBrightness === other._overrideBrightness &&
+ this._brightnessOverride === other._brightnessOverride &&
+ this._glowing === other._glowing &&
+ this._overrideGlowColor === other._overrideGlowColor &&
+ this._glowColor === other._glowColor &&
+ this._invisible === other._invisible &&
+ this._nbt === other._nbt &&
+ this._shadowRadius === other._shadowRadius &&
+ this._shadowStrength === other._shadowStrength &&
+ this._useNBT === other._useNBT
+ )
+ }
+}
diff --git a/src/outliner/resizableOutlinerElement.ts b/src/outliner/resizableOutlinerElement.ts
new file mode 100644
index 00000000..64b6902c
--- /dev/null
+++ b/src/outliner/resizableOutlinerElement.ts
@@ -0,0 +1,151 @@
+import { makeNotZero } from '../util/misc'
+
+export class ResizableOutlinerElement extends OutlinerElement {
+ // Properties
+ public name: string
+ public position: ArrayVector3
+ public rotation: ArrayVector3
+ public scale: ArrayVector3
+ public visibility: boolean
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ public preview_controller = PREVIEW_CONTROLLER
+
+ // Transform flags
+ public movable = true
+ public rotatable = true
+ public scalable = true
+ public resizable = true
+
+ // Resizable Workaround properties
+ get from() {
+ return this.position
+ }
+ set from(value: ArrayVector3) {
+ this.position = value
+ }
+ get to() {
+ return [0, 0, 0]
+ }
+ get stretch() {
+ return []
+ }
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ get uv_offset() {
+ return []
+ }
+
+ constructor(data: any, uuid = guid()) {
+ super(data, uuid)
+
+ this.extend(data)
+
+ this.name ??= 'resizable_outliner_element'
+ this.position ??= [0, 0, 0]
+ this.rotation ??= [0, 0, 0]
+ this.scale ??= [1, 1, 1]
+ this.visibility ??= true
+
+ // this.sanitizeName()
+ }
+
+ get origin() {
+ return this.position
+ }
+
+ getWorldCenter(): THREE.Vector3 {
+ Reusable.vec3.set(0, 0, 0)
+ // @ts-ignore
+ return THREE.fastWorldPosition(this.mesh, Reusable.vec2).add(Reusable.vec3) as THREE.Vector3
+ }
+
+ public extend(data: any) {
+ for (const key in ResizableOutlinerElement.properties) {
+ ResizableOutlinerElement.properties[key].merge(this, data)
+ }
+ if (data.visibility !== undefined) {
+ this.visibility = data.visibility
+ }
+
+ return this
+ }
+
+ selectLow() {
+ Project!.selected_elements.safePush(this)
+ this.selected = true
+ TickUpdates.selection = true
+ return this
+ }
+
+ size(axis?: number, floored?: boolean) {
+ if (axis === undefined) {
+ if (floored) return this.scale.map(n => Math.floor(n))
+ return [...this.scale]
+ }
+ if (floored) return Math.floor(this.scale[axis])
+ return this.scale[axis]
+ }
+
+ private oldScale: ArrayVector3 | undefined
+ resize(
+ val: number | ((n: number) => number),
+ axis: number
+ // negative: boolean,
+ // allowNegative: boolean,
+ // bidirectional: boolean
+ ) {
+ let before = this.oldScale !== undefined ? this.oldScale : this.size(axis)
+ console.log(before, this.oldScale)
+ if (before instanceof Array) before = before[axis]
+ // For some unknown reason scale is not inverted on the y axis
+ const sign = before < 0 && axis !== 1 ? -1 : 1
+
+ const modify = typeof val === 'function' ? val : (n: number) => n + (val * sign) / 16
+
+ this.scale[axis] = modify(before)
+
+ this.preview_controller.updateGeometry?.(this)
+ this.preview_controller.updateTransform(this)
+ }
+}
+new Property(ResizableOutlinerElement, 'string', 'name', { default: 'resizable_outliner_element' })
+new Property(ResizableOutlinerElement, 'vector', 'position', { default: [0, 0, 0] })
+new Property(ResizableOutlinerElement, 'vector', 'rotation', { default: [0, 0, 0] })
+new Property(ResizableOutlinerElement, 'vector', 'scale', { default: [1, 1, 1] })
+new Property(ResizableOutlinerElement, 'string', 'visibility', { default: true })
+
+export const PREVIEW_CONTROLLER = new NodePreviewController(ResizableOutlinerElement, {
+ setup(el: ResizableOutlinerElement) {
+ const mesh = new THREE.Mesh()
+ mesh.fix_rotation = new THREE.Euler(0, 0, 0, 'ZYX')
+ mesh.fix_rotation.x = Math.degToRad(el.rotation[0])
+ mesh.fix_rotation.y = Math.degToRad(el.rotation[1])
+ mesh.fix_rotation.z = Math.degToRad(el.rotation[2])
+ mesh.fix_position = new THREE.Vector3(...el.position)
+ mesh.fix_scale = new THREE.Vector3(...el.scale)
+ Project!.nodes_3d[el.uuid] = mesh
+
+ el.preview_controller.updateGeometry?.(el)
+ // el.preview_controller.updateTransform(el)
+ el.preview_controller.dispatchEvent('setup', { element: el })
+ },
+ updateTransform(el: ResizableOutlinerElement) {
+ NodePreviewController.prototype.updateTransform.call(el.preview_controller, el)
+ if (el.mesh.fix_position) {
+ el.mesh.fix_position.set(...el.position)
+ if (el.parent instanceof Group) {
+ el.mesh.fix_position.x -= el.parent.origin[0]
+ el.mesh.fix_position.y -= el.parent.origin[1]
+ el.mesh.fix_position.z -= el.parent.origin[2]
+ }
+ }
+ if (el.mesh.fix_rotation) {
+ el.mesh.fix_rotation.copy(el.mesh.rotation)
+ }
+ if (el.mesh.fix_scale) {
+ el.mesh.fix_scale.set(...el.scale)
+ makeNotZero(el.mesh.fix_scale)
+ }
+ // @ts-ignore
+ el.preview_controller.dispatchEvent('update_transform', { element: el })
+ },
+})
diff --git a/src/outliner/textDisplay.ts b/src/outliner/textDisplay.ts
index 3d906ad2..1f32a670 100644
--- a/src/outliner/textDisplay.ts
+++ b/src/outliner/textDisplay.ts
@@ -1,247 +1,555 @@
-import { createAction } from '../util/moddingTools'
-import * as events from '../events'
-import { ajModelFormat } from '../modelFormat'
-import { JsonText } from '../minecraft'
-
-const DEFAULT_TEXT = 'The quick brown fox jumps over the lazy dog.'
-const SIZE_DIVISOR = 4.75
-
-events.LOAD.subscribe(() => {
- console.log('Animated Java loaded!')
- class TextDisplayElement extends OutlinerElement {
- static title = 'Text Display'
- static type = 'textDisplay'
- static icon = 'text_fields'
- static movable = true
- static resizable = true
- static rotatable = true
- static needsUniqueName = true
- static menu = new Menu([...Outliner.control_menu_group, '_', 'rename', 'delete'])
- static buttons = [
- Outliner.buttons.export,
- Outliner.buttons.locked,
- Outliner.buttons.visibility,
+import {
+ BLUEPRINT_FORMAT,
+ IBlueprintTextDisplayConfigJSON,
+ isCurrentFormat,
+} from '../blueprintFormat'
+import { PACKAGE } from '../constants'
+import { createAction, createBlockbenchMod } from '../util/moddingTools'
+// import * as MinecraftFull from '../assets/MinecraftFull.json'
+import { getVanillaFont } from '../systems/minecraft/fontManager'
+import { JsonText } from '../systems/minecraft/jsonText'
+import { Valuable } from '../util/stores'
+import { toSafeFuntionName } from '../util/minecraftUtil'
+import { TextDisplayConfig } from '../nodeConfigs'
+import { TEXT_DISPLAY_CONFIG_ACTION } from '../interface/textDisplayConfigDialog'
+import { events } from '../util/events'
+import { translate } from '../util/translation'
+import { ResizableOutlinerElement } from './resizableOutlinerElement'
+import { VanillaBlockDisplay } from './vanillaBlockDisplay'
+import { VanillaItemDisplay } from './vanillaItemDisplay'
+
+interface TextDisplayOptions {
+ name?: string
+ text?: string
+ position?: ArrayVector3
+ rotation?: ArrayVector3
+ scale?: ArrayVector3
+ lineLength?: number
+ backgroundColor?: string
+ backgroundAlpha?: number
+ align?: Alignment
+ visibility?: boolean
+}
+type Alignment = 'left' | 'center' | 'right'
+
+export class TextDisplay extends ResizableOutlinerElement {
+ static type = `${PACKAGE.name}:text_display`
+ static selected: TextDisplay[] = []
+ static all: TextDisplay[] = []
+
+ public type = TextDisplay.type
+ public icon = 'text_fields'
+ public needsUniqueName = true
+
+ // Properties
+ public align: Alignment
+ public config: IBlueprintTextDisplayConfigJSON
+
+ public menu = new Menu([
+ ...Outliner.control_menu_group,
+ TEXT_DISPLAY_CONFIG_ACTION,
+ '_',
+ 'rename',
+ 'delete',
+ ])
+ public buttons = [Outliner.buttons.export, Outliner.buttons.locked, Outliner.buttons.visibility]
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ public preview_controller = PREVIEW_CONTROLLER
+
+ public ready = false
+ public textError = new Valuable('')
+
+ private _updating = false
+ private _text = new Valuable('Hello World!')
+ private _newText: string | undefined
+ private _lineWidth = new Valuable(200)
+ private _newLineWidth: number | undefined
+ private _backgroundColor = new Valuable('#000000')
+ private _newBackgroundColor: string | undefined
+ private _backgroundAlpha = new Valuable(0.25)
+ private _newBackgroundAlpha: number | undefined
+ private _shadow = new Valuable(false)
+ private _newShadow: boolean | undefined
+
+ constructor(data: TextDisplayOptions, uuid = guid()) {
+ super(data, uuid)
+ TextDisplay.all.push(this)
+
+ for (const key in TextDisplay.properties) {
+ TextDisplay.properties[key].reset(this)
+ }
+
+ this.name = 'vanilla_block_display'
+ this.extend(data)
+
+ this.name ??= 'text_display'
+ this.position ??= [0, 0, 0]
+ this.rotation ??= [0, 0, 0]
+ this.scale ??= [1, 1, 1]
+ this.align ??= 'center'
+ this.visibility ??= true
+ this.config ??= {}
+
+ this.sanitizeName()
+
+ this._text.subscribe(v => {
+ this._newText = v
+ void this.updateText()
+ })
+ this._lineWidth.subscribe(v => {
+ this._newLineWidth = v
+ void this.updateText()
+ })
+ this._backgroundColor.subscribe(v => {
+ this._newBackgroundColor = v
+ void this.updateText()
+ })
+ this._backgroundAlpha.subscribe(v => {
+ this._newBackgroundAlpha = v
+ void this.updateText()
+ })
+ this._shadow.subscribe(v => {
+ this._newShadow = v
+ void this.updateText()
+ })
+ }
+
+ public sanitizeName(): string {
+ this.name = toSafeFuntionName(this.name)
+ const otherNodes = [
+ ...TextDisplay.all.filter(v => v.uuid !== this.uuid),
+ ...Group.all,
+ ...VanillaBlockDisplay.all,
+ ...VanillaItemDisplay.all,
]
- // eslint-disable-next-line @typescript-eslint/naming-convention
- static preview_controller: NodePreviewController
-
- text = 'Text Display'
- type = TextDisplayElement.type
- icon = TextDisplayElement.icon
- position: ArrayVector3 = [0, 0, 0]
- rotation: ArrayVector3 = [0, 0, 0]
- visibility = true
-
- constructor(data: Record, uuid: string = guid()) {
- super(data, uuid)
- for (const key in TextDisplayElement.properties) {
- TextDisplayElement.properties[key].reset(this)
+ const otherNames = new Set(otherNodes.map(v => v.name))
+
+ if (!otherNames.has(this.name)) {
+ return this.name
+ }
+
+ let i = 1
+ const match = this.name.match(/\d+$/)
+ if (match) {
+ i = parseInt(match[0])
+ this.name = this.name.slice(0, -match[0].length)
+ }
+
+ let maxTries = 10000
+ while (maxTries-- > 0) {
+ const newName = `${this.name}${i}`
+ if (!otherNames.has(newName)) {
+ this.name = newName
+ return newName
}
+ i++
+ }
+
+ throw new Error('Could not make TextDisplay name unique!')
+ }
+
+ get text() {
+ if (this._text === undefined) return TextDisplay.properties['text'].default as string
+ return this._text.get()
+ }
+
+ set text(value) {
+ if (this._text === undefined) return
+ if (value === this.text) return
+ this._text.set(value)
+ }
+
+ get lineWidth() {
+ if (this._lineWidth === undefined)
+ return TextDisplay.properties['lineWidth'].default as number
+ return this._lineWidth.get()
+ }
+
+ set lineWidth(value) {
+ if (this._lineWidth === undefined) return
+ this._lineWidth.set(value)
+ }
+
+ get backgroundColor() {
+ if (this._backgroundColor === undefined)
+ return TextDisplay.properties['backgroundColor'].default as string
+ return this._backgroundColor.get()
+ }
- data && typeof data === 'object' && this.extend(data)
+ set backgroundColor(value) {
+ if (this._backgroundColor === undefined) return
+ this._backgroundColor.set(value)
+ }
+
+ get backgroundAlpha() {
+ if (this._backgroundAlpha === undefined)
+ return TextDisplay.properties['backgroundAlpha'].default as number
+ return this._backgroundAlpha.get()
+ }
+
+ set backgroundAlpha(value) {
+ if (this._backgroundAlpha === undefined) return
+ this._backgroundAlpha.set(value)
+ }
+
+ get shadow() {
+ if (this._shadow === undefined) return TextDisplay.properties['shadow'].default as boolean
+ return this._shadow.get()
+ }
+
+ set shadow(value) {
+ if (this._shadow === undefined) return
+ this._shadow.set(value)
+ }
+
+ getUndoCopy() {
+ const copy = new TextDisplay(this)
+
+ for (const key in TextDisplay.properties) {
+ TextDisplay.properties[key].copy(this, copy)
}
- get origin() {
- return this.position
+ copy.uuid = this.uuid
+ copy.type = this.type
+ delete copy.parent
+ return copy
+ }
+
+ getSaveCopy() {
+ const el: any = {}
+ for (const key in TextDisplay.properties) {
+ TextDisplay.properties[key].copy(this, el)
}
+ el.uuid = this.uuid
+ el.type = this.type
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+ return el
+ }
- getWorldCenter() {
- // @ts-ignore
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
- return THREE.fastWorldPosition(this.mesh, Reusable.vec2)
+ select() {
+ if (Group.selected) {
+ Group.selected.unselect()
}
- extend(object: Record) {
- for (const key in TextDisplayElement.properties) {
- TextDisplayElement.properties[key].merge(this, object)
+ if (!Pressing.ctrl && !Pressing.shift) {
+ if (Cube.selected.length) {
+ Cube.selected.forEachReverse(el => el.unselect())
}
+ if (selected.length) {
+ selected.forEachReverse(el => el !== this && el.unselect())
+ }
+ }
- this.sanitizeName()
- return this
+ TextDisplay.selected.safePush(this)
+ this.selectLow()
+ this.showInOutliner()
+ updateSelection()
+ if (Animator.open && Blockbench.Animation.selected) {
+ Blockbench.Animation.selected.getBoneAnimator(this).select()
}
+ return this
+ }
- getUndoCopy() {
- const copy = new TextDisplayElement(this)
- copy.uuid = this.uuid
- delete copy.parent
- return copy
+ unselect() {
+ if (!this.selected) return
+ if (
+ Animator.open &&
+ Timeline.selected_animator &&
+ Timeline.selected_animator.element === this &&
+ Timeline.selected
+ ) {
+ Timeline.selected.empty()
}
+ Project!.selected_elements.remove(this)
+ TextDisplay.selected.remove(this)
+ this.selected = false
+ TickUpdates.selection = true
+ }
- getSaveCopy() {
- const el: any = {}
- for (const key in TextDisplayElement.properties) {
- TextDisplayElement.properties[key].copy(this, el)
+ async updateText() {
+ if (this._updating) return
+ this._updating = true
+ let latestMesh: THREE.Mesh | undefined
+ while (
+ this._newText !== undefined ||
+ this._newLineWidth !== undefined ||
+ this._newBackgroundColor !== undefined ||
+ this._newBackgroundAlpha !== undefined ||
+ this._newShadow !== undefined
+ ) {
+ let text: JsonText | undefined
+ this.textError.set('')
+ try {
+ text = JsonText.fromString(this.text)
+ } catch (e: any) {
+ console.error(e)
+ this.textError.set(e.message as string)
+ this._updating = false
}
- el.type = 'textDisplay'
- el.uuid = this.uuid
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
- return el
+ this._newText = undefined
+ this._newLineWidth = undefined
+ this._newBackgroundColor = undefined
+ this._newBackgroundAlpha = undefined
+ this._newShadow = undefined
+ if (!text) continue
+ latestMesh = await this.setText(text)
}
+ this._updating = false
+ return latestMesh
+ }
- select(event?: any, isOutlinerClick?: boolean) {
- super.select(event, isOutlinerClick)
- if (Animator.open && Blockbench.Animation.selected) {
- // eslint-disable-next-line @typescript-eslint/no-extra-semi
- ;(Blockbench.Animation.selected as _Animation).getBoneAnimator(this).select()
- }
+ async waitForReady() {
+ while (!this.ready) {
+ await new Promise(resolve => setTimeout(resolve, 100))
+ }
+ }
+
+ private async setText(jsonText: JsonText) {
+ await this.waitForReady()
+ const font = await getVanillaFont()
+ // Hide the geo while rendering
+
+ const { mesh, outline } = await font.generateTextMesh({
+ jsonText,
+ maxLineWidth: this.lineWidth,
+ backgroundColor: this.backgroundColor,
+ backgroundAlpha: this.backgroundAlpha,
+ shadow: this.shadow,
+ })
+ mesh.name = this.uuid + '_text'
+ const previousMesh = this.mesh.children.find(v => v.name === mesh.name)
+ if (previousMesh) this.mesh.remove(previousMesh)
+ this.mesh.add(mesh)
+
+ outline.name = this.uuid + '_outline'
+ outline.visible = this.selected
+ this.mesh.outline = outline
+ const previousOutline = this.mesh.children.find(v => v.name === outline.name)
+ if (previousOutline) this.mesh.remove(previousOutline)
+ this.mesh.add(outline)
+ this.mesh.visible = this.visibility
+ return mesh
+ }
+}
+new Property(TextDisplay, 'string', 'text', { default: '"Hello World!"' })
+new Property(TextDisplay, 'number', 'lineWidth', { default: 200 })
+new Property(TextDisplay, 'string', 'backgroundColor', { default: '#000000' })
+new Property(TextDisplay, 'number', 'backgroundAlpha', { default: 0.25 })
+new Property(TextDisplay, 'string', 'align', { default: 'center' })
+new Property(TextDisplay, 'object', 'config', {
+ get default() {
+ return new TextDisplayConfig().toJSON()
+ },
+})
+OutlinerElement.registerType(TextDisplay, TextDisplay.type)
+
+export const PREVIEW_CONTROLLER = new NodePreviewController(TextDisplay, {
+ setup(el: TextDisplay) {
+ ResizableOutlinerElement.prototype.preview_controller.setup(el)
+ // Minecraft's transparency is funky 😭
+ Project!.nodes_3d[el.uuid].renderOrder = -1
+
+ void getVanillaFont().then(() => {
+ el.ready = true
+ el.preview_controller.updateTransform(el)
+ el.preview_controller.updateGeometry(el)
+ el.preview_controller.dispatchEvent('setup', { element: el })
+ })
+ },
+ updateGeometry(el: TextDisplay) {
+ void el.updateText().then(() => {
+ el.preview_controller.updateTransform(el)
+ })
+ },
+ updateTransform(el: TextDisplay) {
+ ResizableOutlinerElement.prototype.preview_controller.updateTransform(el)
+ },
+})
+
+class TextDisplayAnimator extends BoneAnimator {
+ private _name: string
+
+ public uuid: string
+ public element: TextDisplay | undefined
+
+ constructor(uuid: string, animation: _Animation, name: string) {
+ super(uuid, animation, name)
+ this.uuid = uuid
+ this._name = name
+ }
+
+ getElement() {
+ this.element = OutlinerNode.uuids[this.uuid] as TextDisplay
+ return this.element
+ }
+
+ select() {
+ this.getElement()
+ if (!this.element) {
+ unselectAllElements()
+ return this
+ }
+
+ if (this.element.locked) {
return this
}
- unselect() {
- super.unselect()
- if (
- Animator.open &&
- Timeline.selected_animator &&
- Timeline.selected_animator.element == this
- ) {
- Timeline.selected_animator.selected = false
+ if (!this.element.selected && this.element) {
+ this.element.select()
+ }
+ GeneralAnimator.prototype.select.call(this)
+
+ if (
+ this[Toolbox.selected.animation_channel] &&
+ ((Timeline.selected && Timeline.selected.length === 0) ||
+ (Timeline.selected && (Timeline.selected[0].animator as any)) !== this)
+ ) {
+ let nearest: _Keyframe | undefined
+ this[Toolbox.selected.animation_channel].forEach((kf: _Keyframe) => {
+ if (Math.abs(kf.time - Timeline.time) < 0.002) {
+ nearest = kf
+ }
+ })
+ if (nearest) {
+ nearest.select()
}
}
+
+ if (this.element && this.element.parent && this.element.parent !== 'root') {
+ this.element.parent.openUp()
+ }
+
+ return this
}
- new Property(TextDisplayElement, 'string', 'name', { default: 'Text Display' })
- new Property(TextDisplayElement, 'string', 'text', { default: DEFAULT_TEXT })
- new Property(TextDisplayElement, 'vector', 'position')
- new Property(TextDisplayElement, 'vector', 'rotation')
- new Property(TextDisplayElement, 'boolean', 'visibility', { default: true })
-
- OutlinerElement.registerType(TextDisplayElement, 'textDisplay')
-
- const PreviewController = new NodePreviewController(TextDisplayElement, {
- setup(element: TextDisplayElement) {
- // const measure = new CanvasFrame(1, 1)
- // measure.ctx.font = FONT
- // measure.ctx.filter = PIXEL_FILTER
- // const metrics = measure.ctx.measureText(element.text)
- // const width = Math.ceil(metrics.width)
- // const height = Math.ceil(metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent)
-
- // const canvas = new CanvasFrame(width, height)
- // canvas.ctx.filter = PIXEL_FILTER
- // canvas.ctx.font = FONT
- // canvas.ctx.textBaseline = 'top'
- // canvas.ctx.fillStyle = '#ffffff'
- // canvas.ctx.fillText(element.text, 0, 0)
-
- // const text = new JsonText([
- // { text: 'Hello World!', color: 'red' },
- // { text: 'Hello World Again!' },
- // ])
- const text = new JsonText([
- { text: 'MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM' },
- ])
- const canvas = text.renderToCanvas()
- const width = canvas.width
- const height = canvas.height
-
- const texture = new THREE.CanvasTexture(canvas.canvas)
- // @ts-ignore
- texture.colorSpace = THREE.sRGBEncoding
- texture.magFilter = THREE.NearestFilter
- console.log('texture', texture)
- texture.image.style.border = '2px solid black'
- jQuery('#preview')[0].appendChild(texture.image)
-
- const material = new THREE.MeshBasicMaterial({
- map: texture,
- transparent: true,
- alphaTest: 0.5,
- })
- const textGeometry = new THREE.BoxGeometry(
- width / SIZE_DIVISOR,
- height / SIZE_DIVISOR,
- 0
- )
- // @ts-ignore
- // Remove the backface
- textGeometry.attributes.uv.array = textGeometry.attributes.uv.array.map(
- (v: number, i: number) => (i < 40 ? 0 : v)
- )
-
- // Background
- const backgroundGeometry = new THREE.BoxGeometry(
- (width + 2) / SIZE_DIVISOR,
- (height + 8) / SIZE_DIVISOR,
- 0
- )
- const backgroundMaterial = new THREE.MeshBasicMaterial({
- color: 0x000000,
- transparent: true,
- opacity: 0.255,
- })
- // @ts-ignore
- // Remove the backface
- backgroundGeometry.index!.array = backgroundGeometry.index!.array.map(
- (v: number, i: number) => (i < 30 ? 0 : v)
- )
- const backgroundMesh = new THREE.Mesh(backgroundGeometry, backgroundMaterial)
- // Prevent z-fighting
- backgroundMesh.position.z = 0.01
- // Align edges of text
- backgroundMesh.position.x = 1 / SIZE_DIVISOR
-
- const mesh = new THREE.Mesh(textGeometry, material)
- Project!.nodes_3d[element.uuid] = mesh
- mesh.name = element.name
- mesh.type = element.type
- // @ts-ignore
- mesh.isElement = true
- mesh.visible = element.visibility
- mesh.rotation.order = 'ZYX'
- mesh.add(backgroundMesh)
- // Align bottom of mesh with the origin
- mesh.position.y = height / SIZE_DIVISOR / 2 + 4 / SIZE_DIVISOR
-
- PreviewController.updateTransform(element)
- PreviewController.dispatchEvent('setup', { element })
- },
- })
-
- const ACTION = createAction('animated_java:add_text_display', {
- name: 'Add Text Display',
- icon: 'text_fields',
- category: 'animated_java',
- condition: () => Format.id === ajModelFormat.id && Mode.selected.id === 'edit',
- click() {
- Undo.initEdit({ outliner: true, elements: [], selection: true })
-
- const textDisplay = new TextDisplayElement({}).init()
- const group = getCurrentGroup()
- textDisplay.addTo(group)
+ doRender() {
+ this.getElement()
+ return !!(this.element && this.element.mesh)
+ }
- Format.bone_rig && group && textDisplay.extend({ position: group.origin.slice() })
- Group.selected && Group.selected.unselect()
+ displayRotation(arr: ArrayVector3 | ArrayVector4, multiplier = 1) {
+ const bone = this.getElement().mesh
- textDisplay.select()
+ if (bone.fix_rotation) {
+ bone.rotation.copy(bone.fix_rotation as THREE.Euler)
+ }
- Undo.finishEdit('Add Text', {
- outliner: true,
- elements: selected,
- selection: true,
+ if (arr) {
+ if (arr.length === 4) {
+ const added_rotation = new THREE.Euler().setFromQuaternion(
+ new THREE.Quaternion().fromArray(arr),
+ 'ZYX'
+ )
+ bone.rotation.x -= added_rotation.x * multiplier
+ bone.rotation.y -= added_rotation.y * multiplier
+ bone.rotation.z += added_rotation.z * multiplier
+ } else {
+ bone.rotation.x -= Math.degToRad(arr[0]) * multiplier
+ bone.rotation.y -= Math.degToRad(arr[1]) * multiplier
+ bone.rotation.z += Math.degToRad(arr[2]) * multiplier
+ }
+ }
+ if (this.rotation_global) {
+ const quat = bone.parent?.getWorldQuaternion(Reusable.quat1)
+ if (!quat) return this
+ quat.invert()
+ bone.quaternion.premultiply(quat)
+ }
+ return this
+ }
+
+ displayPosition(arr: ArrayVector3, multiplier = 1) {
+ const bone = this.getElement().mesh
+ if (bone.fix_position) {
+ bone.position.copy(bone.fix_position as THREE.Vector3)
+ }
+ if (arr) {
+ bone.position.x -= arr[0] * multiplier
+ bone.position.y += arr[1] * multiplier
+ bone.position.z += arr[2] * multiplier
+ }
+ return this
+ }
+
+ displayScale(arr: ArrayVector3, multiplier = 1) {
+ if (!arr) return this
+ const bone = this.getElement().mesh
+ if (bone.fix_scale) {
+ bone.scale.copy(bone.fix_scale)
+ }
+ bone.scale.x *= 1 + (arr[0] - 1) * multiplier || 0.00001
+ bone.scale.y *= 1 + (arr[1] - 1) * multiplier || 0.00001
+ bone.scale.z *= 1 + (arr[2] - 1) * multiplier || 0.00001
+ return this
+ }
+}
+TextDisplayAnimator.prototype.type = TextDisplay.type
+TextDisplay.animator = TextDisplayAnimator as any
+
+createBlockbenchMod(
+ `${PACKAGE.name}:textDisplay`,
+ {
+ subscriptions: [] as Array<() => void>,
+ },
+ context => {
+ Interface.Panels.outliner.menu.addAction(CREATE_ACTION, 3)
+ Toolbars.outliner.add(CREATE_ACTION, 0)
+ MenuBar.menus.edit.addAction(CREATE_ACTION, 8)
+
+ context.subscriptions.push(
+ events.SELECT_PROJECT.subscribe(project => {
+ if (project.format.id !== BLUEPRINT_FORMAT.id) return
+ console.log('SELECT_PROJECT')
+ project.textDisplays ??= []
+ TextDisplay.all.empty()
+ TextDisplay.all.push(...project.textDisplays)
+ }),
+ events.UNSELECT_PROJECT.subscribe(project => {
+ if (project.format.id !== BLUEPRINT_FORMAT.id) return
+ console.log('UNSELECT_PROJECT')
+ project.textDisplays = [...TextDisplay.all]
+ TextDisplay.all.empty()
})
+ )
+ return context
+ },
+ context => {
+ Interface.Panels.outliner.menu.removeAction(CREATE_ACTION.id)
+ Toolbars.outliner.remove(CREATE_ACTION)
+ MenuBar.menus.edit.removeAction(CREATE_ACTION.id)
+
+ context.subscriptions.forEach(unsub => unsub())
+ }
+)
+
+export const CREATE_ACTION = createAction(`${PACKAGE.name}:create_text_display`, {
+ name: translate('action.create_text_display.title'),
+ icon: 'text_fields',
+ category: 'animated_java',
+ condition() {
+ return isCurrentFormat() && Mode.selected.id === Modes.options.edit.id
+ },
+ click() {
+ Undo.initEdit({ outliner: true, elements: [], selection: true })
+
+ const textDisplay = new TextDisplay({}).init()
+ const group = getCurrentGroup()
+
+ if (group instanceof Group) {
+ textDisplay.addTo(group)
+ textDisplay.extend({ position: group.origin.slice() as ArrayVector3 })
+ }
- return textDisplay
- },
- })
+ selected.forEachReverse(el => el.unselect())
+ Group.selected && Group.selected.unselect()
+ textDisplay.select()
- Interface.Panels.outliner.menu.addAction(ACTION, 3)
- Toolbars.outliner.add(ACTION, 0)
- MenuBar.menus.edit.addAction(ACTION, 8)
-})
+ Undo.finishEdit('Create Text Display', {
+ outliner: true,
+ elements: selected,
+ selection: true,
+ })
-// export async function createText() {
-// const font = await new FontFace('Minecraft', url as Buffer, {}).load()
-// document.fonts.add(font)
-
-// const texture = new Blockbench.Texture({
-// name: 'font-test',
-// width: 128,
-// height: 128,
-// })
-
-// const ctx = texture.canvas.getContext('2d')!
-// ctx.fillStyle = 'black'
-// ctx.font = '20px Minecraft'
-// ctx.fillText('Hello, world!', 0, 16)
-// return texture
-// }
+ return textDisplay
+ },
+})
diff --git a/src/outliner/vanillaBlockDisplay.ts b/src/outliner/vanillaBlockDisplay.ts
new file mode 100644
index 00000000..66f32599
--- /dev/null
+++ b/src/outliner/vanillaBlockDisplay.ts
@@ -0,0 +1,505 @@
+import { IBlueprintBoneConfigJSON, isCurrentFormat } from '../blueprintFormat'
+import { PACKAGE } from '../constants'
+import { VANILLA_BLOCK_DISPLAY_CONFIG_ACTION } from '../interface/vanillaBlockDisplayConfigDialog'
+import { BoneConfig } from '../nodeConfigs'
+import { getBlockModel } from '../systems/minecraft/blockModelManager'
+import { BlockStateValue, getBlockState } from '../systems/minecraft/blockstateManager'
+import { MINECRAFT_REGISTRY } from '../systems/minecraft/registryManager'
+import { getCurrentVersion } from '../systems/minecraft/versionManager'
+import { events } from '../util/events'
+import { parseBlock, toSafeFuntionName } from '../util/minecraftUtil'
+import { createAction, createBlockbenchMod } from '../util/moddingTools'
+import { Valuable } from '../util/stores'
+import { translate } from '../util/translation'
+import { ResizableOutlinerElement } from './resizableOutlinerElement'
+import { TextDisplay } from './textDisplay'
+import { VanillaItemDisplay } from './vanillaItemDisplay'
+
+const ERROR_OUTLINE_MATERIAL = Canvas.outlineMaterial.clone()
+ERROR_OUTLINE_MATERIAL.color.set('#ff0000')
+
+interface VanillaBlockDisplayOptions {
+ name?: string
+ block?: string
+ position?: ArrayVector3
+ rotation?: ArrayVector3
+ scale?: ArrayVector3
+ visibility?: boolean
+}
+
+export class VanillaBlockDisplay extends ResizableOutlinerElement {
+ static type = `${PACKAGE.name}:vanilla_block_display`
+ static selected: VanillaBlockDisplay[] = []
+ static all: VanillaBlockDisplay[] = []
+
+ public type = VanillaBlockDisplay.type
+ public icon = 'deployed_code'
+ public needsUniqueName = true
+
+ // Properties
+ public _block = new Valuable('minecraft:stone')
+ public config: IBlueprintBoneConfigJSON
+
+ public error = new Valuable('')
+
+ public menu = new Menu([
+ ...Outliner.control_menu_group,
+ VANILLA_BLOCK_DISPLAY_CONFIG_ACTION,
+ '_',
+ 'rename',
+ 'delete',
+ ])
+ public buttons = [Outliner.buttons.export, Outliner.buttons.locked, Outliner.buttons.visibility]
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ public preview_controller = PREVIEW_CONTROLLER
+
+ constructor(data: VanillaBlockDisplayOptions, uuid = guid()) {
+ super(data, uuid)
+ VanillaBlockDisplay.all.push(this)
+
+ for (const key in VanillaBlockDisplay.properties) {
+ VanillaBlockDisplay.properties[key].reset(this)
+ }
+
+ this.name = 'vanilla_block_display'
+ this.extend(data)
+
+ this.block ??= 'minecraft:stone'
+ this.config ??= {}
+
+ const updateBlock = async (newBlock: string) => {
+ if (!MINECRAFT_REGISTRY.block) {
+ requestAnimationFrame(() => void updateBlock(newBlock))
+ return
+ }
+ const parsed = await parseBlock(newBlock)
+ if (!parsed) {
+ this.error.set('Invalid block ID.')
+ } else if (
+ (parsed.resource.namespace === 'minecraft' || parsed.resource.namespace === '') &&
+ MINECRAFT_REGISTRY.block.has(parsed.resource.name)
+ ) {
+ this.error.set('')
+ this.preview_controller.updateGeometry(this)
+ } else {
+ this.error.set(`This block does not exist in Minecraft ${getCurrentVersion()!.id}.`)
+ }
+ if (this.mesh?.outline instanceof THREE.LineSegments) {
+ if (this.error.get()) this.mesh.outline.material = ERROR_OUTLINE_MATERIAL
+ else this.mesh.outline.material = Canvas.outlineMaterial
+ }
+ }
+
+ this._block.subscribe(value => {
+ void updateBlock(value)
+ })
+ }
+
+ get block() {
+ if (this._block === undefined) return 'minecraft:stone'
+ return this._block.get()
+ }
+ set block(value: string) {
+ if (this._block === undefined) return
+ if (this.block === value) return
+ this._block.set(value)
+ }
+
+ public sanitizeName(): string {
+ this.name = toSafeFuntionName(this.name)
+ const otherNodes = [
+ ...VanillaBlockDisplay.all.filter(v => v.uuid !== this.uuid),
+ ...Group.all,
+ ...TextDisplay.all,
+ ...VanillaItemDisplay.all,
+ ]
+ const otherNames = new Set(otherNodes.map(v => v.name))
+
+ if (!otherNames.has(this.name)) {
+ return this.name
+ }
+
+ let i = 1
+ const match = this.name.match(/\d+$/)
+ if (match) {
+ i = parseInt(match[0])
+ this.name = this.name.slice(0, -match[0].length)
+ }
+
+ let maxTries = 10000
+ while (maxTries-- > 0) {
+ const newName = `${this.name}${i}`
+ if (!otherNames.has(newName)) {
+ this.name = newName
+ return newName
+ }
+ i++
+ }
+
+ throw new Error('Could not make VanillaBlockDisplay name unique!')
+ }
+
+ getUndoCopy() {
+ const copy = {} as VanillaBlockDisplayOptions & { uuid: string; type: string }
+
+ for (const key in VanillaBlockDisplay.properties) {
+ VanillaBlockDisplay.properties[key].copy(this, copy)
+ }
+
+ copy.uuid = this.uuid
+ copy.type = this.type
+ return copy
+ }
+
+ getSaveCopy() {
+ const el: any = {}
+ for (const key in VanillaBlockDisplay.properties) {
+ VanillaBlockDisplay.properties[key].copy(this, el)
+ }
+ el.uuid = this.uuid
+ el.type = this.type
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+ return el
+ }
+
+ select() {
+ if (Group.selected) {
+ Group.selected.unselect()
+ }
+ if (!Pressing.ctrl && !Pressing.shift) {
+ if (Cube.selected.length) {
+ Cube.selected.forEachReverse(el => el.unselect())
+ }
+ if (selected.length) {
+ selected.forEachReverse(el => el !== this && el.unselect())
+ }
+ }
+
+ VanillaBlockDisplay.selected.safePush(this)
+ this.selectLow()
+ this.showInOutliner()
+ updateSelection()
+ if (Animator.open && Blockbench.Animation.selected) {
+ Blockbench.Animation.selected.getBoneAnimator(this).select()
+ }
+ return this
+ }
+
+ unselect() {
+ if (!this.selected) return
+ if (
+ Animator.open &&
+ Timeline.selected_animator &&
+ Timeline.selected_animator.element === this &&
+ Timeline.selected
+ ) {
+ Timeline.selected.empty()
+ }
+ Project!.selected_elements.remove(this)
+ VanillaBlockDisplay.selected.remove(this)
+ this.selected = false
+ TickUpdates.selection = true
+ this.preview_controller.updateHighlight(this)
+ }
+}
+new Property(VanillaBlockDisplay, 'string', 'block', { default: 'minecraft:stone' })
+new Property(VanillaBlockDisplay, 'object', 'config', {
+ get default() {
+ return new BoneConfig().toJSON()
+ },
+})
+OutlinerElement.registerType(VanillaBlockDisplay, VanillaBlockDisplay.type)
+
+export const PREVIEW_CONTROLLER = new NodePreviewController(VanillaBlockDisplay, {
+ setup(el: VanillaBlockDisplay) {
+ ResizableOutlinerElement.prototype.preview_controller.setup(el)
+ },
+ updateGeometry(el: VanillaBlockDisplay) {
+ if (!el.mesh) return
+
+ void getBlockModel(el.block)
+ .then(result => {
+ if (!result?.mesh) return
+
+ el.mesh.clear()
+ result.outline.name = el.uuid + '_outline'
+ result.outline.visible = el.selected
+ el.mesh.outline = result.outline
+ el.mesh.add(result.mesh)
+ el.mesh.add(result.outline)
+
+ el.preview_controller.updateHighlight(el)
+ el.preview_controller.updateTransform(el)
+ el.mesh.visible = el.visibility
+ TickUpdates.selection = true
+ })
+ .catch(err => {
+ console.error(err)
+ if (typeof err.message === 'string') {
+ el.error.set(err.message as string)
+ }
+ })
+ .finally(() => {
+ if (el.mesh?.outline instanceof THREE.LineSegments) {
+ if (el.error.get()) el.mesh.outline.material = ERROR_OUTLINE_MATERIAL
+ else el.mesh.outline.material = Canvas.outlineMaterial
+ }
+ })
+ },
+ updateTransform(el: VanillaBlockDisplay) {
+ ResizableOutlinerElement.prototype.preview_controller.updateTransform(el)
+ },
+ updateHighlight(el: VanillaBlockDisplay, force?: boolean | VanillaBlockDisplay) {
+ if (!isCurrentFormat() || !el?.mesh) return
+ const highlighted = Modes.edit && (force === true || force === el || el.selected) ? 1 : 0
+
+ const blockModel = el.mesh.children.at(0) as THREE.Mesh
+ if (!blockModel) return
+ for (const child of blockModel.children) {
+ if (!(child instanceof THREE.Mesh)) continue
+ const highlight = child.geometry.attributes.highlight
+
+ if (highlight.array[0] != highlighted) {
+ // @ts-ignore
+ highlight.array.set(Array(highlight.count).fill(highlighted))
+ highlight.needsUpdate = true
+ }
+ }
+ },
+})
+
+class VanillaBlockDisplayAnimator extends BoneAnimator {
+ private _name: string
+
+ public uuid: string
+ public element: VanillaBlockDisplay | undefined
+
+ constructor(uuid: string, animation: _Animation, name: string) {
+ super(uuid, animation, name)
+ this.uuid = uuid
+ this._name = name
+ }
+
+ getElement() {
+ this.element = OutlinerNode.uuids[this.uuid] as VanillaBlockDisplay
+ return this.element
+ }
+
+ select() {
+ this.getElement()
+ if (!this.element) {
+ unselectAllElements()
+ return this
+ }
+
+ if (this.element.locked) {
+ return this
+ }
+
+ if (!this.element.selected && this.element) {
+ this.element.select()
+ }
+ GeneralAnimator.prototype.select.call(this)
+
+ if (
+ this[Toolbox.selected.animation_channel] &&
+ ((Timeline.selected && Timeline.selected.length === 0) ||
+ (Timeline.selected && (Timeline.selected[0].animator as any)) !== this)
+ ) {
+ let nearest: _Keyframe | undefined
+ this[Toolbox.selected.animation_channel].forEach((kf: _Keyframe) => {
+ if (Math.abs(kf.time - Timeline.time) < 0.002) {
+ nearest = kf
+ }
+ })
+ if (nearest) {
+ nearest.select()
+ }
+ }
+
+ if (this.element && this.element.parent && this.element.parent !== 'root') {
+ this.element.parent.openUp()
+ }
+
+ return this
+ }
+
+ doRender() {
+ this.getElement()
+ return !!(this.element && this.element.mesh)
+ }
+
+ displayRotation(arr: ArrayVector3 | ArrayVector4, multiplier = 1) {
+ const bone = this.getElement().mesh
+
+ if (bone.fix_rotation) {
+ bone.rotation.copy(bone.fix_rotation as THREE.Euler)
+ }
+
+ if (arr) {
+ if (arr.length === 4) {
+ const added_rotation = new THREE.Euler().setFromQuaternion(
+ new THREE.Quaternion().fromArray(arr),
+ 'ZYX'
+ )
+ bone.rotation.x -= added_rotation.x * multiplier
+ bone.rotation.y -= added_rotation.y * multiplier
+ bone.rotation.z += added_rotation.z * multiplier
+ } else {
+ bone.rotation.x -= Math.degToRad(arr[0]) * multiplier
+ bone.rotation.y -= Math.degToRad(arr[1]) * multiplier
+ bone.rotation.z += Math.degToRad(arr[2]) * multiplier
+ }
+ }
+ if (this.rotation_global) {
+ const quat = bone.parent?.getWorldQuaternion(Reusable.quat1)
+ if (!quat) return this
+ quat.invert()
+ bone.quaternion.premultiply(quat)
+ }
+ return this
+ }
+
+ displayPosition(arr: ArrayVector3, multiplier = 1) {
+ const bone = this.getElement().mesh
+ if (bone.fix_position) {
+ bone.position.copy(bone.fix_position as THREE.Vector3)
+ }
+ if (arr) {
+ bone.position.x -= arr[0] * multiplier
+ bone.position.y += arr[1] * multiplier
+ bone.position.z += arr[2] * multiplier
+ }
+ return this
+ }
+
+ displayScale(arr: ArrayVector3, multiplier = 1) {
+ if (!arr) return this
+ const bone = this.getElement().mesh
+ if (bone.fix_scale) {
+ bone.scale.copy(bone.fix_scale)
+ }
+
+ bone.scale.x *= 1 + (arr[0] - 1) * multiplier || 0.00001
+ bone.scale.y *= 1 + (arr[1] - 1) * multiplier || 0.00001
+ bone.scale.z *= 1 + (arr[2] - 1) * multiplier || 0.00001
+ return this
+ }
+}
+VanillaBlockDisplayAnimator.prototype.type = VanillaBlockDisplay.type
+VanillaBlockDisplay.animator = VanillaBlockDisplayAnimator as any
+
+createBlockbenchMod(
+ `${PACKAGE.name}:vanillaBlockDisplay`,
+ {
+ subscriptions: [] as Array<() => void>,
+ },
+ context => {
+ Interface.Panels.outliner.menu.addAction(CREATE_ACTION, 3)
+ Toolbars.outliner.add(CREATE_ACTION, 0)
+ MenuBar.menus.edit.addAction(CREATE_ACTION, 8)
+
+ context.subscriptions.push(
+ events.SELECT_PROJECT.subscribe(project => {
+ project.vanillaBlockDisplays ??= []
+ VanillaBlockDisplay.all.empty()
+ VanillaBlockDisplay.all.push(...project.vanillaBlockDisplays)
+ }),
+ events.UNSELECT_PROJECT.subscribe(project => {
+ project.vanillaBlockDisplays = [...VanillaBlockDisplay.all]
+ VanillaBlockDisplay.all.empty()
+ })
+ )
+ return context
+ },
+ context => {
+ Interface.Panels.outliner.menu.removeAction(CREATE_ACTION.id)
+ Toolbars.outliner.remove(CREATE_ACTION)
+ MenuBar.menus.edit.removeAction(CREATE_ACTION.id)
+
+ context.subscriptions.forEach(unsub => unsub())
+ }
+)
+
+export const CREATE_ACTION = createAction(`${PACKAGE.name}:create_vanilla_block_display`, {
+ name: translate('action.create_vanilla_block_display.title'),
+ icon: 'deployed_code',
+ category: 'animated_java',
+ condition() {
+ return isCurrentFormat() && Mode.selected.id === Modes.options.edit.id
+ },
+ click() {
+ Undo.initEdit({ outliner: true, elements: [], selection: true })
+
+ const vanillaBlockDisplay = new VanillaBlockDisplay({}).init()
+ const group = getCurrentGroup()
+
+ if (group instanceof Group) {
+ vanillaBlockDisplay.addTo(group)
+ vanillaBlockDisplay.extend({ position: group.origin.slice() as ArrayVector3 })
+ }
+
+ selected.forEachReverse(el => el.unselect())
+ Group.selected && Group.selected.unselect()
+ vanillaBlockDisplay.select()
+
+ Undo.finishEdit('Create Vanilla Block Display', {
+ outliner: true,
+ elements: selected,
+ selection: true,
+ })
+
+ return vanillaBlockDisplay
+ },
+})
+
+export function debugBlocks() {
+ const maxX = Math.floor(Math.sqrt(MINECRAFT_REGISTRY.block.items.length))
+ for (let i = 0; i < MINECRAFT_REGISTRY.block.items.length; i++) {
+ const block = MINECRAFT_REGISTRY.block.items[i]
+ const x = (i % maxX) * 32
+ const y = Math.floor(i / maxX) * 32
+ new VanillaBlockDisplay({ name: block, block, position: [x, 8, y] }).init()
+ }
+}
+
+export async function debugBlockState(block: string) {
+ const blockState = await getBlockState(block)
+ if (!blockState) return
+
+ const permutations = computeAllStatePermutations(blockState.stateValues)
+
+ const maxX = Math.floor(Math.sqrt(permutations.length))
+ for (let i = 0; i < permutations.length; i++) {
+ const x = (i % maxX) * 32
+ const y = Math.floor(i / maxX) * 32
+ const str = generateBlockStateString(permutations[i])
+ new VanillaBlockDisplay({
+ name: block + str,
+ block: block + str,
+ position: [x, 8, y],
+ }).init()
+ }
+}
+
+function generateBlockStateString(state: Record) {
+ const str = Object.entries(state).map(([k, v]) => `${k}=${v.toString()}`)
+ return `[${str.join(',')}]`
+}
+
+// FetchBot is the GOAT 🐐
+function computeAllStatePermutations(state: Record) {
+ const maxPermutation = Object.values(state).reduce((acc, cur) => acc * cur.length, 1)
+ const permutations: Array> = []
+ for (let i = 0; i < maxPermutation; i++) {
+ const permutation: Record = {}
+ let i2 = i
+ Object.entries(state).forEach(([key, value]) => {
+ const index = i2 % value.length
+ permutation[key] = String(value[index])
+ i2 = Math.floor(i2 / value.length)
+ })
+ permutations.push(permutation)
+ }
+ return permutations
+}
diff --git a/src/outliner/vanillaItemDisplay.ts b/src/outliner/vanillaItemDisplay.ts
new file mode 100644
index 00000000..75834945
--- /dev/null
+++ b/src/outliner/vanillaItemDisplay.ts
@@ -0,0 +1,443 @@
+import { IBlueprintBoneConfigJSON, isCurrentFormat } from '../blueprintFormat'
+import { PACKAGE } from '../constants'
+import { VANILLA_ITEM_DISPLAY_CONFIG_ACTION } from '../interface/vanillaItemDisplayConfigDialog'
+import { BoneConfig } from '../nodeConfigs'
+import { getItemModel } from '../systems/minecraft/itemModelManager'
+import { MINECRAFT_REGISTRY } from '../systems/minecraft/registryManager'
+import { getCurrentVersion } from '../systems/minecraft/versionManager'
+import { events } from '../util/events'
+import { toSafeFuntionName } from '../util/minecraftUtil'
+import { createAction, createBlockbenchMod } from '../util/moddingTools'
+import { Valuable } from '../util/stores'
+import { translate } from '../util/translation'
+import { ResizableOutlinerElement } from './resizableOutlinerElement'
+import { TextDisplay } from './textDisplay'
+import { VanillaBlockDisplay } from './vanillaBlockDisplay'
+
+interface VanillaItemDisplayOptions {
+ name?: string
+ item?: string
+ position?: ArrayVector3
+ rotation?: ArrayVector3
+ scale?: ArrayVector3
+ visibility?: boolean
+}
+
+export class VanillaItemDisplay extends ResizableOutlinerElement {
+ static type = `${PACKAGE.name}:vanilla_item_display`
+ static selected: VanillaItemDisplay[] = []
+ static all: VanillaItemDisplay[] = []
+
+ public type = VanillaItemDisplay.type
+ public icon = 'icecream'
+ public needsUniqueName = true
+
+ // Properties
+ public _item = new Valuable('minecraft:diamond')
+ public config: IBlueprintBoneConfigJSON
+
+ public error = new Valuable('')
+
+ public menu = new Menu([
+ ...Outliner.control_menu_group,
+ VANILLA_ITEM_DISPLAY_CONFIG_ACTION,
+ '_',
+ 'rename',
+ 'delete',
+ ])
+ public buttons = [Outliner.buttons.export, Outliner.buttons.locked, Outliner.buttons.visibility]
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ public preview_controller = PREVIEW_CONTROLLER
+
+ constructor(data: VanillaItemDisplayOptions, uuid = guid()) {
+ super(data, uuid)
+ VanillaItemDisplay.all.push(this)
+
+ for (const key in VanillaItemDisplay.properties) {
+ VanillaItemDisplay.properties[key].reset(this)
+ }
+
+ this.name = 'vanilla_item_display'
+ this.extend(data)
+
+ this.item ??= 'minecraft:diamond'
+ this.position ??= [0, 0, 0]
+ this.rotation ??= [0, 0, 0]
+ this.scale ??= [1, 1, 1]
+ this.visibility ??= true
+ this.config ??= {}
+
+ this.sanitizeName()
+
+ const updateItem = (newItem: string) => {
+ if (!MINECRAFT_REGISTRY.item) {
+ requestAnimationFrame(() => updateItem(newItem))
+ return
+ }
+ let [namespace, id] = newItem.split(':')
+ if (!id) {
+ id = namespace
+ namespace = 'minecraft'
+ }
+ if (
+ (namespace === 'minecraft' || namespace === '') &&
+ MINECRAFT_REGISTRY.item.has(id)
+ ) {
+ this.error.set('')
+ this.preview_controller.updateGeometry(this)
+ } else {
+ this.error.set(`This item does not exist in Minecraft ${getCurrentVersion()!.id}.`)
+ }
+ }
+
+ this._item.subscribe(value => {
+ updateItem(value)
+ })
+ }
+
+ get item() {
+ if (this._item === undefined) return 'minecraft:diamond'
+ return this._item.get()
+ }
+ set item(value: string) {
+ if (this._item === undefined) return
+ this._item.set(value)
+ }
+
+ public sanitizeName(): string {
+ this.name = toSafeFuntionName(this.name)
+ const otherNodes = [
+ ...VanillaItemDisplay.all.filter(v => v.uuid !== this.uuid),
+ ...Group.all,
+ ...TextDisplay.all,
+ ...VanillaBlockDisplay.all,
+ ]
+ const otherNames = new Set(otherNodes.map(v => v.name))
+
+ if (!otherNames.has(this.name)) {
+ return this.name
+ }
+
+ let i = 1
+ const match = this.name.match(/\d+$/)
+ if (match) {
+ i = parseInt(match[0])
+ this.name = this.name.slice(0, -match[0].length)
+ }
+
+ let maxTries = 10000
+ while (maxTries-- > 0) {
+ const newName = `${this.name}${i}`
+ if (!otherNames.has(newName)) {
+ this.name = newName
+ return newName
+ }
+ i++
+ }
+
+ throw new Error('Could not make VanillaItemDisplay name unique!')
+ }
+
+ getUndoCopy() {
+ const copy = {} as VanillaItemDisplayOptions & { uuid: string; type: string }
+
+ for (const key in VanillaItemDisplay.properties) {
+ VanillaItemDisplay.properties[key].copy(this, copy)
+ }
+
+ copy.uuid = this.uuid
+ copy.type = this.type
+ return copy
+ }
+
+ getSaveCopy() {
+ const el: any = {}
+ for (const key in VanillaItemDisplay.properties) {
+ VanillaItemDisplay.properties[key].copy(this, el)
+ }
+ el.uuid = this.uuid
+ el.type = this.type
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+ return el
+ }
+
+ select() {
+ if (Group.selected) {
+ Group.selected.unselect()
+ }
+ if (!Pressing.ctrl && !Pressing.shift) {
+ if (Cube.selected.length) {
+ Cube.selected.forEachReverse(el => el.unselect())
+ }
+ if (selected.length) {
+ selected.forEachReverse(el => el !== this && el.unselect())
+ }
+ }
+
+ VanillaItemDisplay.selected.safePush(this)
+ this.selectLow()
+ this.showInOutliner()
+ updateSelection()
+ if (Animator.open && Blockbench.Animation.selected) {
+ Blockbench.Animation.selected.getBoneAnimator(this).select()
+ }
+ return this
+ }
+
+ unselect() {
+ if (!this.selected) return
+ if (
+ Animator.open &&
+ Timeline.selected_animator &&
+ Timeline.selected_animator.element === this &&
+ Timeline.selected
+ ) {
+ Timeline.selected.empty()
+ }
+ Project!.selected_elements.remove(this)
+ VanillaItemDisplay.selected.remove(this)
+ this.selected = false
+ TickUpdates.selection = true
+ this.preview_controller.updateHighlight(this)
+ }
+}
+new Property(VanillaItemDisplay, 'string', 'item', { default: 'minecraft:diamond' })
+new Property(VanillaItemDisplay, 'object', 'config', {
+ get default() {
+ return new BoneConfig().toJSON()
+ },
+})
+OutlinerElement.registerType(VanillaItemDisplay, VanillaItemDisplay.type)
+
+export const PREVIEW_CONTROLLER = new NodePreviewController(VanillaItemDisplay, {
+ setup(el: VanillaItemDisplay) {
+ ResizableOutlinerElement.prototype.preview_controller.setup(el)
+ },
+ updateGeometry(el: VanillaItemDisplay) {
+ if (!el.mesh) return
+
+ void getItemModel(el.item)
+ .then(result => {
+ if (!result) return
+
+ el.mesh.clear()
+ el.mesh.add(result.mesh)
+ el.mesh.add(result.outline)
+ el.mesh.outline = result.outline
+
+ el.preview_controller.updateHighlight(el)
+ el.preview_controller.updateTransform(el)
+ el.mesh.visible = el.visibility
+ TickUpdates.selection = true
+ })
+ .catch(err => {
+ if (typeof err.message === 'string') {
+ el.error.set(err.message as string)
+ }
+ })
+ },
+ updateTransform(el: VanillaItemDisplay) {
+ ResizableOutlinerElement.prototype.preview_controller.updateTransform(el)
+ },
+ updateHighlight(el: VanillaItemDisplay, force?: boolean | VanillaItemDisplay) {
+ if (!isCurrentFormat() || !el?.mesh) return
+ const highlighted = Modes.edit && (force === true || force === el || el.selected) ? 1 : 0
+
+ const itemModel = el.mesh.children.at(0) as THREE.Mesh
+ if (!itemModel) return
+ for (const child of itemModel.children) {
+ if (!(child instanceof THREE.Mesh)) continue
+ const highlight = child.geometry.attributes.highlight
+
+ if (highlight.array[0] != highlighted) {
+ // @ts-ignore
+ highlight.array.set(Array(highlight.count).fill(highlighted))
+ highlight.needsUpdate = true
+ }
+ }
+ },
+})
+
+class VanillaItemDisplayAnimator extends BoneAnimator {
+ private _name: string
+
+ public uuid: string
+ public element: VanillaItemDisplay | undefined
+
+ constructor(uuid: string, animation: _Animation, name: string) {
+ super(uuid, animation, name)
+ this.uuid = uuid
+ this._name = name
+ }
+
+ getElement() {
+ this.element = OutlinerNode.uuids[this.uuid] as VanillaItemDisplay
+ return this.element
+ }
+
+ select() {
+ this.getElement()
+ if (!this.element) {
+ unselectAllElements()
+ return this
+ }
+
+ if (this.element.locked) {
+ return this
+ }
+
+ if (!this.element.selected && this.element) {
+ this.element.select()
+ }
+ GeneralAnimator.prototype.select.call(this)
+
+ if (
+ this[Toolbox.selected.animation_channel] &&
+ ((Timeline.selected && Timeline.selected.length === 0) ||
+ (Timeline.selected && (Timeline.selected[0].animator as any)) !== this)
+ ) {
+ let nearest: _Keyframe | undefined
+ this[Toolbox.selected.animation_channel].forEach((kf: _Keyframe) => {
+ if (Math.abs(kf.time - Timeline.time) < 0.002) {
+ nearest = kf
+ }
+ })
+ if (nearest) {
+ nearest.select()
+ }
+ }
+
+ if (this.element && this.element.parent && this.element.parent !== 'root') {
+ this.element.parent.openUp()
+ }
+
+ return this
+ }
+
+ doRender() {
+ this.getElement()
+ return !!(this.element && this.element.mesh)
+ }
+
+ displayRotation(arr: ArrayVector3 | ArrayVector4, multiplier = 1) {
+ const bone = this.getElement().mesh
+
+ if (bone.fix_rotation) {
+ bone.rotation.copy(bone.fix_rotation as THREE.Euler)
+ }
+
+ if (arr) {
+ if (arr.length === 4) {
+ const added_rotation = new THREE.Euler().setFromQuaternion(
+ new THREE.Quaternion().fromArray(arr),
+ 'ZYX'
+ )
+ bone.rotation.x -= added_rotation.x * multiplier
+ bone.rotation.y -= added_rotation.y * multiplier
+ bone.rotation.z += added_rotation.z * multiplier
+ } else {
+ bone.rotation.x += Math.degToRad(-arr[0]) * multiplier
+ bone.rotation.y += Math.degToRad(-arr[1]) * multiplier
+ bone.rotation.z += Math.degToRad(arr[2]) * multiplier
+ }
+ }
+ if (this.rotation_global) {
+ const quat = bone.parent?.getWorldQuaternion(Reusable.quat1)
+ if (!quat) return this
+ quat.invert()
+ bone.quaternion.premultiply(quat)
+ }
+ return this
+ }
+
+ displayPosition(arr: ArrayVector3, multiplier = 1) {
+ const bone = this.getElement().mesh
+ if (bone.fix_position) {
+ bone.position.copy(bone.fix_position as THREE.Vector3)
+ }
+ if (arr) {
+ bone.position.x -= arr[0] * multiplier
+ bone.position.y += arr[1] * multiplier
+ bone.position.z += arr[2] * multiplier
+ }
+ return this
+ }
+
+ displayScale(arr: ArrayVector3, multiplier = 1) {
+ if (!arr) return this
+ const bone = this.getElement().mesh
+ if (bone.fix_scale) {
+ bone.scale.copy(bone.fix_scale)
+ }
+ bone.scale.x = 1 + (arr[0] - 1) * multiplier || 0.00001
+ bone.scale.y = 1 + (arr[1] - 1) * multiplier || 0.00001
+ bone.scale.z = 1 + (arr[2] - 1) * multiplier || 0.00001
+ return this
+ }
+}
+VanillaItemDisplayAnimator.prototype.type = VanillaItemDisplay.type
+VanillaItemDisplay.animator = VanillaItemDisplayAnimator as any
+
+createBlockbenchMod(
+ `${PACKAGE.name}:vanillaItemDisplay`,
+ {
+ subscriptions: [] as Array<() => void>,
+ },
+ context => {
+ Interface.Panels.outliner.menu.addAction(CREATE_ACTION, 3)
+ Toolbars.outliner.add(CREATE_ACTION, 0)
+ MenuBar.menus.edit.addAction(CREATE_ACTION, 8)
+
+ context.subscriptions.push(
+ events.SELECT_PROJECT.subscribe(project => {
+ project.vanillaItemDisplays ??= []
+ VanillaItemDisplay.all.empty()
+ VanillaItemDisplay.all.push(...project.vanillaItemDisplays)
+ }),
+ events.UNSELECT_PROJECT.subscribe(project => {
+ project.vanillaItemDisplays = [...VanillaItemDisplay.all]
+ VanillaItemDisplay.all.empty()
+ })
+ )
+ return context
+ },
+ context => {
+ Interface.Panels.outliner.menu.removeAction(CREATE_ACTION.id)
+ Toolbars.outliner.remove(CREATE_ACTION)
+ MenuBar.menus.edit.removeAction(CREATE_ACTION.id)
+
+ context.subscriptions.forEach(unsub => unsub())
+ }
+)
+
+export const CREATE_ACTION = createAction(`${PACKAGE.name}:create_vanilla_item_display`, {
+ name: translate('action.create_vanilla_item_display.title'),
+ icon: 'icecream',
+ category: 'animated_java',
+ condition() {
+ return isCurrentFormat() && Mode.selected.id === Modes.options.edit.id
+ },
+ click() {
+ Undo.initEdit({ outliner: true, elements: [], selection: true })
+
+ const vanillaItemDisplay = new VanillaItemDisplay({}).init()
+ const group = getCurrentGroup()
+
+ if (group instanceof Group) {
+ vanillaItemDisplay.addTo(group)
+ vanillaItemDisplay.extend({ position: group.origin.slice() as ArrayVector3 })
+ }
+
+ selected.forEachReverse(el => el.unselect())
+ Group.selected && Group.selected.unselect()
+ vanillaItemDisplay.select()
+
+ Undo.finishEdit('Create Vanilla Item Display', {
+ outliner: true,
+ elements: selected,
+ selection: true,
+ })
+
+ return vanillaItemDisplay
+ },
+})
diff --git a/src/pluginPackage/about.svelte b/src/pluginPackage/about.svelte
new file mode 100644
index 00000000..453bbcba
--- /dev/null
+++ b/src/pluginPackage/about.svelte
@@ -0,0 +1,130 @@
+
+ Do you wish your Vanilla Minecraft creations could move and animate like the ones in
+ mods?
+ Are you tired of Minecraft's
+
+ {#each 'silly model limitations?'.split('') as letter, i}
+ {@html letter === ' ' ? ' ' : letter}
+ {/each}
+
+ Animated Java is here to help!
+
+
+
+
Welcome to Animated Java!
+
+
+
+
+ Animated Java is a plugin for Blockbench that allows you to create custom animations and models
+ for Vanilla Minecraft. It uses the power of Java Edition's Data Pack and Resource Pack systems
+ to bring your creations to life!
+
+
+Features
+
+
+ Variants: Swap between different textures in-game.
+
+ Highly optimized: Hours of performance tests and optimization tweaks have gone into Animated
+ Java's Exported Data Pack to make sure it has as little performance impact as possible.
+
+ Limited Molang support. If Blockbench can render it, Animated Java can animate it.
+ Text Display previewing and animation support.
+ Resource Pack-less exporting. Animate Vanilla block and item models!
+ Locators: Run commands relative to a locators position via keyframes.
+
+ Camera Plugin Support: Install the Official Camera Plugin to create cinematic camera paths with ease!
+
+ Animation Tweening: Create smooth transitions between animations.
+ Many different configuration options.
+
+ Complete Documentation at animated-java.dev/documentation/
+
+ And much more!
+
+
+Getting started
+
+
+ To learn the basics of Animated Java, check out our Getting Started guide.
+
+
+
+ Note that it is highly recommended to have a basic understanding of Java Edition's Data
+ Pack and Resource Pack systems before using Animated Java!
+
+
+Need Help?
+
+
+ Check our Documentation to find answers to common
+ questions or make a post in the
+ #animated-java-support channel on our
+ Discord server to get help from the community and
+ developers.
+
+
+Found a bug?
+
+
+ If you find a bug, please report it on our GitHub . It only takes a few moments of your time, and improves the quality of the project for
+ everyone!
+
+
+
diff --git a/src/pluginPackage/icon.svg b/src/pluginPackage/icon.svg
new file mode 100644
index 00000000..7e3ff7ea
--- /dev/null
+++ b/src/pluginPackage/icon.svg
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/pluginPackage/members.yml b/src/pluginPackage/members.yml
new file mode 100644
index 00000000..aa47fe81
--- /dev/null
+++ b/src/pluginPackage/members.yml
@@ -0,0 +1,2 @@
+maintainers:
+ - SnaveSutit
diff --git a/src/projectSettings.ts b/src/projectSettings.ts
deleted file mode 100644
index 0c22c1bd..00000000
--- a/src/projectSettings.ts
+++ /dev/null
@@ -1,482 +0,0 @@
-import { AnimatedJavaExporter } from './exporter'
-import { GUIStructure } from './guiStructure'
-import { Items } from './minecraft'
-import {
- isValidResourcePackMcMeta,
- isValidResourcePackPath,
- safeFunctionName,
-} from './minecraft/util'
-import { ajModelFormat } from './modelFormat'
-import * as Settings from './settings'
-import { translate } from './util/translation'
-import * as events from './events'
-
-export interface IAnimatedJavaProjectSettings {
- project_namespace: Settings.InlineTextSetting
- project_resolution: Settings.DoubleNumberSetting
- target_minecraft_version: Settings.DropdownSetting
- rig_item: Settings.InlineTextSetting
- rig_item_model: Settings.InlineTextSetting
- rig_export_folder: Settings.FolderSetting
- texture_export_folder: Settings.FolderSetting
- enable_advanced_resource_pack_settings: Settings.CheckboxSetting
- resource_pack_mcmeta: Settings.FileSetting
- verbose: Settings.CheckboxSetting
- exporter: Settings.DropdownSetting
-}
-
-const TRANSLATIONS = {
- project_namespace: {
- displayName: translate('animated_java.project_settings.project_namespace'),
- description: translate(
- 'animated_java.project_settings.project_namespace.description'
- ).split('\n'),
- error: {
- unset: translate('animated_java.project_settings.project_namespace.error.unset'),
- },
- },
- project_resolution: {
- displayName: translate('animated_java.project_settings.project_resolution'),
- description: translate(
- 'animated_java.project_settings.project_resolution.description'
- ).split('\n'),
- },
- target_minecraft_version: {
- displayName: translate('animated_java.project_settings.target_minecraft_version'),
- description: translate(
- 'animated_java.project_settings.target_minecraft_version.description'
- ).split('\n'),
- },
- rig_item: {
- displayName: translate('animated_java.project_settings.rig_item'),
- description: translate('animated_java.project_settings.rig_item.description').split('\n'),
- error: {
- unset: translate('animated_java.project_settings.rig_item.error.unset'),
- space: translate('animated_java.project_settings.rig_item.error.space'),
- invalid_item: translate('animated_java.project_settings.rig_item.error.invalid_item'),
- invalid_namespace: translate(
- 'animated_java.project_settings.rig_item.error.invalid_namespace'
- ),
- },
- warning: {
- unknown_item: translate('animated_java.project_settings.rig_item.warning.unknown_item'),
- },
- },
- rig_item_model: {
- displayName: translate('animated_java.project_settings.rig_item_model'),
- description: translate('animated_java.project_settings.rig_item_model.description').split(
- '\n'
- ),
- error: {
- unset: translate('animated_java.project_settings.rig_item_model.error.unset'),
- invalid_path: translate(
- 'animated_java.project_settings.rig_item_model.error.invalid_path'
- ),
- item_does_not_match: translate(
- 'animated_java.project_settings.rig_item_model.error.item_does_not_match'
- ),
- rig_item_unset: translate(
- 'animated_java.project_settings.rig_item_model.error.rig_item_unset'
- ),
- },
- },
- rig_export_folder: {
- displayName: translate('animated_java.project_settings.rig_export_folder'),
- description: translate(
- 'animated_java.project_settings.rig_export_folder.description'
- ).split('\n'),
- error: {
- unset: translate('animated_java.project_settings.rig_export_folder.error.unset'),
- invalid_path: translate(
- 'animated_java.project_settings.rig_export_folder.error.invalid_path'
- ),
- },
- },
- texture_export_folder: {
- displayName: translate('animated_java.project_settings.texture_export_folder'),
- description: translate(
- 'animated_java.project_settings.texture_export_folder.description'
- ).split('\n'),
- error: {
- unset: translate('animated_java.project_settings.texture_export_folder.error.unset'),
- invalid_path: translate(
- 'animated_java.project_settings.texture_export_folder.error.invalid_path'
- ),
- },
- },
- enable_advanced_resource_pack_settings: {
- displayName: translate(
- 'animated_java.project_settings.enable_advanced_resource_pack_settings'
- ),
- description: translate(
- 'animated_java.project_settings.enable_advanced_resource_pack_settings.description'
- ).split('\n'),
- },
- resource_pack_mcmeta: {
- displayName: translate('animated_java.project_settings.resource_pack_mcmeta'),
- description: translate(
- 'animated_java.project_settings.resource_pack_mcmeta.description'
- ).split('\n'),
- error: {
- unset: translate('animated_java.project_settings.resource_pack_mcmeta.error.unset'),
- invalid_path: translate(
- 'animated_java.project_settings.resource_pack_mcmeta.error.invalid_path'
- ),
- },
- },
- verbose: {
- displayName: translate('animated_java.project_settings.verbose'),
- description: translate('animated_java.project_settings.verbose.description').split('\n'),
- },
- exporter: {
- displayName: translate('animated_java.project_settings.exporter'),
- description: translate('animated_java.project_settings.exporter.description').split('\n'),
- },
-}
-
-export function getDefaultProjectSettings(): IAnimatedJavaProjectSettings {
- return {
- project_namespace: new Settings.InlineTextSetting(
- {
- id: 'animated_java:project_settings/project_namespace',
- displayName: TRANSLATIONS.project_namespace.displayName,
- description: TRANSLATIONS.project_namespace.description,
- defaultValue: '',
- docsLink: '/docs/project_settings#project_namespace',
- get subtext() {
- return Project?.animated_java_uuid
- },
- },
- function onUpdate(setting) {
- if (setting.value === '')
- setting.infoPopup = Settings.createInfo(
- 'error',
- TRANSLATIONS.project_namespace.error.unset
- )
- setting.value = safeFunctionName(setting.value)
- return setting
- }
- ),
-
- project_resolution: new Settings.DoubleNumberSetting(
- {
- id: 'animated_java:project_settings/project_resolution',
- displayName: TRANSLATIONS.project_resolution.displayName,
- description: TRANSLATIONS.project_resolution.description,
- get defaultValue(): [number, number] {
- return [Project!.texture_width, Project!.texture_height]
- },
- min: 1,
- secondNumberLabel: 'x',
- docsLink: '/docs/project_settings#project_resolution',
- },
- undefined,
- undefined,
- function onConfirm(setting) {
- setProjectResolution(setting.numberA, setting.numberB, true)
- }
- ),
-
- target_minecraft_version: new Settings.DropdownSetting({
- id: 'animated_java:project_settings/target_minecraft_version',
- displayName: TRANSLATIONS.target_minecraft_version.displayName,
- description: TRANSLATIONS.target_minecraft_version.description,
- defaultValue: 0,
- options: [
- {
- name: '1.20+',
- value: '1.20+',
- },
- {
- name: '1.19.4',
- value: '1.19.4',
- },
- ],
- docsLink: '/docs/project_settings#target_minecraft_version',
- }),
-
- rig_item: new Settings.InlineTextSetting(
- {
- id: 'animated_java:project_settings/rig_item',
- displayName: TRANSLATIONS.rig_item.displayName,
- description: TRANSLATIONS.rig_item.description,
- defaultValue: 'minecraft:white_dye',
- // resettable: true,
- docsLink: '/docs/project_settings#rig_item',
- },
- function onUpdate(setting) {
- setting.value = setting.value.toLowerCase()
-
- if (setting.value === '') {
- setting.infoPopup = Settings.createInfo(
- 'error',
- TRANSLATIONS.rig_item.error.unset
- )
- return
- } else if (setting.value.includes(' ')) {
- setting.infoPopup = Settings.createInfo(
- 'error',
- TRANSLATIONS.rig_item.error.space
- )
- return
- }
-
- const [namespace, path] = setting.value.split(':')
- if (!(namespace && path)) {
- setting.infoPopup = Settings.createInfo(
- 'error',
- TRANSLATIONS.rig_item.error.invalid_namespace
- )
- return
- }
-
- if (!Items.isItem(setting.value)) {
- setting.infoPopup = Settings.createInfo(
- 'warning',
- TRANSLATIONS.rig_item.warning.unknown_item
- )
- return
- }
-
- return
- }
- ),
-
- rig_item_model: new Settings.FileSetting(
- {
- id: 'animated_java:project_settings/rig_item_model',
- displayName: TRANSLATIONS.rig_item_model.displayName,
- description: TRANSLATIONS.rig_item_model.description,
- defaultValue: '',
- // resettable: true,
- docsLink: '/docs/project_settings#rig_item_model',
- dependsOn: ['animated_java:project_settings/rig_item'],
- },
- function onUpdate(setting) {
- if (!setting.value) {
- setting.infoPopup = Settings.createInfo(
- 'error',
- TRANSLATIONS.rig_item_model.error.unset
- )
- return setting
- } else if (!isValidResourcePackPath(setting.value)) {
- setting.infoPopup = Settings.createInfo(
- 'error',
- TRANSLATIONS.rig_item_model.error.invalid_path
- )
- return setting
- }
- const parsed = PathModule.parse(setting.value)
- const rigItem = Project?.animated_java_settings?.rig_item?.value
- if (!rigItem) {
- setting.infoPopup = Settings.createInfo(
- 'error',
- TRANSLATIONS.rig_item_model.error.rig_item_unset
- )
- return setting
- }
- const [, itemId] = rigItem.split(':')
- if (parsed.name !== itemId) {
- setting.infoPopup = Settings.createInfo(
- 'error',
- TRANSLATIONS.rig_item_model.error.item_does_not_match,
- { rigItem: itemId, pathItem: parsed.name }
- )
- return setting
- }
- }
- ),
-
- rig_export_folder: new Settings.FolderSetting(
- {
- id: 'animated_java:project_settings/rig_export_folder',
- displayName: TRANSLATIONS.rig_export_folder.displayName,
- description: TRANSLATIONS.rig_export_folder.description,
- defaultValue: '',
- // resettable: true,
- docsLink: '/docs/project_settings#rig_export_folder',
- },
- function onUpdate(setting) {
- if (!setting.value) {
- setting.infoPopup = Settings.createInfo(
- 'error',
- TRANSLATIONS.rig_export_folder.error.unset
- )
- return setting
- } else if (!isValidResourcePackPath(setting.value)) {
- setting.infoPopup = Settings.createInfo(
- 'error',
- TRANSLATIONS.rig_export_folder.error.invalid_path
- )
- return setting
- }
- }
- ),
-
- texture_export_folder: new Settings.FolderSetting(
- {
- id: 'animated_java:project_settings/texture_export_folder',
- displayName: TRANSLATIONS.texture_export_folder.displayName,
- description: TRANSLATIONS.texture_export_folder.description,
- defaultValue: '',
- // resettable: true,
- docsLink: '/docs/project_settings#texture_export_folder',
- },
- function onUpdate(setting) {
- if (!setting.value) {
- setting.infoPopup = Settings.createInfo(
- 'error',
- TRANSLATIONS.texture_export_folder.error.unset
- )
- return setting
- } else if (!isValidResourcePackPath(setting.value)) {
- setting.infoPopup = Settings.createInfo(
- 'error',
- TRANSLATIONS.texture_export_folder.error.invalid_path
- )
- return setting
- }
- }
- ),
-
- enable_advanced_resource_pack_settings: new Settings.CheckboxSetting({
- id: 'animated_java:project_settings/enable_advanced_resource_pack_settings',
- displayName: TRANSLATIONS.enable_advanced_resource_pack_settings.displayName,
- description: TRANSLATIONS.enable_advanced_resource_pack_settings.description,
- defaultValue: false,
- docsLink: '/docs/project_settings#enable_advanced_resource_pack_settings',
- }),
-
- resource_pack_mcmeta: new Settings.FileSetting(
- {
- id: 'animated_java:project_settings/resource_pack_mcmeta',
- displayName: TRANSLATIONS.resource_pack_mcmeta.displayName,
- description: TRANSLATIONS.resource_pack_mcmeta.description,
- defaultValue: '',
- // resettable: true,
- docsLink: '/docs/project_settings#resource_pack',
- },
- function onUpdate(setting) {
- if (!setting.value) {
- setting.infoPopup = Settings.createInfo(
- 'error',
- TRANSLATIONS.resource_pack_mcmeta.error.unset
- )
- return setting
- } else if (!isValidResourcePackMcMeta(setting.value)) {
- setting.infoPopup = Settings.createInfo(
- 'error',
- TRANSLATIONS.resource_pack_mcmeta.error.invalid_path
- )
- return setting
- }
- }
- ),
-
- verbose: new Settings.CheckboxSetting({
- id: 'animated_java:project_settings/verbose',
- displayName: TRANSLATIONS.verbose.displayName,
- description: TRANSLATIONS.verbose.description,
- defaultValue: true,
- docsLink: '/docs/project_settings#verbose',
- }),
-
- exporter: new Settings.DropdownSetting(
- {
- id: 'animated_java:project_settings/exporter',
- displayName: TRANSLATIONS.exporter.displayName,
- description: TRANSLATIONS.exporter.description,
- defaultValue: 0,
- docsLink: '/docs/project_settings#exporter',
- options: [],
- },
- undefined,
- function onInit(setting) {
- setting.options = AnimatedJavaExporter.all.map(exporter => ({
- name: exporter.name,
- value: exporter.id,
- }))
- }
- ),
- }
-}
-
-const _ = getDefaultProjectSettings()
-export const projectSettingStructure: GUIStructure = [
- {
- type: 'group',
- title: translate('animated_java.dialog.project_settings.project_group'),
- openByDefault: true,
- children: [
- {
- type: 'setting',
- settingId: _.project_namespace.id,
- },
- {
- type: 'setting',
- settingId: _.project_resolution.id,
- },
- {
- type: 'setting',
- settingId: _.target_minecraft_version.id,
- },
- ],
- },
- {
- type: 'group',
- title: translate('animated_java.project_settings.resourcepack_group'),
- openByDefault: true,
- children: [
- {
- type: 'setting',
- settingId: _.rig_item.id,
- },
- {
- type: 'toggle',
- title: _.enable_advanced_resource_pack_settings.displayName,
- settingId: _.enable_advanced_resource_pack_settings.id,
- active: [
- {
- type: 'setting',
- settingId: _.rig_item_model.id,
- },
- {
- type: 'setting',
- settingId: _.rig_export_folder.id,
- },
- {
- type: 'setting',
- settingId: _.texture_export_folder.id,
- },
- ],
- inactive: [
- {
- type: 'setting',
- settingId: _.resource_pack_mcmeta.id,
- },
- ],
- },
- ],
- },
- {
- type: 'setting',
- settingId: _.exporter.id,
- },
-]
-
-function updateProjectSettings() {
- if (!Project) return
- console.log('updateProjectSettings', Project)
- if (Format === ajModelFormat) {
- if (!Project.animated_java_settings) {
- Project.animated_java_settings = getDefaultProjectSettings()
- }
- for (const setting of Object.values(Project.animated_java_settings)) {
- setting._onInit()
- }
- }
-}
-
-events.LOAD_PROJECT.subscribe(updateProjectSettings)
-events.CONVERT_PROJECT.subscribe(updateProjectSettings)
-events.SELECT_PROJECT.subscribe(updateProjectSettings)
diff --git a/src/renderWorker/WorkerPool.ts b/src/renderWorker/WorkerPool.ts
deleted file mode 100644
index be5b3265..00000000
--- a/src/renderWorker/WorkerPool.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-interface PoolItem {
- available: boolean
- target: T | null
-}
-type TupleRest = ((...t: T) => any) extends (a: any, ...r: infer R) => any ? R : never
-export class Pool Promise> {
- private pool: PoolItem[]
- private tasks: Set> | null>
- constructor(size: number, private factory: () => WorkerType, private task: TaskFn) {
- this.pool = Array.from(Array(size), () => ({ available: true, target: null }))
- this.tasks = new Set()
- }
- async get(): Promise> {
- let item: PoolItem | undefined = this.pool.find(item => item.available)
- if (item) {
- return item
- }
- while (true) {
- await Promise.any(this.tasks.values())
- item = this.pool.find(item => item.available)
- if (item) {
- return item
- }
- }
- }
- mark(item: PoolItem, available: boolean) {
- item.available = available
- }
- async run(...args: TupleRest>): Promise> {
- let item = await this.get()
- this.mark(item, false)
- if (!item.target) {
- item.target = this.factory()
- }
- let resultPromise: Promise> | null = null
- try {
- resultPromise = this.task(item.target!, ...args)
- this.tasks.add(resultPromise)
- let returnValue = await resultPromise
- this.mark(item, true)
- this.tasks.delete(resultPromise)
- return returnValue
- } catch (e) {
- this.mark(item, true)
- item.target?.terminate()
- this.tasks.delete(resultPromise)
- item.target = null
- throw e
- }
- }
- terminate() {
- this.pool.forEach(item => {
- if (item.target) item.target.terminate()
- item.target = null
- })
- }
-}
diff --git a/src/renderWorker/renderer.worker.ts b/src/renderWorker/renderer.worker.ts
deleted file mode 100644
index 9accbf25..00000000
--- a/src/renderWorker/renderer.worker.ts
+++ /dev/null
@@ -1,136 +0,0 @@
-// import { AJBone } from '../rendering/bone'
-// import { Gimbals, Vector } from '../rendering/linear'
-// import molang from 'molangjs'
-// import * as DataTypes from './renderer.worker.types'
-
-// function interpolate(
-// channel: 'position' | 'rotation' | 'scale',
-// allow_expression: boolean,
-// axis: 'x' | 'y' | 'z',
-// time: number
-// ) {
-// var before = false
-// var after = false
-// var result = false
-// let epsilon = 1 / 1200
-
-// function mapAxes(cb) {
-// if (!Animator._last_values[channel]) Animator._last_values[channel] = [0, 0, 0]
-// if (axis) {
-// let result = cb(axis)
-// Animator._last_values[channel][axis] = result
-// return result
-// } else {
-// return ['x', 'y', 'z'].map(axis => {
-// let result = cb(axis)
-// Animator._last_values[channel][axis] = result
-// return result
-// })
-// }
-// }
-
-// for (var keyframe of this[channel]) {
-// if (keyframe.time < time) {
-// if (!before || keyframe.time > before.time) {
-// before = keyframe
-// }
-// } else {
-// if (!after || keyframe.time < after.time) {
-// after = keyframe
-// }
-// }
-// i++
-// }
-// if (before && Math.epsilon(before.time, time, epsilon)) {
-// result = before
-// } else if (after && Math.epsilon(after.time, time, epsilon)) {
-// result = after
-// } else if (before && before.interpolation == Keyframe.interpolation.step) {
-// result = before
-// } else if (before && !after) {
-// result = before
-// } else if (after && !before) {
-// result = after
-// } else if (!before && !after) {
-// //
-// } else {
-// let no_interpolations = Blockbench.hasFlag('no_interpolations')
-// let alpha = Math.getLerp(before.time, after.time, time)
-
-// if (
-// no_interpolations ||
-// (before.interpolation === Keyframe.interpolation.linear &&
-// after.interpolation === Keyframe.interpolation.linear)
-// ) {
-// if (no_interpolations) {
-// alpha = Math.round(alpha)
-// }
-// return mapAxes(axis => before.getLerp(after, axis, alpha, allow_expression))
-// } else if (
-// before.interpolation === Keyframe.interpolation.catmullrom ||
-// after.interpolation === Keyframe.interpolation.catmullrom
-// ) {
-// let sorted = this[channel].slice().sort((kf1, kf2) => kf1.time - kf2.time)
-// let before_index = sorted.indexOf(before)
-// let before_plus = sorted[before_index - 1]
-// let after_plus = sorted[before_index + 2]
-
-// return mapAxes(axis =>
-// before.getCatmullromLerp(before_plus, before, after, after_plus, axis, alpha)
-// )
-// } else if (
-// before.interpolation === Keyframe.interpolation.bezier ||
-// after.interpolation === Keyframe.interpolation.bezier
-// ) {
-// // Bezier
-// return mapAxes(axis => before.getBezierLerp(before, after, axis, alpha))
-// }
-// }
-// if (result && result instanceof Keyframe) {
-// let keyframe = result
-// let method = allow_expression ? 'get' : 'calc'
-// let dp_index =
-// keyframe.time > time || Math.epsilon(keyframe.time, time, epsilon)
-// ? 0
-// : keyframe.data_points.length - 1
-
-// return mapAxes(axis => keyframe[method](axis, dp_index))
-// }
-// return false
-// }
-
-// function buildBoneTree(node: DataTypes.DataOutliner): {
-// boneTree: AJBone
-// boneList: AJBone[]
-// } {
-// const boneList: AJBone[] = []
-
-// function recurse(node: DataTypes.DataOutliner) {
-// const children: AJBone[] = []
-
-// for (const child of node.children) {
-// children.push(recurse(child))
-// }
-
-// const bone = new AJBone(
-// node.id,
-// new Vector(node.origin[0], node.origin[1], node.origin[2]),
-// new Gimbals(node.rot[0], node.rot[1], node.rot[2]),
-// new Vector(1, 1, 1),
-// children
-// )
-
-// boneList.push(bone)
-// return bone
-// }
-
-// const boneTree = recurse(node)
-// return { boneTree, boneList }
-// }
-
-// export default async function (data: DataTypes.Data): Promise {
-// console.log('Render Worker Input:', { data })
-// const { boneTree, boneList } = buildBoneTree(data.outliner)
-
-// return data
-// }
diff --git a/src/renderWorker/renderer.worker.types.ts b/src/renderWorker/renderer.worker.types.ts
deleted file mode 100644
index 1390064d..00000000
--- a/src/renderWorker/renderer.worker.types.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-export interface DataHiarchy {}
-export interface AnimationDataBundle {
- keyframes: {
- [key: string]: {
- bezier: {
- left_time: number
- left_value: number
- right_time: number
- right_value: number
- linked: boolean
- }
- data_points: {
- x: number
- y: number
- z: number
- }[]
- time: number
- interpolation: number
- channel: string
- }[]
- }
- length: number
-}
-
-export interface DataOutliner {
- id: string
- rot: ArrayVector3
- origin: ArrayVector3
- children: DataOutliner[]
-}
-
-export type Data = {
- outliner: DataOutliner
- animation: AnimationDataBundle
-}
-export type Result = {}
diff --git a/src/rendering/animationRenderer.ts b/src/rendering/animationRenderer.ts
deleted file mode 100644
index 9b59d8b0..00000000
--- a/src/rendering/animationRenderer.ts
+++ /dev/null
@@ -1,265 +0,0 @@
-import { LimitClock, roundToN } from '../util/misc'
-import { ProgressBarController } from '../util/progress'
-import { IRenderedRig } from './modelRenderer'
-let progress: ProgressBarController
-
-export function correctSceneAngle() {
- main_preview.controls.rotateLeft(Math.PI)
- scene.setRotationFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI)
-}
-
-export function restoreSceneAngle() {
- main_preview.controls.rotateLeft(-Math.PI)
- scene.setRotationFromAxisAngle(new THREE.Vector3(0, 1, 0), 0)
-}
-
-function getNodeMatrix(node: OutlinerElement, scale: number) {
- const matrixWorld = node.mesh.matrixWorld.clone()
- matrixWorld.setPosition(
- new THREE.Vector3().setFromMatrixPosition(matrixWorld).multiplyScalar(1 / 16)
- )
- matrixWorld.scale(new THREE.Vector3().setScalar(scale))
- return matrixWorld
-}
-
-export interface IAnimationNode {
- type: 'bone' | 'camera' | 'locator'
- name: string
- uuid: string
- node?: Group | NullObject | Locator | OutlinerElement
- matrix: THREE.Matrix4
- pos: THREE.Vector3
- rot: THREE.Quaternion
- scale: THREE.Vector3
- interpolation?: 'instant' | 'default'
-}
-
-export interface IRenderedFrame {
- time: number
- nodes: IAnimationNode[]
- variant?: {
- uuid: string
- executeCondition: string
- }
- commands?: {
- commands: string
- executeCondition: string
- }
- animationState?: {
- animation: string
- executeCondition: string
- }
-}
-
-export interface IRenderedAnimation {
- name: string
- startDelay: number
- loopDelay: number
- frames: IRenderedFrame[]
- /**
- * Duration of the animation in ticks (AKA frames). Same as animation.frames.length
- */
- duration: number
- loopMode: 'loop' | 'once' | 'hold'
-}
-
-let lastAnimation: _Animation
-let previousFrame: Record<
- string,
- { matrix: number[]; interpolation: IAnimationNode['interpolation'] }
->
-export function getAnimationNodes(
- animation: _Animation,
- nodeMap: IRenderedRig['nodeMap'],
- time = 0
-) {
- if (lastAnimation !== animation) {
- lastAnimation = animation
- previousFrame = {}
- }
- const nodes: IAnimationNode[] = []
-
- for (const [uuid, node] of Object.entries(nodeMap)) {
- if (!node.node.export) continue
- const included = animation.affected_bones.find(b => b.value === uuid)
- // Ignore this bone if it's not affected by this animation
- if (
- (!included && animation.affected_bones_is_a_whitelist) ||
- (included && !animation.affected_bones_is_a_whitelist)
- )
- continue
-
- const prevFrame = previousFrame[uuid]
- let interpolation: IAnimationNode['interpolation'] = undefined
- let matrix: THREE.Matrix4
- switch (node.type) {
- case 'bone': {
- matrix = getNodeMatrix(node.node, node.scale)
- const animator = animation.animators[node.node.uuid]!
- if (
- animator?.keyframes
- .filter(k => k.time === roundToN(time - 0.05, 20))
- .find(k => k.data_points.length === 2)
- ) {
- interpolation = 'instant'
- } else if (previousFrame[uuid]?.interpolation === 'instant') {
- interpolation = 'default'
- }
- break
- }
- case 'locator':
- case 'camera':
- matrix = getNodeMatrix(node.node, 1)
- break
- }
-
- const pos = new THREE.Vector3()
- const rot = new THREE.Quaternion()
- const scale = new THREE.Vector3()
- matrix.decompose(pos, rot, scale)
- const matrixArray = matrix.toArray()
-
- if (
- node.type === 'bone' &&
- prevFrame !== undefined &&
- prevFrame.matrix !== undefined &&
- prevFrame.matrix.equals(matrixArray) &&
- prevFrame.interpolation === interpolation
- )
- continue
- previousFrame[uuid] = {
- matrix: matrixArray,
- interpolation,
- }
-
- nodes.push({
- type: node.type,
- name: node.name,
- uuid,
- node: node.node,
- matrix,
- pos,
- rot,
- scale,
- interpolation,
- })
- }
-
- return nodes
-}
-
-function getVariantKeyframe(animation: _Animation, time: number) {
- if (!animation.animators.effects?.variants) return
- for (const kf of animation.animators.effects.variants as _Keyframe[]) {
- if (kf.time === time)
- return {
- uuid: kf.data_points[0].variant,
- executeCondition: kf.data_points[0].executeCondition,
- }
- }
-}
-
-function getCommandsKeyframe(animation: _Animation, time: number) {
- if (!animation.animators.effects?.commands) return
- for (const kf of animation.animators.effects.commands as _Keyframe[]) {
- if (kf.time === time)
- return {
- commands: kf.data_points[0].commands,
- executeCondition: kf.data_points[0].executeCondition,
- }
- }
-}
-
-function getAnimationStateKeyframe(animation: _Animation, time: number) {
- if (!animation.animators.effects?.animationStates) return
- for (const kf of animation.animators.effects.animationStates as _Keyframe[]) {
- if (kf.time === time)
- return {
- animation: kf.data_points[0].animationState,
- executeCondition: kf.data_points[0].executeCondition,
- }
- }
-}
-
-export function updatePreview(animation: _Animation, time: number) {
- Timeline.time = time
- Animator.showDefaultPose(true)
- const nodes: OutlinerNode[] = [...Group.all, ...NullObject.all, ...Locator.all]
- if (OutlinerElement.types.camera) {
- nodes.push(...OutlinerElement.types.camera.all)
- }
- for (const node of nodes) {
- if (!(node.constructor as any).animator) continue
- Animator.resetLastValues()
- animation.getBoneAnimator(node).displayFrame(1)
- }
- Animator.resetLastValues()
- scene.updateMatrixWorld()
- if (animation.effects) animation.effects.displayFrame()
- Blockbench.dispatchEvent('display_animation_frame')
-}
-
-export async function renderAnimation(animation: _Animation, rig: IRenderedRig) {
- const rendered = {
- name: animation.name,
- startDelay: Number(animation.start_delay),
- loopDelay: Number(animation.loop_delay),
- frames: [],
- duration: 0,
- loopMode: animation.loop,
- } as IRenderedAnimation
- animation.select()
-
- const clock = new LimitClock(10)
- for (let time = 0; time <= animation.length; time = roundToN(time + 0.05, 20)) {
- // await new Promise(resolve => requestAnimationFrame(resolve))
- // await new Promise(resolve => setTimeout(resolve, 50))
- updatePreview(animation, time)
- rendered.frames.push({
- time,
- nodes: getAnimationNodes(animation, rig.nodeMap, time),
- variant: getVariantKeyframe(animation, time),
- commands: getCommandsKeyframe(animation, time),
- animationState: getAnimationStateKeyframe(animation, time),
- })
- progress.add(1)
- await clock.sync().then(b => b && progress.update())
- }
- rendered.duration = rendered.frames.length
-
- return rendered
-}
-
-function gatherProgress(): number {
- return Animator.animations.reduce((a, b) => a + b.length * 20, 0)
-}
-
-export async function renderAllAnimations(rig: IRenderedRig) {
- let selectedAnimation: _Animation | undefined
- let currentTime = 0
- progress = new ProgressBarController('Rendering Animations...', gatherProgress())
- Timeline.pause()
- // Save selected animation
- if (Mode.selected.id === 'animate') {
- selectedAnimation = Animator.selected
- currentTime = Timeline.time
- }
-
- correctSceneAngle()
- const animations: IRenderedAnimation[] = []
- for (const animation of Animator.animations) {
- animations.push(await renderAnimation(animation, rig))
- }
- restoreSceneAngle()
-
- // Restore selected animation
- if (Mode.selected.id === 'animate' && selectedAnimation) {
- selectedAnimation.select()
- Timeline.setTime(currentTime)
- Animator.preview()
- } else if (Mode.selected.id === 'edit') {
- Animator.showDefaultPose()
- }
-
- return animations
-}
diff --git a/src/resourcePackExporter.ts b/src/resourcePackExporter.ts
deleted file mode 100644
index 9f3af1c7..00000000
--- a/src/resourcePackExporter.ts
+++ /dev/null
@@ -1,327 +0,0 @@
-import { AJMetaFile } from './ajmeta'
-import { isValidResourcePackPath, safeFunctionName } from './minecraft'
-import { CustomModelData, IRenderedRig } from './rendering/modelRenderer'
-import { animatedJavaSettings } from './settings'
-import { ExpectedError, LimitClock } from './util/misc'
-import { ProgressBarController } from './util/progress'
-import { translate } from './util/translation'
-import { VirtualFolder } from './util/virtualFileSystem'
-
-async function fileExists(path: string) {
- return !!(await fs.promises.stat(path).catch(() => false))
-}
-
-function showPredicateFileOverwriteConfirmation(path: string) {
- const result = confirm(
- translate('animated_java.popup.confirm_predicate_file_overwrite.body', {
- file: PathModule.parse(path).base,
- path,
- }),
- translate('animated_java.popup.confirm_predicate_file_overwrite.title')
- )
- if (!result) throw new ExpectedError('User cancelled export due to predicate file overwrite.')
-}
-
-export async function exportResources(
- ajSettings: typeof animatedJavaSettings,
- projectSettings: NotUndefined,
- rig: IRenderedRig,
- rigExportFolder: string,
- textureExportFolder: string,
- rigItemModelExportPath: string
-) {
- const advancedResourcePackSettingsEnabled =
- projectSettings.enable_advanced_resource_pack_settings.value
- const projectNamespace = projectSettings.project_namespace.value
- const resourcePackPath = PathModule.parse(projectSettings.resource_pack_mcmeta.value).dir
- const resourcePackFolder = new VirtualFolder(
- advancedResourcePackSettingsEnabled
- ? 'internal_resource_pack_folder'
- : PathModule.basename(resourcePackPath),
- undefined,
- true
- )
- const assetsFolder = resourcePackFolder.newFolder('assets')
- const animatedJavaFolder = assetsFolder.newFolder('animated_java')
-
- //------------------------------------
- // Minecraft namespace
- //------------------------------------
-
- const [rigItemNamespace, rigItemName] = projectSettings.rig_item.value.split(':')
- const minecraftFolder = assetsFolder.newFolder('minecraft').newFolder('models/item')
-
- //------------------------------------
- // Empty Model
- //------------------------------------
- minecraftFolder.newFile('animated_java_empty.json', '{}')
-
- //------------------------------------
- // Rig Item Predicate File
- //------------------------------------
-
- interface IPredicateItemModel {
- parent: string
- textures: any
- overrides: Array<{
- predicate: { custom_model_data: number }
- model: string
- }>
- animated_java: {
- rigs: Record
- }
- }
-
- const predicateItemFilePath = advancedResourcePackSettingsEnabled
- ? rigItemModelExportPath
- : PathModule.join(
- PathModule.dirname(resourcePackPath),
- minecraftFolder.path,
- `${rigItemName}.json`
- )
-
- console.log('Predicate item file path:', predicateItemFilePath)
-
- // Default predicate item file content
- let predicateContent: IPredicateItemModel = {
- parent: 'item/generated',
- textures: {
- layer0: `${rigItemNamespace}:item/${rigItemName}`,
- },
- overrides: [],
- animated_java: {
- rigs: {},
- },
- }
- const usedIds: number[] = [] // IDs that are used by other projects
- const consumedIds: number[] = [] // IDs that are used by this project
- // Read predicate item file if it exists
- if (fs.existsSync(predicateItemFilePath)) {
- console.log('Reading predicate item file')
- try {
- const stringContent = await fs.promises.readFile(predicateItemFilePath, 'utf8')
- predicateContent = JSON.parse(stringContent)
- } catch (e) {
- console.warn('Failed to read predicate item file JSON')
- console.warn(e)
- }
- // Show overwrite confirmation if predicate file wasn't created by animated_java.
- if (!predicateContent.animated_java) {
- showPredicateFileOverwriteConfirmation(predicateItemFilePath)
- predicateContent.animated_java = {
- rigs: {
- ORIGINAL_PREDICATE_FILE: {
- used_ids: predicateContent.overrides
- .filter(o => o.predicate.custom_model_data !== undefined)
- .map(o => o.predicate.custom_model_data),
- },
- },
- }
- usedIds.push(...predicateContent.animated_java.rigs.ORIGINAL_PREDICATE_FILE.used_ids)
- }
-
- // Clean up content
- predicateContent.animated_java ??= { rigs: {} }
- predicateContent.animated_java.rigs ??= {}
- predicateContent.overrides = predicateContent.overrides.filter(
- o => o.predicate.custom_model_data !== 1
- )
- // Merge with existing predicate file
- console.log('Merging with existing predicate file')
- console.log(predicateContent)
- for (const [name, rig] of Object.entries(predicateContent.animated_java.rigs)) {
- const localUsedIds = rig.used_ids
- if (name === projectNamespace) {
- // Clean out old overrides
- predicateContent.overrides = predicateContent.overrides.filter(o => {
- return !localUsedIds.includes(o.predicate.custom_model_data)
- })
- continue
- }
- usedIds.push(...localUsedIds)
- }
- }
-
- if (!usedIds.includes(1)) usedIds.push(1)
- predicateContent.overrides.push({
- predicate: { custom_model_data: 1 },
- model: 'item/animated_java_empty',
- })
-
- CustomModelData.usedIds = usedIds
- predicateContent.animated_java.rigs[projectNamespace] = { used_ids: consumedIds }
-
- // Create virtual predicate item file with content
- const predicateItemFile = minecraftFolder.newFile(`${rigItemName}.json`, predicateContent)
-
- //------------------------------------
- // Project namespace
- //------------------------------------
-
- const NAMESPACE = projectSettings.project_namespace.value
- const [modelsFolder, texturesFolder] = animatedJavaFolder.newFolders(
- `/models/item/${NAMESPACE}`,
- `/textures/item/${NAMESPACE}`
- )
-
- for (const texture of Object.values(rig.textures)) {
- let image: Buffer | undefined
- let mcmeta: Buffer | undefined
- let optifineEmissive: Buffer | undefined
- if (texture.source?.startsWith('data:')) {
- image = Buffer.from(texture.source.split(',')[1], 'base64')
- } else if (texture.path && fs.existsSync(texture.path)) {
- if (!isValidResourcePackPath(texture.path)) {
- image = await fs.promises.readFile(texture.path)
- if (fs.existsSync(texture.path + '.mcmeta'))
- mcmeta = await fs.promises.readFile(texture.path + '.mcmeta')
- const emissivePath = texture.path.replace('.png', '') + '_e.png'
- if (fs.existsSync(emissivePath))
- optifineEmissive = await fs.promises.readFile(emissivePath)
- }
- } else {
- console.warn(`Texture "${texture.name}" has no source or path`)
- }
-
- if (image === undefined) continue
-
- const textureName = safeFunctionName(texture.name)
- texturesFolder.newFile(`${textureName}.png`, image)
- if (mcmeta) texturesFolder.newFile(`${textureName}.png.mcmeta`, mcmeta)
- if (optifineEmissive) texturesFolder.newFile(`${textureName}_e.png`, optifineEmissive)
- }
-
- for (const bone of Object.values(rig.nodeMap)) {
- if (bone.type !== 'bone') continue
- modelsFolder.newFile(`${bone.name}.json`, bone.model)
- consumedIds.push((bone.customModelData = CustomModelData.get()))
- predicateItemFile.content.overrides.push({
- predicate: {
- custom_model_data: bone.customModelData,
- },
- model: bone.resourceLocation,
- })
- }
-
- for (const [variantName, variantBoneMap] of Object.entries(rig.variantModels)) {
- if (variantBoneMap.default) continue
- const variantFolder = modelsFolder.newFolder(variantName)
- for (const [uuid, variantBone] of Object.entries(variantBoneMap)) {
- const bone = rig.nodeMap[uuid]
- if (bone.type !== 'bone') continue
- variantFolder.newFile(`${bone.name}.json`, variantBone.model)
- consumedIds.push((variantBone.customModelData = CustomModelData.get()))
- predicateItemFile.content.overrides.push({
- predicate: {
- custom_model_data: variantBone.customModelData,
- },
- model: variantBone.resourceLocation,
- })
- }
- }
-
- predicateItemFile.content.overrides.sort(
- (a: any, b: any) => a.predicate.custom_model_data - b.predicate.custom_model_data
- )
-
- async function processAJMeta(filePaths: string[]) {
- const oldAJMetaPath = PathModule.join(resourcePackPath, '.ajmeta')
- const ajMetaPath = PathModule.join(resourcePackPath, 'resourcepack.ajmeta')
-
- // FIXME - This is an extremely hacky way to filter out the predicate item file from the file list
- filePaths = filePaths.filter(
- p =>
- p !==
- predicateItemFile.path
- .replace(resourcePackFolder.path + '/', '')
- .replaceAll('/', PathModule.sep)
- )
-
- const ajmeta = new AJMetaFile()
-
- if (await fileExists(ajMetaPath)) await ajmeta.load(ajMetaPath)
- else if (await fileExists(oldAJMetaPath)) {
- await ajmeta.load(oldAJMetaPath)
- await fs.promises.unlink(oldAJMetaPath)
- }
-
- let project = ajmeta.getProject(Project!.animated_java_uuid!)
- if (project === undefined) {
- project = ajmeta.addProject(Project!.animated_java_uuid!, NAMESPACE, filePaths)
- }
-
- const progress = new ProgressBarController(
- 'Cleaning up old Resource Pack files...',
- project.file_list.length
- )
- // Clean out old files from disk
- const clock = new LimitClock(10)
- for (let path of project.file_list) {
- await clock.sync().then(b => b && progress.update())
- path = PathModule.join(resourcePackPath, path)
- await fs.promises.unlink(path).catch(() => undefined)
- const dirPath = PathModule.dirname(path)
- const contents = await fs.promises.readdir(dirPath).catch(() => undefined)
- if (contents && contents.length === 0)
- await fs.promises.rmdir(dirPath).catch(() => undefined)
- progress.add(1)
- }
- progress.finish()
-
- project.file_list = filePaths
-
- await fs.promises.writeFile(
- ajMetaPath,
- ajSettings.minify_output.value
- ? JSON.stringify(ajmeta.toJSON())
- : JSON.stringify(ajmeta.toJSON(), null, 4)
- )
- }
-
- if (advancedResourcePackSettingsEnabled) {
- const progress = new ProgressBarController(
- 'Writing Resource Pack to Disk',
- modelsFolder.childCount + texturesFolder.childCount + 1
- )
-
- const filePaths = [...modelsFolder.getAllFilePaths(), ...texturesFolder.getAllFilePaths()]
-
- await processAJMeta(filePaths)
-
- await fs.promises.mkdir(rigExportFolder, { recursive: true })
- await modelsFolder.writeChildrenToDisk(rigExportFolder, {
- progress,
- skipEmptyFolders: true,
- })
-
- await fs.promises.mkdir(textureExportFolder, { recursive: true })
- await texturesFolder.writeChildrenToDisk(textureExportFolder, {
- progress,
- skipEmptyFolders: true,
- })
-
- const predicateItemExportFolder = PathModule.parse(rigItemModelExportPath).dir
- await fs.promises.mkdir(predicateItemExportFolder, { recursive: true })
- await predicateItemFile.writeToDisk(predicateItemExportFolder, {
- progress,
- skipEmptyFolders: true,
- })
-
- progress.finish()
- } else {
- console.log('Writing Resource Pack to Disk')
-
- const filePaths = resourcePackFolder.getAllFilePaths()
-
- await processAJMeta(filePaths)
-
- const progress = new ProgressBarController(
- 'Writing Resource Pack to Disk',
- assetsFolder.childCount
- )
-
- await assetsFolder.writeToDisk(resourcePackPath, { progress, skipEmptyFolders: true })
-
- progress.finish()
- }
-}
diff --git a/src/settings.ts b/src/settings.ts
deleted file mode 100644
index 32fc5c83..00000000
--- a/src/settings.ts
+++ /dev/null
@@ -1,461 +0,0 @@
-import { GUIStructure } from './guiStructure'
-import { formatStr } from './util/misc'
-import { Subscribable } from './util/subscribable'
-import { translate } from './util/translation'
-import * as events from './events'
-import { reducedMotion } from './ui/util/accessability'
-
-export interface IInfoPopup {
- type: 'warning' | 'error' | 'info'
- title: string
- lines: string[]
-}
-
-export type SettingID = `${string}${string}:${string}${string}/${string}${string}`
-
-export interface ISettingOptions {
- /**
- * The id of the setting.
- * The id should be in the format of `namespace:interface/setting_name`.
- * This should be unique across all plugins and interfaces!
- */
- id: SettingID
- displayName: string
- /**
- * A list of paragraphs to display in the description of the setting.
- */
- description: string[]
- /**
- * A string to display below the setting's value in the settings dialog.
- */
- subtext?: string
- /**
- * The default value of the setting.
- */
- defaultValue: V
- /**
- * Whether or not the setting can be reset to its default value.
- */
- resettable?: boolean
- /**
- * A link to the docs page/section for this setting.
- */
- docsLink?: string
- /**
- * A list of settings that this setting depends on.
- * If any of the settings in this list update, this setting will also update.
- */
- dependsOn?: SettingID[]
-}
-
-export type ISettingsObject = Record>
-
-export class Setting extends Subscribable {
- static registeredSettings = new Map>()
-
- id: SettingID
- displayName: string
- description: string[]
- defaultValue: V
- resettable?: boolean
- docsLink?: string
- dependsOn?: SettingID[]
- subtext?: string
-
- private _initialized: boolean
- private _updating: boolean
- protected _value: V
- protected lastValue: V
- infoPopup?: IInfoPopup
- /**
- * Creates a new setting
- * @param onUpdate runs when the setting's value is updated.
- * @param onInit runs when the setting is initialized.
- * @param onConfirm runs when the setting's value is confirmed (when closing the dialog).
- */
- constructor(
- options: ISettingOptions,
- public onUpdate?: (setting: R) => void,
- public onInit?: (setting: R) => void,
- public onConfirm?: (setting: R) => void
- ) {
- super()
- this.id = options.id
- this.displayName = options.displayName
- this.description = options.description
- this.defaultValue = options.defaultValue
- this.resettable = options.resettable
- this.docsLink = options.docsLink
- this.dependsOn = options.dependsOn
- this.subtext = options.subtext
-
- this._value = this.defaultValue
- this.lastValue = this.defaultValue
- this._initialized = false
- this._updating = false
-
- Setting.registeredSettings.set(this.id, this)
- }
-
- get value(): V {
- return this._value
- }
-
- set value(value: V) {
- this._value = value
- this._value ??= this.defaultValue
- this._onUpdate()
- }
-
- set(value: V) {
- this.value = value
- }
-
- _onInit() {
- if (this._initialized) return
- console.log('Initializing setting', this.id)
- if (this.onInit) this.onInit(this as unknown as R)
-
- if (this.dependsOn) {
- for (const id of this.dependsOn) {
- const setting = Setting.registeredSettings.get(id)
- if (!setting) {
- console.warn(
- `Setting ${this.id} depends on setting ${id}, but that setting does not exist.`
- )
- continue
- }
- setting.subscribe(() => {
- this._onUpdate(true)
- })
- }
- }
-
- this._initialized = true
- }
-
- _onUpdate(forced = false) {
- if (this._updating) return
- if (!forced && this.value === this.lastValue) return
- // console.log('Updating setting', this.id, this.value)
- this._updating = true
- this.lastValue = this.value
- this.infoPopup = undefined
- if (this.onUpdate) this.onUpdate(this as unknown as R)
- this.dispatch(this as unknown as R)
- this._updating = false
- }
-
- verify() {
- if (this.onUpdate) this.onUpdate(this as unknown as R)
- return this.infoPopup
- }
-
- _save(): any {
- return this.value
- }
-
- _load(value: any) {
- this.value = value
- }
-}
-
-export class CheckboxSetting extends Setting {}
-export class InlineTextSetting extends Setting {}
-export class CodeboxSetting extends Setting {
- language: string
- constructor(
- options: ISettingOptions