diff --git a/spine-ts/spine-construct3/spine-construct3-lib/src/CustomUI.ts b/spine-ts/spine-construct3/spine-construct3-lib/src/CustomUI.ts index 2596dc444..dc39f1a6e 100644 --- a/spine-ts/spine-construct3/spine-construct3-lib/src/CustomUI.ts +++ b/spine-ts/spine-construct3/spine-construct3-lib/src/CustomUI.ts @@ -27,7 +27,6 @@ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ -// Common theme type interface ModalTheme { overlayBg: string; captionBg: string; @@ -43,38 +42,36 @@ interface ModalTheme { itemSelectedBg: string; } -// Helper: Create theme function getTheme (darkMode: boolean): ModalTheme { return darkMode ? { overlayBg: 'rgba(0, 0, 0, 0.5)', - captionBg: 'rgb(71, 71, 71)', // gray9 - captionText: 'rgb(214, 214, 214)', // gray27 - contentBg: 'rgb(87, 87, 87)', // gray11 - contentText: 'rgb(214, 214, 214)', // gray27 - buttonBg: 'rgb(71, 71, 71)', // gray9 - buttonText: 'rgb(214, 214, 214)', // gray27 - buttonBorder: 'rgb(56, 56, 56)', // gray7 - buttonHoverBg: 'rgb(79, 79, 79)', // gray10 - closeColor: 'rgb(168, 168, 168)', // gray21 - itemHoverBg: 'rgb(79, 79, 79)', // gray10 - itemSelectedBg: 'rgb(56, 56, 56)', // gray7 + captionBg: 'rgb(71, 71, 71)', + captionText: 'rgb(214, 214, 214)', + contentBg: 'rgb(87, 87, 87)', + contentText: 'rgb(214, 214, 214)', + buttonBg: 'rgb(71, 71, 71)', + buttonText: 'rgb(214, 214, 214)', + buttonBorder: 'rgb(56, 56, 56)', + buttonHoverBg: 'rgb(79, 79, 79)', + closeColor: 'rgb(168, 168, 168)', + itemHoverBg: 'rgb(79, 79, 79)', + itemSelectedBg: 'rgb(56, 56, 56)', } : { overlayBg: 'rgba(0, 0, 0, 0.3)', - captionBg: 'rgb(247, 247, 247)', // gray31 - captionText: 'rgb(94, 94, 94)', // gray12 - contentBg: 'rgb(232, 232, 232)', // gray29 - contentText: 'rgb(94, 94, 94)', // gray12 - buttonBg: 'rgb(222, 222, 222)', // gray28 - buttonText: 'rgb(94, 94, 94)', // gray12 - buttonBorder: 'rgb(199, 199, 199)', // gray25 - buttonHoverBg: 'rgb(214, 214, 214)', // gray27 - closeColor: 'rgb(94, 94, 94)', // gray12 - itemHoverBg: 'rgb(214, 214, 214)', // gray27 - itemSelectedBg: 'rgb(199, 199, 199)', // gray25 + captionBg: 'rgb(247, 247, 247)', + captionText: 'rgb(94, 94, 94)', + contentBg: 'rgb(232, 232, 232)', + contentText: 'rgb(94, 94, 94)', + buttonBg: 'rgb(222, 222, 222)', + buttonText: 'rgb(94, 94, 94)', + buttonBorder: 'rgb(199, 199, 199)', + buttonHoverBg: 'rgb(214, 214, 214)', + closeColor: 'rgb(94, 94, 94)', + itemHoverBg: 'rgb(214, 214, 214)', + itemSelectedBg: 'rgb(199, 199, 199)', }; } -// Helper: Create overlay function createOverlay (theme: ModalTheme): HTMLDivElement { const overlay = document.createElement('div'); overlay.style.cssText = ` @@ -94,7 +91,6 @@ function createOverlay (theme: ModalTheme): HTMLDivElement { return overlay; } -// Helper: Create dialog function createDialog (theme: ModalTheme, minWidth: number): HTMLDivElement { const dialog = document.createElement('div'); dialog.style.cssText = ` @@ -109,7 +105,6 @@ function createDialog (theme: ModalTheme, minWidth: number): HTMLDivElement { return dialog; } -// Helper: Create caption with close button function createCaption (title: string, theme: ModalTheme, onClose: () => void): HTMLDivElement { const caption = document.createElement('div'); caption.style.cssText = ` @@ -149,7 +144,6 @@ function createCaption (title: string, theme: ModalTheme, onClose: () => void): return caption; } -// Helper: Create footer function createFooter (): HTMLDivElement { const footer = document.createElement('div'); footer.style.cssText = ` @@ -161,7 +155,6 @@ function createFooter (): HTMLDivElement { return footer; } -// Helper: Create button function createButton (text: string, theme: ModalTheme, onClick: () => void): HTMLButtonElement { const btn = document.createElement('button'); btn.textContent = text; @@ -181,7 +174,6 @@ function createButton (text: string, theme: ModalTheme, onClick: () => void): HT return btn; } -// Helper: Setup modal event handlers function setupModalHandlers (overlay: HTMLDivElement, onCancel: () => void, extraKeyHandler?: (e: KeyboardEvent) => boolean) { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { @@ -193,7 +185,9 @@ function setupModalHandlers (overlay: HTMLDivElement, onCancel: () => void, extr } }; - document.addEventListener('keydown', handleKeyDown); + setTimeout(() => { + document.addEventListener('keydown', handleKeyDown); + }, 0); overlay.addEventListener('click', (e) => { if (e.target === overlay) { @@ -207,7 +201,6 @@ function setupModalHandlers (overlay: HTMLDivElement, onCancel: () => void, extr }; } -// Original interfaces interface ModalButton { text: string; color?: string; @@ -324,11 +317,9 @@ export function showListSelectionModal (options: ListSelectionOptions): Promise< `; itemDiv.addEventListener('click', () => { - // Deselect all items contents.querySelectorAll('div').forEach(div => { div.style.background = 'transparent'; }); - // Select this item itemDiv.style.background = theme.itemSelectedBg; selectedItem = item; }); @@ -486,7 +477,6 @@ export function showMultiListSelectionModal (options: MultiListSelectionOptions) gap: 8px; `; - // Create checkbox const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.checked = selectedSet.has(item); @@ -515,7 +505,6 @@ export function showMultiListSelectionModal (options: MultiListSelectionOptions) }; itemDiv.addEventListener('click', (e) => { - // Don't toggle if clicking directly on checkbox (it handles itself) if (e.target !== checkbox) { toggleSelection(); } diff --git a/spine-ts/spine-construct3/src/instance.ts b/spine-ts/spine-construct3/src/instance.ts index a8a0f184a..5eb5ff28a 100644 --- a/spine-ts/spine-construct3/src/instance.ts +++ b/spine-ts/spine-construct3/src/instance.ts @@ -126,8 +126,9 @@ class SpineC3PluginInstance extends SDK.IWorldInstanceBase { if (id === PLUGIN_CLASS.PROP_SKIN) { this.skins = []; - const validatedString = this.validateSkinString(); - if (validatedString) { + const skinString = this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_SKIN) as string; + const validatedString = await this.validateSkinString(skinString); + if (validatedString !== undefined && validatedString !== skinString) { this._inst.SetPropertyValue(PLUGIN_CLASS.PROP_SKIN, validatedString); return; } @@ -138,6 +139,14 @@ class SpineC3PluginInstance extends SDK.IWorldInstanceBase { } if (id === PLUGIN_CLASS.PROP_ANIMATION) { + + const animationString = this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_ANIMATION) as string; + const validatedString = await this.validateAnimationString(animationString); + if (validatedString !== undefined && validatedString !== this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_ANIMATION)) { + this._inst.SetPropertyValue(PLUGIN_CLASS.PROP_ANIMATION, validatedString); + return; + } + this.setAnimation(); this.layoutView?.Refresh(); return; @@ -259,7 +268,6 @@ class SpineC3PluginInstance extends SDK.IWorldInstanceBase { } } return zoom === -1 ? 1 : zoom; - } private setAnimation () { @@ -267,14 +275,47 @@ class SpineC3PluginInstance extends SDK.IWorldInstanceBase { this.animation = propValue === "" ? undefined : propValue; } - private validateSkinString () { - const skins = this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_SKIN) as string; - if (skins === "") return; - const split = skins.split(","); + private async validateSkinString (skins: string) { + if (skins === "" || !this.skeleton) return; - if (!split.includes("")) return; + const split = skins.split(",").filter(s => s !== ""); + if (split.length === 0) return; - return split.filter(s => s !== "").join(","); + const availableSkins = this.skeleton.data.skins.map(skin => skin.name).filter(s => s !== "default"); + + const invalidSkins = split.filter(skinName => !availableSkins.includes(skinName)); + + if (invalidSkins.length > 0) { + await spine.showAlertModal({ + darkMode: false, + title: this.lang('invalid-skins.title'), + message: this.lang('invalid-skins.message', [invalidSkins.join(", ")]) + }); + + const validSkins = split.filter(skinName => availableSkins.includes(skinName)); + console.log(validSkins); + return validSkins.join(","); + } + + return split.join(","); + } + + private async validateAnimationString (animation: string) { + if (animation === "" || !this.skeleton) return; + + const availableAnimations = this.skeleton.data.animations.map(anim => anim.name); + + if (!availableAnimations.includes(animation)) { + await spine.showAlertModal({ + darkMode: false, + title: this.lang('invalid-animation.title'), + message: this.lang('invalid-animation.message', [animation]) + }); + + return ""; + } + + return animation; } private setSkin () { @@ -483,10 +524,10 @@ class SpineC3PluginInstance extends SDK.IWorldInstanceBase { const errorTextC3 = iRenderer.CreateRendererText() this.errorTextC3 = errorTextC3; - this.errorTextC3.SetFontSize(12); - this.errorTextC3.SetColorRgb(1, 0, 0); - this.errorTextC3.SetTextureUpdateCallback(() => iLayoutView.Refresh()); - return this.errorTextC3; + errorTextC3.SetFontSize(12); + errorTextC3.SetColorRgb(1, 0, 0); + errorTextC3.SetTextureUpdateCallback(() => iLayoutView.Refresh()); + return errorTextC3; } private update (delta: number) { @@ -566,15 +607,15 @@ class SpineC3PluginInstance extends SDK.IWorldInstanceBase { const action = await spine.showModal({ darkMode: false, maxWidth: 500, - title: this.lang(`showModal.${type}.title`), - text: this.lang(`showModal.${type}.text`, [collisionSpriteName]), + title: this.lang(`${type}.title`), + text: this.lang(`${type}.message`, [collisionSpriteName]), buttons: [ { - text: this.lang(`showModal.${type}.buttons.${0}`), + text: this.lang(`${type}.buttons.${0}`), value: 'disable' }, { - text: this.lang(`showModal.${type}.buttons.${1}`, [collisionSpriteName]), + text: this.lang(`${type}.buttons.${1}`, [collisionSpriteName]), color: '#d9534f', value: 'delete' } @@ -597,8 +638,8 @@ class SpineC3PluginInstance extends SDK.IWorldInstanceBase { if (!this.skeleton) { await spine.showAlertModal({ darkMode: false, - title: 'Error', - message: 'Skeleton not loaded. Please ensure atlas and skeleton files are set.', + title: this.lang('skeleton-not-loaded.title'), + message: this.lang('skeleton-not-loaded.message'), }); return; } @@ -607,15 +648,15 @@ class SpineC3PluginInstance extends SDK.IWorldInstanceBase { if (animations.length === 0) { await spine.showAlertModal({ darkMode: false, - title: 'No Animations', - message: 'No animations found in the skeleton.', + title: this.lang('no-animations.title'), + message: this.lang('no-animations.message'), }); return; } const selectedAnimation = await spine.showListSelectionModal({ darkMode: false, - title: 'Select Animation', + title: this.lang('select-animation.title'), items: animations, }); @@ -628,8 +669,8 @@ class SpineC3PluginInstance extends SDK.IWorldInstanceBase { if (!this.skeleton) { await spine.showAlertModal({ darkMode: false, - title: 'Error', - message: 'Skeleton not loaded. Please ensure atlas and skeleton files are set.', + title: this.lang('skeleton-not-loaded.title'), + message: this.lang('skeleton-not-loaded.message'), }); return; } @@ -638,15 +679,15 @@ class SpineC3PluginInstance extends SDK.IWorldInstanceBase { if (skins.length === 0) { await spine.showAlertModal({ darkMode: false, - title: 'No Skins', - message: 'No skins found in the skeleton.', + title: this.lang('no-skins.title'), + message: this.lang('no-skins.message'), }); return; } const selectedSkins = await spine.showMultiListSelectionModal({ darkMode: false, - title: 'Select Skins', + title: this.lang('select-skins.title'), items: skins, selectedItems: this.skins, }); diff --git a/spine-ts/spine-construct3/src/lang/en-US.json b/spine-ts/spine-construct3/src/lang/en-US.json index b2a6437a3..7ecb2cae8 100644 --- a/spine-ts/spine-construct3/src/lang/en-US.json +++ b/spine-ts/spine-construct3/src/lang/en-US.json @@ -689,15 +689,39 @@ } }, "custom_ui": { - "showModal": { - "spine-enable-collision": { - "title": "Delete Sprite Object Type", - "text": "This is the last instance of {0}. You can delete the {0} or just disable the Sprite collision body. Deleting the Sprite collision body will remove all related ACEs in the event sheet, if there's any. How do you want to proceed?", - "buttons": { - "0": "Just disable", - "1": "Disable and delete {0}" - } + "spine-enable-collision": { + "title": "Delete Sprite Object Type", + "message": "This is the last instance of {0}. You can delete the {0} or just disable the Sprite collision body. Deleting the Sprite collision body will remove all related ACEs in the event sheet, if there's any. How do you want to proceed?", + "buttons": { + "0": "Just disable", + "1": "Disable and delete {0}" } + }, + "skeleton-not-loaded": { + "title": "Error", + "message": "Skeleton not loaded. Please ensure atlas and skeleton files are set." + }, + "no-animations": { + "title": "No Animations", + "message": "No animations found in the skeleton." + }, + "no-skins": { + "title": "No Skins", + "message": "No skins found in the skeleton." + }, + "select-animation": { + "title": "Select Animation" + }, + "select-skins": { + "title": "Select Skins" + }, + "invalid-skins": { + "title": "Invalid Skins", + "message": "The following skins do not exist in the skeleton: {0}.\n\nThey have been removed. Please use the \"Select skin\" button to choose from existing skins." + }, + "invalid-animation": { + "title": "Invalid Animation", + "message": "The animation \"{0}\" does not exist in the skeleton.\n\nIt has been removed. Please use the \"Select animation\" button to choose from existing animations." } } } diff --git a/spine-ts/spine-construct3/src/lang/zh-CN.json b/spine-ts/spine-construct3/src/lang/zh-CN.json index e4b86dbb5..e8a169d56 100644 --- a/spine-ts/spine-construct3/src/lang/zh-CN.json +++ b/spine-ts/spine-construct3/src/lang/zh-CN.json @@ -689,15 +689,39 @@ } }, "custom_ui": { - "showModal": { - "spine-enable-collision": { - "title": "删除精灵对象类型", - "text": "这是{0}的最后一个实例。您可以删除{0}或仅禁用精灵碰撞体。删除精灵碰撞体将移除事件表中所有相关的ACE(如果有)。您想如何继续?", - "buttons": { - "0": "仅禁用", - "1": "禁用并删除{0}" - } + "spine-enable-collision": { + "title": "删除精灵对象类型", + "message": "这是{0}的最后一个实例。您可以删除{0}或仅禁用精灵碰撞体。删除精灵碰撞体将移除事件表中所有相关的ACE(如果有)。您想如何继续?", + "buttons": { + "0": "仅禁用", + "1": "禁用并删除{0}" } + }, + "skeleton-not-loaded": { + "title": "错误", + "message": "骨架未加载。请确保已设置图集和骨架文件。" + }, + "no-animations": { + "title": "无动画", + "message": "在骨架中未找到动画。" + }, + "no-skins": { + "title": "无皮肤", + "message": "在骨架中未找到皮肤。" + }, + "select-animation": { + "title": "选择动画" + }, + "select-skins": { + "title": "选择皮肤" + }, + "invalid-skins": { + "title": "无效皮肤", + "message": "以下皮肤在骨架中不存在:{0}。\n\n它们已被删除。请使用\"选择皮肤\"按钮从现有皮肤中选择。" + }, + "invalid-animation": { + "title": "无效动画", + "message": "动画\"{0}\"在骨架中不存在。\n\n它已被删除。请使用\"选择动画\"按钮从现有动画中选择。" } } }