From f6edb8d8c25dc849d8e521a1a024afa68077bcde Mon Sep 17 00:00:00 2001 From: xiaoyun7298 Date: Wed, 18 Feb 2026 19:52:34 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=87=AA=E5=B7=B1=E5=8A=A8?= =?UTF-8?q?=E5=8A=A8=E7=94=BB=E9=A3=98=E5=B8=A6=E6=8F=92=E4=BB=B6=E5=92=8C?= =?UTF-8?q?BsScriptHub=E5=AF=B9PY2.7=E7=9A=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...\345\212\250\345\212\250\347\224\273.json" | 25 + .../Scripts/BsScriptHub/scripts_index.json | 27 +- .../Scripts/BulletScripts/BsScriptHub.py | 6 +- .../final_auto_animator/autoAnim_main.ms | 134 ++++ .../final_auto_animator/core/constants.ms | 134 ++++ .../final_auto_animator/core/curve_editor.ms | 163 +++++ .../core/custom_attributes.ms | 324 +++++++++ .../core/expression_builder.ms | 174 +++++ .../core/hierarchy_manager.ms | 407 +++++++++++ .../core/spline_curve_sampler.ms | 317 ++++++++ .../core/spline_curve_widget.ms | 684 ++++++++++++++++++ .../core/tool_functions.ms | 309 ++++++++ .../final_auto_animator/core/tool_window.ms | 567 +++++++++++++++ .../Quote/final_auto_animator/utils/naming.ms | 122 ++++ .../utils/startup_installer.ms | 209 ++++++ .../final_auto_animator/utils/validation.ms | 127 ++++ 16 files changed, 3727 insertions(+), 2 deletions(-) create mode 100644 "_BsKeyTools/Scripts/BsScriptHub/04_\345\212\250\347\224\273\345\267\245\345\205\267/\350\207\252\345\267\261\345\212\250\345\212\250\347\224\273.json" create mode 100644 _BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/autoAnim_main.ms create mode 100644 _BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/constants.ms create mode 100644 _BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/curve_editor.ms create mode 100644 _BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/custom_attributes.ms create mode 100644 _BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/expression_builder.ms create mode 100644 _BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/hierarchy_manager.ms create mode 100644 _BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/spline_curve_sampler.ms create mode 100644 _BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/spline_curve_widget.ms create mode 100644 _BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/tool_functions.ms create mode 100644 _BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/tool_window.ms create mode 100644 _BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/utils/naming.ms create mode 100644 _BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/utils/startup_installer.ms create mode 100644 _BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/utils/validation.ms diff --git "a/_BsKeyTools/Scripts/BsScriptHub/04_\345\212\250\347\224\273\345\267\245\345\205\267/\350\207\252\345\267\261\345\212\250\345\212\250\347\224\273.json" "b/_BsKeyTools/Scripts/BsScriptHub/04_\345\212\250\347\224\273\345\267\245\345\205\267/\350\207\252\345\267\261\345\212\250\345\212\250\347\224\273.json" new file mode 100644 index 0000000..1b693bd --- /dev/null +++ "b/_BsKeyTools/Scripts/BsScriptHub/04_\345\212\250\347\224\273\345\267\245\345\205\267/\350\207\252\345\267\261\345\212\250\345\212\250\347\224\273.json" @@ -0,0 +1,25 @@ +{ + "name": "自己动动画", + "version": "2.0.0", + "description": "骨骼链程序化次级动画工具(自己动)\n\n用于创建尾巴、头发、触手等骨骼链的自动动画效果。\n\n功能特点:\n- 一键创建控制器、层级和表达式\n- 正弦波表达式驱动动画\n- Spline权重曲线控制振幅分布\n- 4层层级系统(Wave/Master/Copy定位器)\n- 支持X/Y/Z轴动画\n\n使用方法:\n1. 创建骨骼链(建议3-10根)\n2. 选择所有骨骼\n3. 点击^O^按钮一键生成动画", + "author": "Auto Animation Tool Team", + "optimizer": "Bullet.S", + "modified_date": "", + "keywords": [ + "动画", + "次级动画", + "骨骼链", + "尾巴", + "头发", + "触手", + "程序化", + "正弦波", + "自己动", + "表达式" + ], + "preview": "", + "script": "BulletScripts/Quote/final_auto_animator/autoAnim_main.ms", + "url": "", + "tutorial": "" +} + diff --git a/_BsKeyTools/Scripts/BsScriptHub/scripts_index.json b/_BsKeyTools/Scripts/BsScriptHub/scripts_index.json index 52f281c..403fd7f 100644 --- a/_BsKeyTools/Scripts/BsScriptHub/scripts_index.json +++ b/_BsKeyTools/Scripts/BsScriptHub/scripts_index.json @@ -304,6 +304,31 @@ "tutorial": "", "category": "04_动画工具" }, + { + "name": "自己动动画", + "version": "2.0.0", + "description": "骨骼链程序化次级动画工具(自己动)\n\n用于创建尾巴、头发、触手等骨骼链的自动动画效果。\n\n功能特点:\n- 一键创建控制器、层级和表达式\n- 正弦波表达式驱动动画\n- Spline权重曲线控制振幅分布\n- 4层层级系统(Wave/Master/Copy定位器)\n- 支持X/Y/Z轴动画\n\n使用方法:\n1. 创建骨骼链(建议3-10根)\n2. 选择所有骨骼\n3. 点击^O^按钮一键生成动画", + "author": "Auto Animation Tool Team", + "optimizer": "Bullet.S", + "modified_date": "", + "keywords": [ + "动画", + "次级动画", + "骨骼链", + "尾巴", + "头发", + "触手", + "程序化", + "正弦波", + "自己动", + "表达式" + ], + "preview": "", + "script": "BulletScripts/Quote/final_auto_animator/autoAnim_main.ms", + "url": "", + "tutorial": "", + "category": "04_动画工具" + }, { "name": "补间动画工具", "version": "1.0", @@ -329,7 +354,7 @@ "description": "可视化编辑运动轨迹,支持轨迹修改和平滑", "author": "未知", "optimizer": "Bullet.S", - "modified_date": "2026-01-17", + "modified_date": "2026-02-10", "keywords": [ "动画", "轨迹", diff --git a/_BsKeyTools/Scripts/BulletScripts/BsScriptHub.py b/_BsKeyTools/Scripts/BulletScripts/BsScriptHub.py index bdc9495..b0ef4ee 100644 --- a/_BsKeyTools/Scripts/BulletScripts/BsScriptHub.py +++ b/_BsKeyTools/Scripts/BulletScripts/BsScriptHub.py @@ -1672,7 +1672,11 @@ def _open_help(self): def _show_about(self): """显示关于对话框""" dialog = AboutDialog(self, VERSION, self.current_branch) - dialog.exec_() if hasattr(dialog, 'exec_') else dialog.exec() + # Python 2.7: exec is a keyword, use exec_() or getattr + if hasattr(dialog, 'exec_'): + dialog.exec_() + else: + getattr(dialog, 'exec')() def _toggle_branch(self): """切换分支""" diff --git a/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/autoAnim_main.ms b/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/autoAnim_main.ms new file mode 100644 index 0000000..6eab07d --- /dev/null +++ b/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/autoAnim_main.ms @@ -0,0 +1,134 @@ +-- ============================================================================ +-- Auto Animation Tool 2.0 - Main Entry Point +-- ============================================================================ +-- This is the main entry point for the Auto Animation Tool. +-- It loads all required modules in the correct order and initializes the system. +-- +-- Usage: +-- fileIn "D:/UGit/allTogether/aa_self_free/final_auto_animator/autoAnim_main.ms" +-- +-- Version: 2.0.0 +-- Author: Auto Animation Tool Team +-- ============================================================================ + +-- # region Version Info + +global AA_VERSION = "2.0.0" +global AA_BUILD_DATE = "2026-02-13" + +format "\n" +format "========================================\n" +format " Auto Animation Tool v%\n" AA_VERSION +format " Build Date: %\n" AA_BUILD_DATE +format "========================================\n" +format "\n" + +-- # endregion + +-- # region Get Script Directory + +-- Get the directory where this script is located +-- This allows us to load modules using relative paths +scriptPath = getSourceFileName() +scriptDir = getFilenamePath scriptPath + +format "[INFO] Script directory: %\n" scriptDir + +-- # endregion + +-- # region Load Modules + +format "[INFO] Loading modules...\n" + +-- Load constants first (no dependencies) +fileIn (scriptDir + "core\\constants.ms") + +-- Load utility modules (depend on constants) +fileIn (scriptDir + "utils\\naming.ms") +fileIn (scriptDir + "utils\\validation.ms") +fileIn (scriptDir + "utils\\startup_installer.ms") -- Startup script auto-installer + +-- Load core modules (depend on constants and utils) +fileIn (scriptDir + "core\\spline_curve_widget.ms") -- Spline weight curve creation +fileIn (scriptDir + "core\\spline_curve_sampler.ms") -- Spline X-coordinate sampling +fileIn (scriptDir + "core\\expression_builder.ms") +fileIn (scriptDir + "core\\hierarchy_manager.ms") +fileIn (scriptDir + "core\\curve_editor.ms") +fileIn (scriptDir + "core\\custom_attributes.ms") +fileIn (scriptDir + "core\\tool_functions.ms") +fileIn (scriptDir + "core\\tool_window.ms") -- Tool window UI (original code preserved) + +format "[INFO] All modules loaded successfully!\n" +format "\n" + +-- # endregion + +-- # region Module Status + +format "========================================\n" +format "Module Status:\n" +format "========================================\n" +format " [OK] core/constants.ms\n" +format " [OK] utils/naming.ms\n" +format " [OK] utils/validation.ms\n" +format " [OK] utils/startup_installer.ms\n" +format " [OK] core/spline_curve_widget.ms\n" +format " [OK] core/spline_curve_sampler.ms\n" +format " [OK] core/expression_builder.ms\n" +format " [OK] core/hierarchy_manager.ms\n" +format " [OK] core/curve_editor.ms\n" +format " [OK] core/custom_attributes.ms\n" +format " [OK] core/tool_functions.ms\n" +format " [OK] core/tool_window.ms\n" +format "========================================\n" +format "\n" + +-- # endregion + +-- # region Quick Test + +format "[INFO] Running quick module test...\n" +format "\n" + +-- Test constants +format "[TEST] Constants:\n" +format " AA_TWO_PI = %\n" AA_TWO_PI +format " AA_TIME_SCALE = %\n" AA_TIME_SCALE +format "\n" + +-- Test naming utilities +format "[TEST] Naming utilities:\n" +testName = AA_GenerateUniqueName "TestObject" "_Locator" +format " Generated name: %\n" testName +format "\n" + +-- Test validation +format "[TEST] Validation:\n" +testBones = selection as array +if testBones.count > 0 then ( + isValid = AA_ValidateBoneChain testBones + format " Bone chain valid: %\n" isValid + + if isValid then ( + percentages = AA_CalculateBonePercent testBones + format " Bone percentages: %\n" percentages + ) +) else ( + format " No bones selected for testing\n" +) +format "\n" + +format "========================================\n" +format "Auto Animation Tool v% Ready!\n" AA_VERSION +format "========================================\n" +format "\n" + +-- # endregion + +-- # region Startup Script Installation + +-- Ensure startup script is installed for scene reload support +AA_EnsureStartupInstalled scriptDir + +-- # endregion + diff --git a/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/constants.ms b/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/constants.ms new file mode 100644 index 0000000..5f13253 --- /dev/null +++ b/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/constants.ms @@ -0,0 +1,134 @@ +-- ============================================================================ +-- Auto Animation Tool - Constants Module +-- ============================================================================ +-- This module defines all global constants used throughout the auto animation system. +-- These constants replace magic numbers to improve code readability and maintainability. +-- +-- Version: 2.0.0 +-- Author: Auto Animation Tool Team +-- ============================================================================ + +-- # region Mathematical Constants + +-- TWO_PI: Full sine wave cycle (2 * PI = 6.28319) +-- Used in sine wave expression calculations for frequency conversion +global AA_TWO_PI = 6.28319 + +-- MAGIC_CONST: Bezier tangent calculation constant (0.25 * (1.0 - 0.25) = 0.1875) +-- Used in bezier curve tangent calculations for smooth interpolation +global AA_MAGIC_CONST = 0.1875 + +-- # endregion + +-- # region Time and Frequency Constants + +-- TIME_SCALE: Frame to frequency conversion factor (114.592) +-- This constant converts frame-based loop values to frequency values +-- Formula: frequency = TWO_PI / (loop / TIME_SCALE) +global AA_TIME_SCALE = 114.592 + +-- CURVE_TIME_OFFSET: Timeline offset for curve evaluation (-100000.0) +-- This large negative offset ensures curve evaluation doesn't conflict with actual timeline +-- Used when sampling bezier curves for amplitude modulation +global AA_CURVE_TIME_OFFSET = -100000.0 + +-- # endregion + +-- # region Layer Names + +-- Layer names for organizing autoAnim objects +global AA_LAYER_ROOT = "autoAnim" +global AA_LAYER_CONTROLLERS = "autoAnim_Controllers" +global AA_LAYER_CURVES = "autoAnim_WeightCurves" +global AA_LAYER_HELPERS = "autoAnim_Helpers" + +-- # endregion + +-- # region Grid Layout Settings + +-- X offset for stacking X/Y/Z grids horizontally within same group +-- Should be >= grid width (100) plus some padding +global AA_GRID_STACK_OFFSET = 110.0 + +-- Grid spacing between different bone chain groups +-- Should accommodate 3 panels (X/Y/Z) per group: 3 * AA_GRID_STACK_OFFSET + padding +global AA_GRID_SPACING = 350.0 + +-- Maximum columns before wrapping to next row +global AA_GRID_COLUMNS = 3 + +-- Starting position for the first grid +global AA_GRID_START_POS = [0.0, 0.0, 0.0] + +-- # endregion + +-- # region Layer Management Functions + +-- Get or create the root autoAnim layer +-- Returns the layer object +fn AA_GetOrCreateRootLayer = ( + local layerObj = layerManager.getLayerFromName AA_LAYER_ROOT + if layerObj == undefined do ( + layerObj = layerManager.newLayerFromName AA_LAYER_ROOT + ) + return layerObj +) + +-- Get or create a child layer under autoAnim root +-- Returns the layer object +fn AA_GetOrCreateChildLayer layerName = ( + -- Ensure root layer exists first + local rootLayer = AA_GetOrCreateRootLayer() + + local layerObj = layerManager.getLayerFromName layerName + if layerObj == undefined do ( + layerObj = layerManager.newLayerFromName layerName + ) + -- Set parent to root layer + layerObj.setParent rootLayer + return layerObj +) + +-- Add node to the Controllers layer (child of autoAnim) +fn AA_AddToControllersLayer nodeObj = ( + local layerObj = AA_GetOrCreateChildLayer AA_LAYER_CONTROLLERS + layerObj.addnode nodeObj +) + +-- Add node to the Weight Curves layer (child of autoAnim) +fn AA_AddToCurvesLayer nodeObj = ( + local layerObj = AA_GetOrCreateChildLayer AA_LAYER_CURVES + layerObj.addnode nodeObj +) + +-- Add node to the Helpers layer (child of autoAnim) +fn AA_AddToHelpersLayer nodeObj = ( + local layerObj = AA_GetOrCreateChildLayer AA_LAYER_HELPERS + layerObj.addnode nodeObj +) + +-- # endregion + +-- # region Module Info + +format "========================================\n" +format "Auto Animation Tool - Constants Module\n" +format "Version: 3.1.0\n" +format "========================================\n" +format "Loaded constants:\n" +format " AA_TWO_PI = %\n" AA_TWO_PI +format " AA_MAGIC_CONST = %\n" AA_MAGIC_CONST +format " AA_TIME_SCALE = %\n" AA_TIME_SCALE +format " AA_CURVE_TIME_OFFSET = %\n" AA_CURVE_TIME_OFFSET +format "Layer names:\n" +format " AA_LAYER_CONTROLLERS = %\n" AA_LAYER_CONTROLLERS +format " AA_LAYER_CURVES = %\n" AA_LAYER_CURVES +format " AA_LAYER_HELPERS = %\n" AA_LAYER_HELPERS +format "Grid layout:\n" +format " AA_GRID_SPACING = %\n" AA_GRID_SPACING +format " AA_GRID_COLUMNS = %\n" AA_GRID_COLUMNS +format " AA_GRID_STACK_OFFSET = %\n" AA_GRID_STACK_OFFSET +format "========================================\n" + +-- # endregion + diff --git a/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/curve_editor.ms b/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/curve_editor.ms new file mode 100644 index 0000000..256c68b --- /dev/null +++ b/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/curve_editor.ms @@ -0,0 +1,163 @@ +-- ============================================================================ +-- Auto Animation Tool - Curve Editor Module +-- ============================================================================ +-- This module handles bezier curve editing for amplitude modulation. +-- The curve is sampled at each bone's position to determine amplitude variation. +-- +-- Curve Structure: +-- - X axis: Bone position in chain (0-100) +-- - Y axis: Amplitude multiplier (0-100) +-- - Stored as bezier_float controller on Curve object +-- +-- IMPORTANT: This module contains the curve editing logic from the original script. +-- The UI components remain in core/custom_attributes.ms +-- +-- Version: 2.0.0 +-- Author: Auto Animation Tool Team +-- ============================================================================ + +-- # region Curve Initialization + +-- Initialize default bezier curve +-- Creates a curve with 2 points: (0,0) and (100,100) +-- +-- Parameters: +-- curveController: The bezier_float controller to initialize +-- +fn AA_InitializeDefaultCurve curveController = ( + + -- Delete existing keys + deleteKeys curveController #allKeys + + -- Add first key at time offset + addNewKey curveController AA_CURVE_TIME_OFFSET + KA = getKey curveController 1 + KA.value = 0.0 + KA.inTangentType = #smooth + KA.inTangentLength = 0.25 + KA.inTangent = AA_MAGIC_CONST + KA.outTangentType = #smooth + KA.outTangentLength = 0.25 + KA.outTangent = AA_MAGIC_CONST + + -- Add second key + addNewKey curveController (AA_CURVE_TIME_OFFSET + 100) + KB = getKey curveController 2 + KB.value = 100.0 + KB.inTangentType = #smooth + KB.inTangentLength = 0.25 + KB.inTangent = -AA_MAGIC_CONST + KB.outTangentType = #smooth + KB.outTangentLength = 0.25 + KB.outTangent = AA_MAGIC_CONST +) + +-- # endregion + +-- # region Curve Sampling + +-- Sample curve value at specific bone percentage +-- Used in expression to get amplitude modulation +-- +-- Parameters: +-- curveController: The bezier_float controller +-- bonePercent: Bone position in chain (0.0 to 1.0) +-- +-- Returns: +-- Float value from curve (typically 0-100) +-- +fn AA_SampleCurveAtPercent curveController bonePercent = ( + -- Convert bone percent (0-1) to curve time (0-100) + curveTime = bonePercent * 100.0 + + -- Sample at offset time + sampleTime = AA_CURVE_TIME_OFFSET + curveTime + + -- Get value from controller + curveValue = at time sampleTime curveController.value + + return curveValue +) + +-- # endregion + +-- # region Curve Manipulation + +-- Update bezier curve key +-- Modifies a key's position and tangents +-- +-- Parameters: +-- curveController: The bezier_float controller +-- keyIndex: Index of key to update (1-based) +-- xPos: X position (0-100) +-- yPos: Y position (0-100) +-- inTangent: Optional in tangent value +-- outTangent: Optional out tangent value +-- +fn AA_UpdateCurveKey curveController keyIndex xPos yPos inTangent:undefined outTangent:undefined = ( + + if keyIndex < 1 or keyIndex > curveController.keys.count then ( + return false + ) + + key = getKey curveController keyIndex + + -- Update key time (X position) + key.time = AA_CURVE_TIME_OFFSET + xPos + + -- Update key value (Y position) + key.value = yPos + + -- Update tangents if provided + if inTangent != undefined then ( + key.inTangent = inTangent + ) + + if outTangent != undefined then ( + key.outTangent = outTangent + ) + + return true +) + +-- Add new key to curve +-- Inserts a new key at specified position +-- +-- Parameters: +-- curveController: The bezier_float controller +-- xPos: X position (0-100) +-- yPos: Y position (0-100) +-- +-- Returns: +-- Index of newly created key +-- +fn AA_AddCurveKey curveController xPos yPos = ( + + timeValue = AA_CURVE_TIME_OFFSET + xPos + + addNewKey curveController timeValue + + -- Find the newly added key + for i = 1 to curveController.keys.count do ( + key = getKey curveController i + if key.time == timeValue then ( + key.value = yPos + key.inTangentType = #smooth + key.outTangentType = #smooth + return i + ) + ) + + return 0 +) + +-- # endregion + +-- # region Module Info + +format "========================================\n" +format "Curve Editor Module Loaded\n" +format "========================================\n" + +-- # endregion + diff --git a/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/custom_attributes.ms b/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/custom_attributes.ms new file mode 100644 index 0000000..fd786a1 --- /dev/null +++ b/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/custom_attributes.ms @@ -0,0 +1,324 @@ +-- ============================================================================ +-- Auto Animation Tool - Custom Attributes Module +-- ============================================================================ +-- This module contains the Custom Attributes definition for the auto animation system. +-- This includes all UI elements, parameters, and event handlers. +-- +-- Custom Attributes Structure: +-- - boneSineAniUsePos: Main parameters and UI +-- +-- Note: The original "Float Controller" dotnet PictureBox curve UI has been removed. +-- Weight curves now use Spline objects exclusively. +-- +-- Version: 3.0.0 +-- Author: Auto Animation Tool Team +-- ============================================================================ + +boneSineAniUsePos = attributes boneSineAniUsePos +attribid:#(0x764520ba, 0x2bbba0c9) +version:1 +( + -- # region Parameters + parameters boneSineAniUsePos rollout: boneSineAniUsePos + ( + range type:#float UI:range defult:0 + loop type:#float UI:loop defult:0 + + speedV type:#float UI:speedV defult:12.0 + offsetV type:#float UI:offsetV defult:4.0 + rangeAdd type:#float UI:rangeAdd defult:3 + + frameFix type:#float UI:frameFix defult:0.0 + + setsInfo type:#string default:"" + boneInfo type:#string default:"" + + -- Record bone chain length (count) + boneCount type:#integer defult:1 + -- Record bone chain length ratio + bonelongPercent type:#string default:"" + + -- Spline weight curve parameters + -- Reference to the Spline weight curve object + splineCurveNode type:#node + -- Weight curve mode: 2 = Spline Curve (default, UI Curve mode removed) + weightCurveMode type:#integer default:2 + + -- Twist control parameters + -- twistMode: 0=None, 1=FirstLast (spinner driven), 2=Individual (manual euler) + twistMode type:#integer default:1 + twistStart type:#float UI:twistStart default:0.0 + twistEnd type:#float UI:twistEnd default:0.0 + ) + -- # endregion + + -- # region On Create + on create do + ( + isLeaf = true + ) + -- # endregion + + -- # region Main Rollout + Rollout boneSineAniUsePos "自己动" + ( + group "摆动" + ( + spinner speedV "速度Z:" width:150 Height:16 Align:#Center Offset:[0,0] Type:#float Range:[-1000,1000,12.0] enabled:false + + button setLoopbt "按时间轴循环:" width:100 Height:20 Align:#Center Offset:[0,0] + spinner loop "循环:" width:150 Height:16 Align:#Center Offset:[0,0] Type:#float Range:[-10000,10000,0] scale:1 + + spinner range "幅度:" width:150 Height:16 Align:#Center Offset:[0,0] Type:#float Range:[-1000,1000,15.0] + spinner offsetV "滞后:" width:150 Height:16 Align:#Center Offset:[0,0] Type:#float Range:[-10000,10000,4.0] + spinner rangeAdd "增益:" width:150 Height:16 Align:#Center Offset:[0,0] Type:#float Range:[-1000,1000,3.0] + spinner frameFix "帧位:" width:150 Height:16 Align:#Center Offset:[0,0] Type:#float Range:[-1000,1000,0] + ) + + group "权重曲线" + ( + label lblSplineCurve "Spline曲线:" across:2 + button btnSelectSpline "选择" width:60 tooltip:"在场景中选择关联的Spline曲线" + ) + + -- Twist control spinners (only visible when twistMode == 1, FirstLast mode) + -- Using label + spinners for visibility control (group cannot be hidden) + group "twist控制" + ( + spinner twistStart "起始:" width:150 height:16 type:#float range:[-36000,36000,0] Align:#Center Offset:[0,0] + spinner twistEnd "结束:" width:150 height:16 type:#float range:[-36000,36000,0] Align:#Center Offset:[0,0] + ) + + group "塌陷" + ( + radioButtons rdoRangeType "" labels:#("Time Line","Range") default:1 columns:2 + label lbStart "Start :" across:2 enabled:false + spinner spnStart "" align:#left width:60 range:[-9999,9999,0] type:#integer enabled:false + label lbEnd "End :" across:2 enabled:false + spinner spnEnd "" align:#left width:60 range:[-9999,9999,0] type:#integer enabled:false + ) + + button resetToV "默认" tooltip: "左键恢复预设,右键对帧位" across:2 + button collapseToOri "塌陷↑" tooltip: "恢复成原始骨骼,选择需要塌陷的骨骼链" across:2 + button resetToOpen "开(?′ヮ′)?" tooltip: "打开表达式动画" across:2 + button resetToZero "?(ˊ?ˋ*)?关" tooltip: "关闭表达式动画" across:2 + + + label aboutsplide "~~~~~~~~~~~~~~" + label aboutYun "written by:东见云" + + on setLoopbt pressed do + ( + animationRange.end.frame - animationRange.start.frame + loop.value = animationRange.end.frame - animationRange.start.frame + ) + + -- Select the associated Spline curve in scene + on btnSelectSpline pressed do + ( + if splineCurveNode != undefined and isValidNode splineCurveNode do ( + select splineCurveNode + ) + ) + + + -- On rollout open + on boneSineAniUsePos open do + ( + --if animButtonState == true then (max tool animmode; set animate off) + + speedV.value = (AA_TWO_PI / (loop.value / AA_TIME_SCALE)) + if startTime == undefined then ( + global startTime + ) + startTime = (animationRange.start as string) as integer + if endTime == undefined then ( + global endTime + ) + endTime = (animationRange.end as string) as integer + + spnStart.value = startTime + spnEnd.value = endTime + + -- Show/hide twist controls based on twistMode + -- Only show when twistMode == 1 (FirstLast mode) + -- Note: group cannot be hidden, only hide spinners inside + local showTwist = (twistMode == 1) + twistStart.visible = showTwist + twistEnd.visible = showTwist + + if boneSineTimeRangeType != undefined then + ( + rdoRangeType.state = boneSineTimeRangeType + case boneSineTimeRangeType of ( + 1:( + lbStart.enabled = false + lbEnd.enabled = false + spnStart.enabled = false + spnEnd.enabled = false + ) + 2:( + lbStart.enabled = true + lbEnd.enabled = true + spnStart.enabled = true + spnEnd.enabled = true + ) + ) + ) + ) + + -- Speed change syncs with period + on loop changed val do ( + speedV.value = (AA_TWO_PI / (val / AA_TIME_SCALE)) + ) + + -- Collapse range toggle + on rdoRangeType changed val do ( + case val of ( + 1:( + lbStart.enabled = false + lbEnd.enabled = false + spnStart.enabled = false + spnEnd.enabled = false + ) + 2:( + lbStart.enabled = true + lbEnd.enabled = true + spnStart.enabled = true + spnEnd.enabled = true + ) + ) + if boneSineTimeRangeType == undefined then ( + global boneSineTimeRangeType + ) + boneSineTimeRangeType = val + ) + + -- Timeline control + on spnStart changed val do ( + if spnEnd.value < val then ( + spnEnd.value = val + ) + if startTime == undefined then ( + global startTime + ) + startTime = val + ) + + on spnEnd changed val do ( + if spnStart.value > val then ( + spnStart.value = val + ) + if endTime == undefined then ( + global endTime + ) + endTime = val + ) + -- # endregion + + -- # region Collapse Handler + on collapseToOri pressed do + ( + undo on + ( + tStart = (animationRange.start as string) as integer + if rdoRangeType.state == 2 then + (tStart = spnStart.value) + + tEnd = (animationRange.end as string) as integer + if rdoRangeType.state == 2 then + (tEnd = spnEnd.value) + + tNow = (currentTime as string) as integer + TimeLength = (tEnd - tStart) + 1 + frames = #() + for i = tStart to tEnd do append frames i + boneN = execute($.modifiers[1].boneInfo) + sel = #() + for i in boneN do (append sel (getnodebyname i)) + + if sel.count < 1 then (messageBox "骨骼重命名就会看到我喔~~~") else ( + TM_record = #() + -- Record transform matrices + for frame in frames do + ( + at time frame + ( + TM_buffer = #() + for obj in sel do (append TM_buffer obj.transform) + append TM_record TM_buffer + ) + ) + -- Restore controllers + for i in sel do ( + i.pos.controller = Position_XYZ() + i.rotation.controller = Euler_XYZ() + ) + -- Restore animation + animate on + ( + for f = 1 to frames.count do + ( + at time (frames[f] as time) + ( + for i = 1 to sel.count do (sel[i].transform = TM_record[f][i]) + ) + ) + print "KKKK" + ) + ) + try (delete $) catch(print "未选择控制") + ) + ) + -- # endregion + + -- # region Animation Toggle Handlers + -- Close animation + on resetToZero pressed do ( + with animate off ( + if speedV.value != 0 then + ( + buffer = #() + append buffer $.modifiers[1].speedV + $.modifiers[1].setsInfo = buffer as string + $.modifiers[1].speedV = 0 + redrawViews() + print "表达式动画已关闭" + ) + + $.modifiers[1].loop = AA_TIME_SCALE * (AA_TWO_PI / $.modifiers[1].speedV) + ) + ) + + -- Open animation + on resetToOpen pressed do ( + with animate off ( + try ( + bufferGet = execute($.modifiers[1].setsInfo) + $.modifiers[1].speedV = bufferGet[1] + + $.modifiers[1].loop = AA_TIME_SCALE * (AA_TWO_PI / $.modifiers[1].speedV) + + print "表达式动画已打开" + redrawViews() + ) catch() + ) + ) + + -- Preset + on resetToV pressed do ( + with animate off ( + redrawViews() + print "表达式动画已打开" + ) + ) + + -- Frame position + on resetToV rightclick do ( + frameFix.value = (animationRange.start.frame as integer) + ) + -- # endregion + ) + -- # endregion +) + diff --git a/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/expression_builder.ms b/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/expression_builder.ms new file mode 100644 index 0000000..f2dcc3b --- /dev/null +++ b/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/expression_builder.ms @@ -0,0 +1,174 @@ +-- ============================================================================ +-- Auto Animation Tool - Expression Builder Module +-- ============================================================================ +-- This module handles the creation of sine wave expressions for procedural animation. +-- It builds MaxScript float_script expressions that drive bone positions. +-- +-- Expression Formula: +-- curveVV = AA_SampleSplineAtX splineCurve (boneper * 100) +-- sin(frequency * (F + frameFix + phaseOffset)) * amplitude +-- +-- Where: +-- frequency = TWO_PI / (loop / TIME_SCALE) +-- phaseOffset = offsetV * boneper * -1 +-- amplitude = curveVV * rangeAdd * 0.01 + range +-- +-- Weight Curve: +-- Uses Spline object for weight sampling (replaces old dotnet PictureBox curve) +-- Fallback to curveVV = 100 if no Spline is available +-- +-- Version: 3.0.0 +-- Author: Auto Animation Tool Team +-- ============================================================================ + +-- # region Expression Building + +-- Build sine wave expression for a single bone +-- Creates a float_script controller with sine wave expression +-- +-- Parameters: +-- axisSet: Axis to animate (1=X, 2=Y, 3=Z) +-- ctrl_master: Master control object with Custom Attributes +-- boneTarget: Target bone to apply expression to +-- bonePercent: Bone's position in chain (0.0 to 1.0) +-- +-- Returns: +-- The created float_script controller +-- +fn AA_BuildSineExpression axisSet ctrl_master boneTarget bonePercent = ( + + -- Create position controller if needed + if classof(boneTarget.pos.controller) != position_list then ( + boneTarget.pos.controller = Position_XYZ() + boneTarget.pos.controller = position_list() + posPP = Position_XYZ() + boneTarget.pos.controller[1][axisSet].value = 0 + boneTarget.pos.controller.Available.controller = posPP + ) + + -- Create float_script controller for expression + posPPScript = float_script() + boneTarget.pos.controller[2][axisSet].controller = posPPScript + + -- Add references to master controller parameters + posPPScript.AddNode "this" ctrl_master + posPPScript.AddTarget "range" ctrl_master.modifiers[1].boneSineAniUsePos[#range] + posPPScript.AddTarget "loop" ctrl_master.modifiers[1].boneSineAniUsePos[#loop] + posPPScript.AddTarget "offsetV" ctrl_master.modifiers[1].boneSineAniUsePos[#offsetV] + posPPScript.AddTarget "rangeAdd" ctrl_master.modifiers[1].boneSineAniUsePos[#rangeAdd] + posPPScript.AddTarget "frameFix" ctrl_master.modifiers[1].boneSineAniUsePos[#frameFix] + + -- Add bone-specific constant + posPPScript.AddConstant "boneper" bonePercent + + -- Get Spline weight curve from attributes + local splineNode = undefined + local attrHolder = ctrl_master.modifiers[#Attribute_Holder] + if attrHolder != undefined do ( + if hasProperty attrHolder.boneSineAniUsePos #splineCurveNode do ( + splineNode = attrHolder.boneSineAniUsePos.splineCurveNode + ) + ) + + -- Build expression using Spline weight curve + posPPScript.script = "0" + + if splineNode != undefined and isValidNode splineNode then ( + -- Spline mode: use AA_SampleSplineAtX for weight lookup + posPPScript.AddNode "splineCurve" splineNode + posPPScript.script = + "curveVV = AA_SampleSplineAtX splineCurve (boneper * 100)" + "\n" + + "sin( (" + (AA_TWO_PI as string) + " / (loop / " + (AA_TIME_SCALE as string) + ")) * ((F) + frameFix + (offsetV*1 * boneper * -1))) * " + + "( curveVV*rangeAdd*0.01 + range)" + ) else ( + -- Fallback: no Spline available, use constant weight = 100 + posPPScript.script = + "curveVV = 100" + "\n" + + "sin( (" + (AA_TWO_PI as string) + " / (loop / " + (AA_TIME_SCALE as string) + ")) * ((F) + frameFix + (offsetV*1 * boneper * -1))) * " + + "( curveVV*rangeAdd*0.01 + range)" + ) + + return posPPScript +) + +-- Apply sine wave expressions to bone chain +-- Applies expressions to all bones in a chain for a specific axis +-- +-- Parameters: +-- axisSet: Axis to animate (1=X, 2=Y, 3=Z) +-- ctrl_master: Master control object +-- bones: Array of bone objects +-- percentValues: Array of bone percentages (0.0 to 1.0) +-- +fn AA_ApplyExpressionsToChain axisSet ctrl_master bones percentValues = ( + + if bones.count != percentValues.count then ( + messageBox "Error: Bone count doesn't match percentage count" title:"Expression Builder Error" + return false + ) + + for i = 1 to bones.count do ( + AA_BuildSineExpression axisSet ctrl_master bones[i] percentValues[i] + ) + + return true +) + +-- # endregion + +-- # region Utility Functions + +-- Format number to three digits with leading zeros +-- Used for consistent naming of helper objects +-- +-- Parameters: +-- num: Number to format +-- +-- Returns: +-- String with three digits (e.g., 1 -> "001", 42 -> "042") +-- +fn AA_FormatNumberToThreeDigits num = ( + numStr = num as string + while (numStr.count < 3) do ( + numStr = "0" + numStr + ) + return numStr +) + +-- Calculate frequency from loop value +-- Converts loop (frame count) to frequency for sine wave +-- +-- Parameters: +-- loopValue: Loop value in frames +-- +-- Returns: +-- Frequency value for sine wave calculation +-- +fn AA_CalculateFrequency loopValue = ( + return AA_TWO_PI / (loopValue / AA_TIME_SCALE) +) + +-- Calculate phase offset for bone +-- Determines phase offset based on bone position in chain +-- +-- Parameters: +-- bonePercent: Bone's position in chain (0.0 to 1.0) +-- offsetValue: Offset multiplier from UI +-- +-- Returns: +-- Phase offset value +-- +fn AA_CalculatePhaseOffset bonePercent offsetValue = ( + return offsetValue * bonePercent * -1.0 +) + +-- # endregion + +-- # region Module Info + +format "========================================\n" +format "Expression Builder Module Loaded\n" +format "========================================\n" + +-- # endregion + diff --git a/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/hierarchy_manager.ms b/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/hierarchy_manager.ms new file mode 100644 index 0000000..bd240e0 --- /dev/null +++ b/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/hierarchy_manager.ms @@ -0,0 +1,407 @@ +-- ============================================================================ +-- Auto Animation Tool - Hierarchy Manager Module +-- ============================================================================ +-- This module manages the 4-layer hierarchy system for auto animation. +-- +-- 4-Layer Hierarchy Structure: +-- Original Bone ← Wave Locator ← Master Locator ← Copy Bone +-- +-- Layer Explanation: +-- 1. Copy Bone (_Anim): Duplicate of original bone, receives final animation +-- 2. Master Locator (_Master): Links to Copy Bone, provides stable reference +-- 3. Wave Locator (_Wave): Child of Master, receives sine wave expression +-- 4. Original Bone: Constrained to Wave Locator for final animation +-- +-- IMPORTANT: This algorithm is COMPLETELY PRESERVED from the original script. +-- Only comments and organization have been improved. +-- +-- Version: 2.0.0 +-- Author: Auto Animation Tool Team +-- ============================================================================ + +-- # region Helper Object Creation + +-- Create or recreate a dummy/point helper object +-- If object with same name exists, it will be deleted and recreated +-- +-- Parameters: +-- objName: Name for the helper object +-- +-- Returns: +-- The created point helper object +-- +fn AA_CreateDummyHelper objName = ( + -- Check if object already exists and delete it + existingObj = getNodeByName objName + if existingObj != undefined then ( + delete existingObj + ) + + -- Create new point helper + dummyObj = point pos:[0,0,0] isSelected:off + dummyObj.name = objName + dummyObj.cameross = on + dummyObj.axistripod = off + + return dummyObj +) + +-- # endregion + +-- # region Constraint Functions + +-- Apply position constraint from objA to objB +-- Uses Position_Constraint with relative mode +-- +-- Parameters: +-- objA: Object to be constrained +-- objB: Target object +-- +fn AA_ApplyPositionConstraint objA objB = ( + objA.pos.controller = Position_XYZ() + objA.pos.controller = position_list() + posPP = Position_Constraint() + objA.pos.controller.Available.controller = posPP + posPP.appendTarget objB 100 + posPP.relative = true + objA.pos.controller.Available.controller = Position_XYZ() + objA.pos.controller.setActive 3 +) + +-- Apply rotation constraint from objA to objB +-- Uses Orientation_Constraint with relative mode +-- +-- Parameters: +-- objA: Object to be constrained +-- objB: Target object +-- +fn AA_ApplyRotationConstraint objA objB = ( + objA.rotation.controller = Euler_XYZ() + objA.rotation.controller = rotation_list() + rotPP = Orientation_Constraint() + objA.rotation.controller.Available.controller = rotPP + rotPP.appendTarget objB 100 + rotPP.relative = true + objA.rotation.controller.Available.controller = Local_Euler_XYZ() + objA.rotation.controller.setActive 3 +) + +-- # endregion + +-- # region 4-Layer Hierarchy Creation + +-- Create Wave Locator for a bone +-- Wave Locator is the layer that receives sine wave expressions +-- +-- Parameters: +-- copyBone: The copied bone (_Anim) +-- layerObj: Layer to add objects to +-- +-- Returns: +-- The created Wave Locator object +-- +fn AA_CreateWaveLocator copyBone layerObj = ( + nameWave = copyBone.name + "_Wave" + + -- Create Wave Locator helper + AA_CreateDummyHelper nameWave + waveObj = getNodeByName nameWave + + -- Reset transform in parent space + in coordsys parent waveObj.rotation = (quat 0 0 0 0) + in coordsys parent waveObj.pos = [0,0,0] + waveObj.axistripod = on + waveObj.cross = off + + -- Add to layer + if layerObj != undefined then ( + layerObj.addnode waveObj + ) + + return waveObj +) + +-- Create Master Locator for a bone +-- Master Locator links to the Copy Bone and parents the Wave Locator +-- +-- Parameters: +-- copyBone: The copied bone (_Anim) +-- layerObj: Layer to add objects to +-- +-- Returns: +-- The created Master Locator object +-- +fn AA_CreateMasterLocator copyBone layerObj = ( + nameMaster = copyBone.name + "_Master" + + -- Create Master Locator helper + AA_CreateDummyHelper nameMaster + masterObj = getNodeByName nameMaster + + -- Match transform to copy bone + masterObj.transform = copyBone.transform + + -- Link to copy bone using Link_Constraint + masterObj.Transform.controller = Link_Constraint() + masterObj.transform.controller.AddTarget copyBone ((sliderTime as string) as integer) + + -- Add to layer + if layerObj != undefined then ( + layerObj.addnode masterObj + ) + + -- Hide Master Locator (Wave remains visible for user interaction) + masterObj.isHidden = true + + return masterObj +) + +-- Create complete 4-layer hierarchy for a single bone +-- This is the main function that creates all layers and sets up constraints +-- +-- Parameters: +-- originalBone: The original bone to animate +-- copyBone: The copied bone (_Anim) +-- layerObj: Layer to add objects to (optional) +-- +-- Returns: +-- Array: #(masterLocator, waveLocator) +-- +fn AA_Create4LayerHierarchy originalBone copyBone layerObj = ( + + animate off ( + -- Create Master Locator (Layer 2) + masterObj = AA_CreateMasterLocator copyBone layerObj + + -- Create Wave Locator (Layer 3) + waveObj = AA_CreateWaveLocator copyBone layerObj + + -- Parent Wave to Master + waveObj.parent = masterObj + + -- Constrain original bone to Wave Locator (Layer 4 → Layer 3) + -- This is where the final animation is applied + originalBone.Transform.controller = prs() + AA_ApplyPositionConstraint originalBone waveObj + AA_ApplyRotationConstraint originalBone waveObj + ) + + return #(masterObj, waveObj) +) + +-- [DEPRECATED] Apply LookAt constraints to wave locators +-- NOTE: This function is kept for backward compatibility but replaced by AA_ApplyQuaternionAimConstraints +-- LookAt_Constraint causes gimbal lock flipping when _Anim bones are rotated +-- +-- Parameters: +-- waveLocators: Array of wave locator objects (sorted) +-- targetAxis: LookAt target axis (default 0) +-- targetAxisFlip: Whether to flip target axis (default false) +-- +fn AA_ApplyLookAtConstraints waveLocators targetAxis:0 targetAxisFlip:false = ( + + for s = 1 to waveLocators.count do ( + if s > 1 do ( + local t_b = waveLocators[s] + + -- Setup rotation controller + t_b.rotation.controller = Euler_XYZ() + t_b.rotation.controller = rotation_list() + t_b.rotation.controller.Available.controller = LookAt_Constraint() + + lst = t_b.rotation.controller + lst.SetActive 2 + + -- Get previous locator as target + try ( + lookat_target = waveLocators[(s - 1)] + ) catch () + + if lookat_target != undefined do ( + lst.LookAt_Constraint.controller.appendtarget lookat_target 50 + lst.LookAt_Constraint.controller.lookat_vector_length = 0 + lst.LookAt_Constraint.controller.upnode_world = true + lst.LookAt_Constraint.controller.target_axis = targetAxis + lst.LookAt_Constraint.controller.target_axisFlip = targetAxisFlip + ) + ) + ) +) + +-- # endregion + +-- # region Quaternion Aim Constraints + +-- Initialize position_list controller for a Wave locator +-- This must be called BEFORE applying quaternion aim constraints +-- to ensure the position controller can receive wave expressions later +-- +-- IMPORTANT: This must match the structure expected by mackExpAnim in tool_functions.ms +-- Structure: position_list with [1]=base Position_XYZ, [2]=available slot for expressions +-- +-- Parameters: +-- waveObj: Wave locator object +-- +fn AA_InitWavePositionController waveObj = ( + if classof(waveObj.pos.controller) != position_list then ( + -- Store current local position + local localPos = in coordsys parent waveObj.pos + + -- Setup position_list structure (matching tool_functions.ms mackExpAnim expectations) + waveObj.pos.controller = Position_XYZ() + waveObj.pos.controller = position_list() + + -- First slot [1]: base position + waveObj.pos.controller[1].controller = Position_XYZ() + + -- Second slot [2]: available for expressions (added via .Available.controller) + local posPP = Position_XYZ() + waveObj.pos.controller.Available.controller = posPP + + -- Restore local position in first slot + in coordsys parent waveObj.pos = localPos + ) +) + +-- Apply Quaternion-based aim constraints to wave locators +-- This replaces LookAt_Constraint to avoid gimbal lock flipping +-- +-- Uses rotation_script with quaternion calculation: +-- - Wave[n] looks at Wave[n+1] (forward direction) +-- - Position calculated from Master (fixed) + Wave local offset (independent of rotation) +-- - This avoids circular dependency errors +-- +-- Parameters: +-- waveLocators: Array of wave locator objects (sorted from root to tip) +-- masterLocators: Array of master locator objects (same order as waveLocators) +-- rootParent: Reference node for rotation inheritance (usually first _Anim or world) +-- twistMode: Twist control mode (0=None, 1=FirstLast, 2=Individual). Default 1. +-- mainCtrl: Main controller object with twist parameters (required for FirstLast mode) +-- bonelongPercent: Array of bone length percentages for twist distribution +-- +fn AA_ApplyQuaternionAimConstraints waveLocators masterLocators rootParent twistMode:1 mainCtrl:undefined bonelongPercent:#() = ( + + -- Validate input arrays + if waveLocators.count != masterLocators.count do ( + format "ERROR: waveLocators and masterLocators must have same length\n" + return false + ) + + if waveLocators.count < 2 do ( + format "WARNING: Need at least 2 waves for aim constraints\n" + return true + ) + + -- Initialize position_list for all waves BEFORE applying rotation scripts + -- This ensures pos.controller is ready for dynamic wave expressions + for i = 1 to waveLocators.count do ( + AA_InitWavePositionController waveLocators[i] + ) + + -- Build quaternion script expression string + -- CRITICAL: Must reference Wave position controllers to respond to wave expressions + -- Wave expressions are added AFTER quaternion constraints, so we need dynamic updates + -- + -- IMPORTANT: Use axis-angle quaternion method (from ChainsTools.ms) + -- Calculate in parent LOCAL space to avoid Z-axis flipping issues + -- Reference vector is [1,0,0] (X-axis) because bone chains grow along X-axis + local scriptString = \ + "-- Get Wave world positions (Master world pos + Wave local offset)\n" + \ + "posThis = thisMaster.transform.pos + (thisLocalPos.value * thisMaster.transform.rotation)\n" + \ + "posTarget = targetMaster.transform.pos + (targetLocalPos.value * targetMaster.transform.rotation)\n" + \ + "-- Convert to parent local space\n" + \ + "posThisLocal = posThis * inverse thisMaster.transform\n" + \ + "posTargetLocal = posTarget * inverse thisMaster.transform\n" + \ + "-- Calculate aim vector in LOCAL space\n" + \ + "vector = normalize (posTargetLocal - posThisLocal)\n" + \ + "-- Axis-angle quaternion (reference X-axis [1,0,0] for stability)\n" + \ + "axis = normalize (cross vector [1,0,0])\n" + \ + "angle = acos vector.x\n" + \ + "-- Return local rotation (Wave is child of thisMaster)\n" + \ + "(quat angle axis)" + + -- Apply rotation_script to each wave (except first one, which is at the end of array) + -- NOTE: waveLocators array is in REVERSE order (tip to root) + -- So waveLocators[1] is the last bone, waveLocators[count] is the first bone + -- Each wave should look at the PREVIOUS wave in the array (which is the NEXT bone in the chain) + for i = 2 to waveLocators.count do ( + local thisWave = waveLocators[i] + local prevWave = waveLocators[i - 1] -- Previous in array = next in chain + local thisMaster = masterLocators[i] + local prevMaster = masterLocators[i - 1] + + -- Create rotation_script controller for quaternion aim + local quatCtrl = rotation_script() + + -- Add references: + -- addNode: for Master objects (world position via .transform.pos) + -- addObject: for Wave position_list (local offset via .value, updates with wave expressions) + quatCtrl.addNode "thisMaster" thisMaster + quatCtrl.addNode "targetMaster" prevMaster + quatCtrl.addObject "thisLocalPos" thisWave.pos.controller + quatCtrl.addObject "targetLocalPos" prevWave.pos.controller + quatCtrl.script = scriptString + + -- Apply rotation structure based on twistMode + if twistMode == 0 then ( + -- No twist: direct rotation_script + thisWave.rotation.controller = quatCtrl + ) else ( + -- Twist enabled: use rotation_list + -- [1] = quaternion aim, [2] = twist layer + thisWave.rotation.controller = rotation_list() + thisWave.rotation.controller[1].controller = quatCtrl + + if twistMode == 1 then ( + -- FirstLast mode: euler_xyz with X driven by float_script + local twistEuler = euler_xyz() + thisWave.rotation.controller.Available.controller = twistEuler + + -- Create float_script to drive X rotation based on bonelongPercent + local twistScript = float_script() + twistEuler.x_rotation.controller = twistScript + + -- Get bone percentage for this wave (arrays are reversed, need to map correctly) + -- i goes from 2 to count, map to bonelongPercent index + -- bonelongPercent is in normal order [0, ..., 1.0] + -- waveLocators[count] = first bone (index 1 in bonelongPercent) + -- waveLocators[1] = last bone (index count in bonelongPercent) + local boneIdx = waveLocators.count - i + 1 + local bonePct = 0.0 + if bonelongPercent.count >= boneIdx and boneIdx > 0 do ( + bonePct = bonelongPercent[boneIdx] + ) + + -- Add references to main controller's twist parameters + if mainCtrl != undefined and mainCtrl.modifiers.count > 0 do ( + twistScript.addTarget "twistStart" mainCtrl.modifiers[1].boneSineAniUsePos[#twistStart] + twistScript.addTarget "twistEnd" mainCtrl.modifiers[1].boneSineAniUsePos[#twistEnd] + ) + twistScript.addConstant "bonePct" bonePct + + -- Twist distribution formula: twistStart + (twistEnd - twistStart) * bonePct + -- Convert degrees to radians + twistScript.script = "degtorad (twistStart + (twistEnd - twistStart) * bonePct)" + ) else if twistMode == 2 then ( + -- Individual mode: euler_xyz for manual adjustment + local twistEuler = euler_xyz() + thisWave.rotation.controller.Available.controller = twistEuler + -- Set Euler XYZ as active so user can directly rotate + thisWave.rotation.controller.setActive 2 + ) + ) + ) + + return true +) + +-- # endregion + +-- # region Module Info + +format "========================================\n" +format "Hierarchy Manager Module Loaded\n" +format "========================================\n" + +-- # endregion + diff --git a/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/spline_curve_sampler.ms b/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/spline_curve_sampler.ms new file mode 100644 index 0000000..a415dce --- /dev/null +++ b/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/spline_curve_sampler.ms @@ -0,0 +1,317 @@ +-- ============================================================================ +-- Auto Animation Tool - Spline Curve Sampler Module +-- ============================================================================ +-- This module handles sampling of Spline-based weight curves. +-- Given an X coordinate, returns the corresponding Y value from the curve. +-- +-- Sampling Method: +-- - Uses interpCurve3D to sample points along the curve +-- - Binary search for monotonic curves (X always increasing) +-- - Linear search fallback for non-monotonic curves +-- - Caches results to avoid redundant calculations +-- +-- Version: 2.0.0 +-- Author: Auto Animation Tool Team +-- ============================================================================ + +-- # region Global Variables + +-- Global cache container +global AA_SplineSampleCache = #() + +-- Default number of sample points for curve interpolation +global AA_DEFAULT_SAMPLE_COUNT = 100 + +-- Forward declarations for global functions +global AA_ComputeKnotHash +global AA_FindCache +global AA_RemoveCache +global AA_BuildSampleCache +global AA_BinarySearchY +global AA_LinearSearchY +global AA_GetCachedSample +global AA_SampleSplineAtX + +-- # endregion + +-- # region Cache Structure + +-- Compute hash from knot positions and tangent vectors for change detection +fn AA_ComputeKnotHash splineObj = ( + if splineObj == undefined do return 0.0 + if not isValidNode splineObj do return 0.0 + if numSplines splineObj < 1 do return 0.0 + + local hashVal = 0.0 + local knotCount = numKnots splineObj 1 + for i = 1 to knotCount do ( + local pt = getKnotPoint splineObj 1 i + local inV = getInVec splineObj 1 i + local outV = getOutVec splineObj 1 i + hashVal += pt.x * i + pt.y * (i * 2) + pt.z * (i * 3) + hashVal += inV.x * (i * 0.1) + inV.y * (i * 0.2) + hashVal += outV.x * (i * 0.3) + outV.y * (i * 0.4) + ) + return hashVal +) + +-- Cache entry structure for storing sampled curve data +struct AA_CacheEntry ( + splineObj, -- Reference to the Spline object + timeStamp, -- Time when cache was built + knotCount, -- Number of knots for modification detection + knotHash, -- Hash from knot positions + samplePoints, -- Array of [x, y] sample points + minX, -- Minimum X value in curve + maxX, -- Maximum X value in curve + isMonotonic, -- Whether X values are monotonically increasing + + -- Check if cache is still valid + fn isValid checkTime = ( + if this.splineObj == undefined do return false + if not isValidNode this.splineObj do return false + + local currentKnotCount = numKnots this.splineObj 1 + local currentHash = AA_ComputeKnotHash this.splineObj + local hashMatch = this.knotCount == currentKnotCount and this.knotHash == currentHash + local timeMatch = this.timeStamp == checkTime + + return (hashMatch and timeMatch) + ) +) + +-- # endregion + +-- # region Cache Management + +-- Find cache entry for a Spline object +fn AA_FindCache splineObj = ( + for cache in AA_SplineSampleCache do ( + if cache.splineObj == splineObj do return cache + ) + return undefined +) + +-- Remove cache entry for a Spline object +fn AA_RemoveCache splineObj = ( + local newCache = #() + for cache in AA_SplineSampleCache do ( + if cache.splineObj != splineObj do append newCache cache + ) + AA_SplineSampleCache = newCache +) + +-- # endregion + +-- # region Cache Building + +-- Build sample cache for a Spline curve +-- Pre-samples the curve at regular intervals +-- +-- Parameters: +-- splineObj: The SplineShape object to sample +-- numSamples: Number of sample points (default: AA_DEFAULT_SAMPLE_COUNT) +-- +-- Returns: +-- AA_CacheEntry with sampled data +-- +fn AA_BuildSampleCache splineObj numSamples:AA_DEFAULT_SAMPLE_COUNT = ( + + local cacheTime = currentTime + local samplePoints = #() + + -- Get knot info for cache validation + local knotCount = numKnots splineObj 1 + local knotHash = AA_ComputeKnotHash splineObj + + -- Sample curve directly (fast, no copy needed) + for i = 0 to numSamples do ( + local t = i as float / numSamples + local worldPt = interpCurve3D splineObj 1 t + local localPt = worldPt * (inverse splineObj.transform) + append samplePoints [localPt.x, localPt.y] + ) + + -- Determine if curve is monotonic (X always increasing) + local isMonotonic = true + local prevX = samplePoints[1][1] + for i = 2 to samplePoints.count do ( + if samplePoints[i][1] <= prevX do ( + isMonotonic = false + exit + ) + prevX = samplePoints[i][1] + ) + + -- Find min/max X values + local minX = samplePoints[1][1] + local maxX = samplePoints[1][1] + for pt in samplePoints do ( + if pt[1] < minX do minX = pt[1] + if pt[1] > maxX do maxX = pt[1] + ) + + -- Create cache entry + local cacheEntry = AA_CacheEntry \ + splineObj:splineObj \ + timeStamp:cacheTime \ + knotCount:knotCount \ + knotHash:knotHash \ + samplePoints:samplePoints \ + minX:minX \ + maxX:maxX \ + isMonotonic:isMonotonic + + -- Update global cache + AA_RemoveCache splineObj + append AA_SplineSampleCache cacheEntry + + return cacheEntry +) + +-- # endregion + +-- # region Sampling Functions + +-- Binary search for Y value at given X (for monotonic curves) +-- +-- Parameters: +-- samplePoints: Array of [x, y] points +-- targetX: The X coordinate to find +-- +-- Returns: +-- Interpolated Y value +-- +fn AA_BinarySearchY samplePoints targetX = ( + local low = 1 + local high = samplePoints.count + + -- Binary search to find bracketing points + while (high - low) > 1 do ( + local mid = (low + high) / 2 + if samplePoints[mid][1] <= targetX then + low = mid + else + high = mid + ) + + -- Linear interpolation between bracketing points + local x1 = samplePoints[low][1] + local y1 = samplePoints[low][2] + local x2 = samplePoints[high][1] + local y2 = samplePoints[high][2] + + if (x2 - x1) < 0.0001 do return y1 + + local t = (targetX - x1) / (x2 - x1) + return y1 + t * (y2 - y1) +) + +-- Linear search for Y value at given X (for non-monotonic curves) +-- Returns the first intersection found +-- +-- Parameters: +-- samplePoints: Array of [x, y] points +-- targetX: The X coordinate to find +-- +-- Returns: +-- Y value at closest X match +-- +fn AA_LinearSearchY samplePoints targetX = ( + local closestDist = 1e9 + local closestY = 0.0 + + for i = 1 to samplePoints.count - 1 do ( + local x1 = samplePoints[i][1] + local x2 = samplePoints[i+1][1] + + -- Check if targetX is between these two points + if (targetX >= x1 and targetX <= x2) or (targetX >= x2 and targetX <= x1) do ( + local y1 = samplePoints[i][2] + local y2 = samplePoints[i+1][2] + + local range = x2 - x1 + if abs(range) < 0.0001 do ( + return y1 + ) + + local t = (targetX - x1) / range + return y1 + t * (y2 - y1) + ) + ) + + -- Fallback: find closest point + for pt in samplePoints do ( + local dist = abs(pt[1] - targetX) + if dist < closestDist do ( + closestDist = dist + closestY = pt[2] + ) + ) + + return closestY +) + +-- Get cached sample or rebuild cache if needed +-- +-- Parameters: +-- splineObj: The SplineShape object +-- +-- Returns: +-- AA_CacheEntry (valid cache) +-- +fn AA_GetCachedSample splineObj = ( + local checkTime = currentTime + local cache = AA_FindCache splineObj + + if cache == undefined or not cache.isValid checkTime do ( + cache = AA_BuildSampleCache splineObj + ) + + return cache +) + +-- Sample Spline curve at given X coordinate +-- Main sampling function - returns Y value for given X +-- +-- Parameters: +-- splineObj: The SplineShape object to sample +-- xValue: The X coordinate to sample at (typically 0-100) +-- +-- Returns: +-- Y value from the curve at the given X position +-- +fn AA_SampleSplineAtX splineObj xValue = ( + + if splineObj == undefined do return 0.0 + if not isValidNode splineObj do return 0.0 + + -- Get or build cache + local cache = AA_GetCachedSample splineObj + + -- Clamp X to curve range + local clampedX = xValue + if clampedX < cache.minX do clampedX = cache.minX + if clampedX > cache.maxX do clampedX = cache.maxX + + -- Use appropriate search algorithm + local yValue = 0.0 + if cache.isMonotonic then ( + yValue = AA_BinarySearchY cache.samplePoints clampedX + ) else ( + yValue = AA_LinearSearchY cache.samplePoints clampedX + ) + + return yValue +) + +-- # endregion + +-- # region Module Info + +format "========================================\n" +format "Spline Curve Sampler Module Loaded\n" +format "========================================\n" + +-- # endregion + diff --git a/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/spline_curve_widget.ms b/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/spline_curve_widget.ms new file mode 100644 index 0000000..ffb9670 --- /dev/null +++ b/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/spline_curve_widget.ms @@ -0,0 +1,684 @@ +-- ============================================================================ +-- Auto Animation Tool - Spline Curve Widget Module +-- ============================================================================ +-- This module handles creation and management of Spline-based weight curves. +-- The Spline curve allows animated weight control that changes over time. +-- +-- Spline Structure: +-- - X axis: Bone position in chain (0-100) +-- - Y axis: Weight multiplier (0-100) +-- - Z axis: Ignored (soft constraint) +-- +-- Features: +-- - Create weight curve Spline with reference plane +-- - Support vertex editing (add/delete/move) +-- - Support modifiers (FFD, Noise, etc.) +-- - Optional follow mode for bone chain +-- +-- Version: 2.0.0 +-- Author: Auto Animation Tool Team +-- ============================================================================ + +-- # region Global Variables + +-- Counter for unique naming +global AA_WeightCurveCounter = 0 + +-- Forward declarations for global functions +global AA_CreateWeightCurveSpline +global AA_CreateReferencePlane +global AA_GridConfig +global AA_CreateGridLines +global AA_CreateXAxis +global AA_CreateYAxis +global AA_CreateOriginMarker +global AA_CreateBoundary +global AA_CreateCoordinateLabels +global AA_CreateSnapPoints +global AA_GetOrCreateLayer +global AA_MoveToLayer +global AA_FreezeHierarchy + +-- Grid layout and grouping functions +global AA_FindExistingGrids +global AA_FindExistingGridGroups +global AA_CheckGridCollision +global AA_CalculateNextGridPosition +global AA_SetGridOrientation +global AA_CreateGridGroup +global AA_StackGridInGroup + +-- # endregion + +-- # region Grid Configuration + +struct AA_GridConfig ( + -- Grid settings + minX = 0.0, + maxX = 100.0, + minY = 0.0, + maxY = 100.0, + + -- Grid density + majorStep = 20.0, -- Major grid lines every 20 units + minorStep = 10.0, -- Minor grid lines every 10 units + + -- Colors + majorGridColor = color 100 100 100, -- Darker gray for major lines + minorGridColor = color 60 60 60, -- Lighter gray for minor lines + xAxisColor = color 255 50 50, -- Red X-axis + yAxisColor = color 50 255 50, -- Green Y-axis + labelColor = color 180 180 180, -- Light gray for labels + originColor = color 255 255 50, -- Yellow origin marker + snapPointColor = color 128 128 128, -- Gray for snap points + + -- Z offsets to avoid overlap + gridZ = -0.1, + axisZ = 0.0, + labelZ = 0.1, + snapPointZ = 0.0, + + -- Label settings + labelSize = 4.0, + labelOffset = 6.0, -- Distance from axis + + -- Snap point settings + snapPointSize = 1.5, -- Size of snap point helpers + + -- Display options + showMinorGrid = true, + showLabels = true, + showOriginMarker = true, + showBoundary = true, + showSnapPoints = true, -- Show points at grid intersections for snapping + + -- Layer settings + layerName = "autoAnim_Helpers", + freezeReferences = true -- Freeze reference objects to prevent selection +) + +-- # endregion + +-- # region Spline Creation + +-- Create a weight curve Spline +-- Creates a SplineShape with default range [0,0] to [100,100] +-- +-- Parameters: +-- startX: Start X coordinate (default: 0) +-- startY: Start Y coordinate (default: 0) +-- endX: End X coordinate (default: 100) +-- endY: End Y coordinate (default: 100) +-- pos: Position in world space (default: [0,0,0]) +-- +-- Returns: +-- The created SplineShape object +-- +fn AA_CreateWeightCurveSpline startX:0.0 startY:0.0 endX:100.0 endY:100.0 pos:[0,0,0] = ( + + -- Increment counter for unique naming + AA_WeightCurveCounter += 1 + local curveName = "AA_WeightCurve_" + (formattedPrint AA_WeightCurveCounter format:"03d") + + -- Create SplineShape + local splineObj = SplineShape name:curveName pos:pos + + -- Calculate middle point + local midX = (startX + endX) / 2.0 + local midY = (startY + endY) / 2.0 + + -- Add first spline with three knots (start, middle, end) + -- Create as straight line - all knots use #bezier type for collinear handles + addNewSpline splineObj + addKnot splineObj 1 #smooth #curve [startX, startY, 0] + addKnot splineObj 1 #smooth #curve [midX, midY, 0] + addKnot splineObj 1 #smooth #curve [endX, endY, 0] + updateShape splineObj + + -- Convert all knots to #bezier type for collinear handles + setKnotType splineObj 1 1 #bezier + setKnotType splineObj 1 2 #bezier + setKnotType splineObj 1 3 #bezier + updateShape splineObj + + -- Calculate direction vector from start to end + local dirX = endX - startX + local dirY = endY - startY + local totalLen = sqrt(dirX * dirX + dirY * dirY) + + -- Normalize direction + if totalLen > 0 then ( + dirX = dirX / totalLen + dirY = dirY / totalLen + ) + + -- Handle length (1/6 of total distance for smooth straight line) + local handleLen = totalLen / 6.0 + + -- Start point: handles along the line direction + -- InVec points backward (opposite direction), OutVec points forward (toward end) + setInVec splineObj 1 1 [startX - dirX * handleLen, startY - dirY * handleLen, 0] + setOutVec splineObj 1 1 [startX + dirX * handleLen, startY + dirY * handleLen, 0] + + -- Middle point: handles along the line direction + -- InVec points toward start, OutVec points toward end + setInVec splineObj 1 2 [midX - dirX * handleLen, midY - dirY * handleLen, 0] + setOutVec splineObj 1 2 [midX + dirX * handleLen, midY + dirY * handleLen, 0] + + -- End point: handles along the line direction + -- InVec points backward (toward start), OutVec points forward (opposite direction) + setInVec splineObj 1 3 [endX - dirX * handleLen, endY - dirY * handleLen, 0] + setOutVec splineObj 1 3 [endX + dirX * handleLen, endY + dirY * handleLen, 0] + + updateShape splineObj + + -- Keep pivot at default (0,0,0) - do not modify + + -- Set display properties + splineObj.wireColor = color 255 128 0 -- Orange color for visibility + splineObj.renderable = false + splineObj.displayRenderMesh = false + + return splineObj +) + +-- # endregion + +-- # region Grid Line Creation + +-- Create spline-based grid lines (transparent, never blocks other objects) +fn AA_CreateGridLines config baseName:"AA_RefGrid" = ( + local gridObj = SplineShape name:(baseName + "_Lines") pos:[0,0,0] + + local splineIdx = 0 + + -- Create vertical lines (along Y direction) + for x = config.minX to config.maxX by config.minorStep do ( + local isMajor = (mod (x - config.minX) config.majorStep) == 0 + if config.showMinorGrid or isMajor then ( + addNewSpline gridObj + splineIdx += 1 + addKnot gridObj splineIdx #corner #line [x, config.minY, config.gridZ] + addKnot gridObj splineIdx #corner #line [x, config.maxY, config.gridZ] + ) + ) + + -- Create horizontal lines (along X direction) + for y = config.minY to config.maxY by config.minorStep do ( + local isMajor = (mod (y - config.minY) config.majorStep) == 0 + if config.showMinorGrid or isMajor then ( + addNewSpline gridObj + splineIdx += 1 + addKnot gridObj splineIdx #corner #line [config.minX, y, config.gridZ] + addKnot gridObj splineIdx #corner #line [config.maxX, y, config.gridZ] + ) + ) + + updateShape gridObj + gridObj.wireColor = config.majorGridColor + gridObj.renderable = false + + return gridObj +) + +-- Create colored X-axis +fn AA_CreateXAxis config baseName:"AA_RefGrid" = ( + local xAxisObj = SplineShape name:(baseName + "_XAxis") pos:[0,0,0] + addNewSpline xAxisObj + addKnot xAxisObj 1 #corner #line [config.minX, 0, config.axisZ] + addKnot xAxisObj 1 #corner #line [config.maxX, 0, config.axisZ] + updateShape xAxisObj + xAxisObj.wireColor = config.xAxisColor + xAxisObj.renderable = false + return xAxisObj +) + +-- Create colored Y-axis +fn AA_CreateYAxis config baseName:"AA_RefGrid" = ( + local yAxisObj = SplineShape name:(baseName + "_YAxis") pos:[0,0,0] + addNewSpline yAxisObj + addKnot yAxisObj 1 #corner #line [0, config.minY, config.axisZ] + addKnot yAxisObj 1 #corner #line [0, config.maxY, config.axisZ] + updateShape yAxisObj + yAxisObj.wireColor = config.yAxisColor + yAxisObj.renderable = false + return yAxisObj +) + +-- Create origin marker (small cross at 0,0) +fn AA_CreateOriginMarker config baseName:"AA_RefGrid" = ( + local markerSize = config.majorStep / 4.0 + local markerObj = SplineShape name:(baseName + "_Origin") pos:[0,0,0] + + -- Horizontal line of cross + addNewSpline markerObj + addKnot markerObj 1 #corner #line [-markerSize, 0, config.axisZ + 0.01] + addKnot markerObj 1 #corner #line [markerSize, 0, config.axisZ + 0.01] + + -- Vertical line of cross + addNewSpline markerObj + addKnot markerObj 2 #corner #line [0, -markerSize, config.axisZ + 0.01] + addKnot markerObj 2 #corner #line [0, markerSize, config.axisZ + 0.01] + + updateShape markerObj + markerObj.wireColor = config.originColor + markerObj.renderable = false + return markerObj +) + +-- Create boundary box +fn AA_CreateBoundary config baseName:"AA_RefGrid" = ( + local boundObj = SplineShape name:(baseName + "_Boundary") pos:[0,0,0] + addNewSpline boundObj + addKnot boundObj 1 #corner #line [config.minX, config.minY, config.gridZ] + addKnot boundObj 1 #corner #line [config.maxX, config.minY, config.gridZ] + addKnot boundObj 1 #corner #line [config.maxX, config.maxY, config.gridZ] + addKnot boundObj 1 #corner #line [config.minX, config.maxY, config.gridZ] + close boundObj 1 + + updateShape boundObj + boundObj.wireColor = color 150 150 150 + boundObj.renderable = false + return boundObj +) + +-- # endregion + +-- # region Coordinate Labels Creation + +-- Create Text labels for coordinates +fn AA_CreateCoordinateLabels config baseName:"AA_RefGrid" = ( + local labelGroup = Dummy name:(baseName + "_Labels") boxSize:[1,1,1] + labelGroup.pos = [0,0,0] + + -- X-axis labels (below the axis) + for x = config.minX to config.maxX by config.majorStep do ( + local labelText = (x as integer) as string + local txt = Text text:labelText size:config.labelSize \ + pos:[x, -config.labelOffset, config.labelZ] \ + wireColor:config.labelColor + txt.renderable = false + txt.parent = labelGroup + ) + + -- Y-axis labels (left of the axis) + for y = config.minY to config.maxY by config.majorStep do ( + -- Skip 0 to avoid overlap with X-axis label + if y > 0 then ( + local labelText = (y as integer) as string + local txt = Text text:labelText size:config.labelSize \ + pos:[-config.labelOffset, y, config.labelZ] \ + wireColor:config.labelColor + txt.renderable = false + txt.parent = labelGroup + ) + ) + + return labelGroup +) + +-- # endregion + +-- # region Snap Points Creation + +-- Create Point helpers at grid intersections for snapping +fn AA_CreateSnapPoints config baseName:"AA_RefGrid" = ( + local snapGroup = Dummy name:(baseName + "_SnapPoints") boxSize:[1,1,1] + snapGroup.pos = [0,0,0] + + -- Create points at all grid intersections (using minorStep) + for x = config.minX to config.maxX by config.minorStep do ( + for y = config.minY to config.maxY by config.minorStep do ( + local pt = Point name:"AA_SnapPt" size:config.snapPointSize \ + pos:[x, y, config.snapPointZ] \ + wireColor:config.snapPointColor \ + centermarker:true cross:false axisTripod:false box:false + pt.renderable = false + pt.parent = snapGroup + ) + ) + + return snapGroup +) + +-- # endregion + +-- # region Layer Management + +-- Get or create a layer by name +fn AA_GetOrCreateLayer layerName = ( + local layer = LayerManager.getLayerFromName layerName + if layer == undefined then ( + layer = LayerManager.newLayerFromName layerName + ) + return layer +) + +-- Move object and all its children to a layer +fn AA_MoveToLayer obj layer = ( + layer.addNode obj + for child in obj.children do ( + AA_MoveToLayer child layer + ) +) + +-- Freeze object and all its children (except specified exclusions) +fn AA_FreezeHierarchy obj excludeNames:#() = ( + local shouldFreeze = true + for exName in excludeNames do ( + if matchPattern obj.name pattern:exName do shouldFreeze = false + ) + if shouldFreeze do ( + obj.isFrozen = true + if isProperty obj #showFrozenInGray do obj.showFrozenInGray = false + ) + for child in obj.children do ( + AA_FreezeHierarchy child excludeNames:excludeNames + ) +) + +-- # endregion + +-- # region Reference Plane Creation + +-- Create the complete reference plane V2 with all components +-- Uses Spline-based grid lines (transparent) instead of Plane geometry +-- +-- Parameters: +-- splineObj: The Spline weight curve to attach to +-- config: AA_GridConfig struct (optional, uses defaults if not provided) +-- +-- Returns: +-- Dummy object that is the parent of all grid components +-- +fn AA_CreateReferencePlane splineObj config:undefined = ( + + -- Use default config if not provided + if config == undefined do config = AA_GridConfig() + + -- Create base name from spline name + local baseName = (substituteString splineObj.name "WeightCurve" "RefGrid") + + -- Create parent Point helper for all grid components (easier to select than Dummy) + local gridParent = Point name:baseName size:15 centermarker:true cross:true axisTripod:true box:true + gridParent.pos = [0,0,0] + gridParent.wirecolor = color 255 200 0 -- Golden yellow for visibility + gridParent.renderable = false + + -- Create grid lines + local gridLines = AA_CreateGridLines config baseName:baseName + gridLines.parent = gridParent + + -- Create colored axes + local xAxis = AA_CreateXAxis config baseName:baseName + xAxis.parent = gridParent + + local yAxis = AA_CreateYAxis config baseName:baseName + yAxis.parent = gridParent + + -- Create origin marker (if enabled) + if config.showOriginMarker then ( + local origin = AA_CreateOriginMarker config baseName:baseName + origin.parent = gridParent + ) + + -- Create boundary (if enabled) + if config.showBoundary then ( + local boundary = AA_CreateBoundary config baseName:baseName + boundary.parent = gridParent + ) + + -- Create coordinate labels (if enabled) + if config.showLabels then ( + local labels = AA_CreateCoordinateLabels config baseName:baseName + labels.parent = gridParent + ) + + -- Create snap points at grid intersections (if enabled) + if config.showSnapPoints then ( + local snapPoints = AA_CreateSnapPoints config baseName:baseName + snapPoints.parent = gridParent + ) + + -- Attach spline curve to grid + splineObj.parent = gridParent + + -- Move all objects to the specified layer + local helperLayer = AA_GetOrCreateLayer config.layerName + AA_MoveToLayer gridParent helperLayer + + -- Freeze reference objects (exclude the weight curve spline and gridParent) + if config.freezeReferences then ( + -- Freeze all children except the weight curve + for child in gridParent.children do ( + if child != splineObj then ( + AA_FreezeHierarchy child excludeNames:#() + ) + ) + -- DO NOT freeze gridParent - keep it selectable for moving the whole grid + ) + + return gridParent +) + +-- # endregion + +-- # region Grid Layout and Grouping + +-- Find all existing reference grid parent dummies in scene +-- Returns: Array of grid parent Dummy nodes (AA_RefGrid_*) +fn AA_FindExistingGrids = ( + local grids = #() + for obj in objects do ( + if matchPattern obj.name pattern:"AA_RefGrid_*" do ( + -- Only collect parent helpers, not child objects (Point or Dummy for backward compatibility) + if classof obj == Dummy or classof obj == Point do ( + append grids obj + ) + ) + ) + return grids +) + +-- Find all existing grid group containers in scene +-- Returns: Array of grid group Dummy nodes (*_GridGroup) +fn AA_FindExistingGridGroups = ( + local groups = #() + for obj in objects do ( + if matchPattern obj.name pattern:"*_GridGroup" do ( + if classof obj == Dummy do ( + append groups obj + ) + ) + ) + return groups +) + +-- Check if a candidate position collides with any existing grid or grid group +-- Uses distance-based collision detection +-- +-- Parameters: +-- pos: Candidate position [x, y, z] +-- existingObjects: Array of existing grid/group objects to check against +-- threshold: Minimum distance to consider as collision (default: 96.0 = 80% of 120 spacing) +-- +-- Returns: +-- true if collision detected, false otherwise +fn AA_CheckGridCollision pos existingObjects threshold:96.0 = ( + for obj in existingObjects do ( + local dist = length (obj.pos - pos) + if dist < threshold do return true + ) + return false +) + +-- Calculate next available grid position with collision avoidance +-- Handles user-moved grids by checking actual positions instead of just counting +-- +-- Parameters: +-- spacing: Distance between grids (default: AA_GRID_SPACING) +-- columns: Max columns before row wrap (default: AA_GRID_COLUMNS) +-- +-- Returns: +-- Point3 position for new grid that doesn't collide with existing ones +fn AA_CalculateNextGridPosition spacing:AA_GRID_SPACING columns:AA_GRID_COLUMNS = ( + -- Get all existing grid groups (primary collision targets) + local existingGroups = AA_FindExistingGridGroups() + local threshold = spacing * 0.8 -- 80% of spacing as collision threshold + + -- Try grid positions until finding one without collision + for attempts = 0 to 99 do ( + local row = attempts / columns + local col = mod attempts columns + + -- Calculate candidate position (MaxScript requires single line Point3) + local posX = AA_GRID_START_POS.x + (col * spacing) + local posY = AA_GRID_START_POS.y - (row * spacing) + local posZ = AA_GRID_START_POS.z + local candidatePos = [posX, posY, posZ] + + -- Check collision with existing grid groups + if not (AA_CheckGridCollision candidatePos existingGroups threshold:threshold) do ( + return candidatePos + ) + ) + + -- Fallback: offset from last grid group + if existingGroups.count > 0 then ( + return existingGroups[existingGroups.count].pos + [spacing, 0, 0] + ) else ( + return AA_GRID_START_POS + ) +) + +-- Set the orientation of a grid or grid group +-- This allows grids to be rotated (e.g., standing up vertically) +-- Note: Animation driving is NOT affected because sampling uses local coordinates +-- +-- Parameters: +-- gridObj: The grid parent dummy or grid group to rotate +-- orientation: #horizontal (default), #vertical_xz (face Y), #vertical_yz (face X) +-- +-- Returns: +-- true if successful +fn AA_SetGridOrientation gridObj orientation:#horizontal = ( + if gridObj == undefined do return false + if not isValidNode gridObj do return false + + case orientation of ( + #horizontal: ( + -- Flat on XY plane (default) + gridObj.rotation = (quat 0 0 0 1) + ) + #vertical_xz: ( + -- Standing up, facing Y axis (rotate 90 degrees around X) + gridObj.rotation = (quat 0.7071068 0 0 0.7071068) + ) + #vertical_yz: ( + -- Standing up, facing X axis (rotate 90 degrees around Y) + gridObj.rotation = (quat 0 0.7071068 0 0.7071068) + ) + default: ( + -- Unknown orientation, keep current + return false + ) + ) + + return true +) + +-- Create a group container for a bone chain's grids +-- All X/Y/Z grids for the same bone chain will be children of this group +-- +-- Parameters: +-- prefix: Bone chain prefix name (used in naming) +-- basePos: Base position for this group (optional, auto-calculated if undefined) +-- +-- Returns: +-- Dummy object as group container +fn AA_CreateGridGroup prefix basePos:undefined = ( + local groupName = prefix + "_GridGroup" + + -- Calculate position if not provided + if basePos == undefined do ( + basePos = AA_CalculateNextGridPosition() + ) + + -- Delete if exists (recreate with new position) + local existing = getNodeByName groupName + if existing != undefined do delete existing + + -- Create group container dummy (no rotation, just a container) + -- Each child grid will be rotated individually in AA_StackGridInGroup + local groupDummy = Dummy name:groupName pos:basePos boxSize:[1,1,1] + groupDummy.renderable = false + + -- Create a dedicated sub-layer for this bone chain's grids + -- Layer hierarchy: autoAnim_WeightCurves > {prefix}_GridGroup + local curvesLayer = AA_GetOrCreateChildLayer AA_LAYER_CURVES + local groupLayerName = groupName + local groupLayer = layerManager.getLayerFromName groupLayerName + if groupLayer == undefined do ( + groupLayer = layerManager.newLayerFromName groupLayerName + ) + -- Set parent using setParent (not addChildLayer which doesn't exist) + groupLayer.setParent curvesLayer + + -- Add group dummy to its dedicated layer + groupLayer.addnode groupDummy + + return groupDummy +) + +-- Stack a grid within its parent group +-- X/Y/Z grids are stacked with X-offset (horizontal direction) to avoid overlap +-- Each grid is rotated to vertical orientation (standing up, facing front view) +-- This makes X/Y/Z grids visible side-by-side horizontally in front view +-- +-- Parameters: +-- gridParent: The reference grid dummy to position +-- groupContainer: The parent group dummy +-- axisIndex: (deprecated) Now ignored, position is based on existing children count +-- stackOffset: X offset between stacked grids (default: AA_GRID_STACK_OFFSET) +fn AA_StackGridInGroup gridParent groupContainer axisIndex stackOffset:AA_GRID_STACK_OFFSET = ( + if gridParent == undefined do return false + if groupContainer == undefined do return false + + -- Count existing children to determine position (before parenting) + local existingCount = groupContainer.children.count + + -- First rotate the grid to vertical orientation BEFORE parenting + -- This ensures the grid stands up facing the front view + AA_SetGridOrientation gridParent orientation:#vertical_xz + + -- Parent to group + gridParent.parent = groupContainer + + -- Stack position in parent space: offset in X direction based on existing children count + -- First grid at 0, second at stackOffset, third at stackOffset*2, etc. + in coordsys parent gridParent.pos = [existingCount * stackOffset, 0, 0] + + -- Move grid and all its children to the same layer as the group container + local groupLayerName = groupContainer.name + local groupLayer = layerManager.getLayerFromName groupLayerName + if groupLayer != undefined do ( + groupLayer.addnode gridParent + -- Also add all children (spline curve, grid lines, etc.) + for child in gridParent.children do ( + groupLayer.addnode child + ) + ) + + return true +) + +-- # endregion + +-- # region Module Info + +format "========================================\n" +format "Spline Curve Widget Module Loaded\n" +format "========================================\n" + +-- # endregion + diff --git a/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/tool_functions.ms b/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/tool_functions.ms new file mode 100644 index 0000000..e5d5c0d --- /dev/null +++ b/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/tool_functions.ms @@ -0,0 +1,309 @@ +-- ============================================================================ +-- Auto Animation Tool - Tool Functions Module +-- ============================================================================ +-- This module contains the external functions used by the tool window. +-- These functions handle hierarchy creation, expression application, etc. +-- +-- Version: 2.0.0 +-- ============================================================================ + +-- # region Hierarchy Sorting + +-- Sort objects by hierarchy (parents first, then children) +fn sortArrayByHeirarchy objectArray = ( + sortedArray = #() + childArray = #() + -- first find roots and put them at left + for obj in objectArray do ( + if (finditem objectArray obj.parent) == 0 + then append sortedArray obj + else append childArray obj + ) + -- now insert children on immediate right of their parents + -- repeat scan until child array is empty + while childArray.count > 0 do ( + i = 1 + while i <= childArray.count do ( + parentIdx = finditem sortedArray childArray[i].parent + if parentIdx != 0 then ( + insertItem childArray[i] sortedArray (parentIdx+1) + deleteItem childArray i + ) else ( + i += 1 + ) + ) + ) + sortedArray +) + +-- # endregion + +-- # region Controller Creation + +-- Create main animation controller with Custom Attributes +-- Parameters: +-- prefix: Name prefix for the controller +-- boneChain: Array of bones in the chain +-- r: Controller size +-- p: Controller position +-- gridGroup: Optional grid group container (if provided, grid will be stacked in group) +-- axisIndex: Axis index for stacking (1=X, 2=Y, 3=Z) +-- twistMode: Twist control mode (0=None, 1=FirstLast, 2=Individual) +fn creatAutoAnimMainCtrl prefix boneChain r p gridGroup:undefined axisIndex:1 twistMode:1 = ( + -- r = controller size, p = controller position + local existing = getnodebyname prefix + if existing != undefined do delete existing + ctrl_master = undefined + ctrl_master = text size:r kerning:0 leading:0 pos:p isSelected:off + ctrl_master.text = prefix + ctrl_master.name = prefix + + -- Position adjustment + ctrl_master.rotation = (quat 0.707107 0 0 0.707107) + ctrl_master.pos = p + + -- Add Custom Attributes + addmodifier ctrl_master (EmptyModifier()) + add_attribute = custattributes.add ctrl_master.modifiers[1] boneSineAniUsePos + + -- Initialize default values + ctrl_master.modifiers[1].range = 0 + ctrl_master.modifiers[1].loop = 160 + ctrl_master.modifiers[1].speedV = 8.8 + ctrl_master.modifiers[1].offsetV = 100 + ctrl_master.modifiers[1].rangeAdd = 20 + ctrl_master.modifiers[1].frameFix = 0.0 + ctrl_master.modifiers[1].twistMode = twistMode + + -- Store bone names (use custom format to avoid truncation) + boneInfoName = #() + for i in boneChain do (append boneInfoName i.name) + -- Build string manually to avoid "..." truncation in large arrays + local boneInfoStr = "#(" + for i = 1 to boneInfoName.count do ( + boneInfoStr += "\"" + boneInfoName[i] + "\"" + if i < boneInfoName.count then boneInfoStr += ", " + ) + boneInfoStr += ")" + ctrl_master.modifiers[1].boneInfo = boneInfoStr + + -- Calculate bone percentages + distanceRec = #() + disAll = 0 + append distanceRec 0 + + for i = 1 to boneChain.count-1 do ( + dis = length(boneChain[i+1].transform.pos - boneChain[i].transform.pos) + disAll += dis + append distanceRec disAll + ) + -- Avoid divide by zero + if disAll == 0 then disAll = 1.0 + longthPer = #() + perDD = 0 + for i = 1 to boneChain.count do (append longthPer (distanceRec[i]/disAll)) + + strfix = "#(" + for i = 1 to longthPer.count do ( + strfix += (longthPer[i] as string) + if i < longthPer.count then (strfix += ", ") + ) + strfix += ")" + + ctrl_master.modifiers[1].boneCount = longthPer.count + ctrl_master.modifiers[1].bonelongPercent = strfix + + -- Create Spline weight curve at [0,0,0] + -- No position offset - Spline stays at origin with pivot at [0,0,0] + local splineCurve = AA_CreateWeightCurveSpline pos:[0, 0, 0] + + -- Create reference plane - this sets splineCurve.parent = refPlane + -- Plane at [0,0,0] with pivot at [0,0,0], no parent + -- Spline becomes child of Plane, also at [0,0,0] + local refPlane = AA_CreateReferencePlane splineCurve + + -- Add controller to Controllers layer + AA_AddToControllersLayer ctrl_master + -- Add Spline and reference plane to Weight Curves layer + AA_AddToCurvesLayer splineCurve + AA_AddToCurvesLayer refPlane + + -- Position the reference plane based on gridGroup or calculate new position + if gridGroup != undefined then ( + -- Stack grid within the group + AA_StackGridInGroup refPlane gridGroup axisIndex + ) else ( + -- Calculate new position using collision detection + local newPos = AA_CalculateNextGridPosition() + refPlane.pos = newPos + ) + + -- Associate Spline with controller's Custom Attributes + ctrl_master.modifiers[1].boneSineAniUsePos.splineCurveNode = splineCurve + ctrl_master.modifiers[1].boneSineAniUsePos.weightCurveMode = 2 -- Use Spline mode by default + + return ctrl_master +) + +-- # endregion + +-- # region Expression Animation + +-- Add expression animation to objects +-- axisSet: 1=X, 2=Y, 3=Z position +fn mackExpAnim axisSet ctrl_master objs percentValue = ( + -- Get Spline weight curve from attributes + local splineNode = undefined + local attrHolder = ctrl_master.modifiers[#Attribute_Holder] + if attrHolder != undefined do ( + if hasProperty attrHolder.boneSineAniUsePos #splineCurveNode do ( + splineNode = attrHolder.boneSineAniUsePos.splineCurveNode + ) + ) + + for i = 1 to objs.count do ( + boneTar = objs[i] + if classof(boneTar.pos.controller) != position_list then ( + boneTar.pos.controller = Position_XYZ () + boneTar.pos.controller = position_list () + posPP = Position_XYZ () + boneTar.pos.controller[1][axisSet].value = 0 + boneTar.pos.controller.Available.controller = posPP + ) + + posPPScript = float_script() + boneTar.pos.controller[2][axisSet].controller = posPPScript + + posPPScript.AddNode "this" ctrl_master + posPPScript.AddTarget "range" ctrl_master.modifiers[1].boneSineAniUsePos[#range] + posPPScript.AddTarget "loop" ctrl_master.modifiers[1].boneSineAniUsePos[#loop] + posPPScript.AddTarget "offsetV" ctrl_master.modifiers[1].boneSineAniUsePos[#offsetV] + posPPScript.AddTarget "rangeAdd" ctrl_master.modifiers[1].boneSineAniUsePos[#rangeAdd] + posPPScript.AddTarget "frameFix" ctrl_master.modifiers[1].boneSineAniUsePos[#frameFix] + posPPScript.AddConstant "boneper" percentValue[i] + + -- Build expression using Spline weight curve or fallback to constant + if splineNode != undefined and isValidNode splineNode then ( + -- Spline mode: use AA_SampleSplineAtX for weight lookup + posPPScript.AddNode "splineCurve" splineNode + posPPScript.script = + "curveVV = AA_SampleSplineAtX splineCurve (boneper * 100)" + "\n" + + "sin( (" + (AA_TWO_PI as string) + " / (loop / " + (AA_TIME_SCALE as string) + ")) * ((F) + frameFix + (offsetV*1 * boneper * -1))) * ( curveVV*rangeAdd*0.01 + range)" + ) else ( + -- Fallback: no Spline available, use constant weight = 100 + posPPScript.script = + "curveVV = 100" + "\n" + + "sin( (" + (AA_TWO_PI as string) + " / (loop / " + (AA_TIME_SCALE as string) + ")) * ((F) + frameFix + (offsetV*1 * boneper * -1))) * ( curveVV*rangeAdd*0.01 + range)" + ) + ) +) + +-- # endregion + +-- # region Utility Functions + +-- Format number to 3 digits with leading zeros +fn formatNumberToThreeDigits num = ( + numStr = num as string + while (numStr.count < 3) do (numStr = "0" + numStr) + return numStr +) + +-- Reverse array in place +fn ReverseListFN arr = ( + if arr == undefined do return undefined + if (mod arr.count 2) == 0 then ( + for i = 1 to (arr.count / 2) do ( + local atemp = arr[i] + index = arr.count - (i - 1) + arr[i] = arr[index] + arr[index] = atemp + ) + ) else ( + endindex = (arr.count - 1) * 0.5 + for i = 1 to endindex do ( + local atemp = arr[i] + index = arr.count - (i - 1) + arr[i] = arr[index] + arr[index] = atemp + ) + ) +) + +-- # endregion + +-- # region Natural Sort Helper + +-- Get SHLWAPI for natural sorting +fn getSHLWAPI = ( + source = "using System;\n" + source += "using System.Runtime.InteropServices;\n" + source += "class SHLWAPI\n" + source += "{\n" + source += " [DllImport(\"shlwapi.dll\", CharSet = CharSet.Auto)]\n" + source += " public static extern int StrCmpLogicalW(string lpszStr, string lpszComp);\n" + source += "}\n" + + csharpProvider = dotnetobject "Microsoft.CSharp.CSharpCodeProvider" + compilerParams = dotnetobject "System.CodeDom.Compiler.CompilerParameters" + + compilerParams.GenerateInMemory = on + compilerResults = csharpProvider.CompileAssemblyFromSource compilerParams #(source) + compilerResults.CompiledAssembly.CreateInstance "SHLWAPI" +) + +-- # endregion + +-- # region Locator and Constraint Functions + +-- Create or replace a point helper (locator) +-- Uses visible Point helper with centermarker and axisTripod for easy selection +fn yunDummyCheckCreate obj = ( + yunSel = getNodeByName obj + -- Delete if exists + if yunSel != undefined then (delete yunSel) + Dummy_Anim = point pos:[0,0,0] isSelected:off size:13 centermarker:true cross:true axisTripod:true box:false + Dummy_Anim.name = obj + Dummy_Anim.wirecolor = color 0 200 255 -- Cyan for visibility +) + +-- Apply position constraint from objA to objB +fn yunPosconstraint objA objB = ( + objA.pos.controller = Position_XYZ () + objA.pos.controller = position_list () + posPP = Position_Constraint() + objA.pos.controller.Available.controller = posPP + posPP.appendTarget objB 100 + posPP.relative = true + objA.pos.controller.Available.controller = Position_XYZ () + objA.pos.controller.setActive 3 +) + +-- Apply rotation constraint from objA to objB +fn yunRotconstraint objA objB = ( + objA.rotation.controller = Euler_XYZ () + objA.rotation.controller = rotation_list () + rotPP = Orientation_Constraint() + objA.rotation.controller.Available.controller = rotPP + rotPP.appendTarget objB 100 + rotPP.relative = true + objA.rotation.controller.Available.controller = Local_Euler_XYZ () + objA.rotation.controller.setActive 3 +) + +-- # endregion + +-- # region Module Info + +format "========================================\n" +format "Tool Functions Module Loaded\n" +format " - sortArrayByHeirarchy\n" +format " - creatAutoAnimMainCtrl\n" +format " - mackExpAnim\n" +format " - yunDummyCheckCreate\n" +format " - yunPosconstraint\n" +format " - yunRotconstraint\n" +format "========================================\n" + +-- # endregion + diff --git a/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/tool_window.ms b/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/tool_window.ms new file mode 100644 index 0000000..9c89bde --- /dev/null +++ b/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/core/tool_window.ms @@ -0,0 +1,567 @@ +-- ============================================================================ +-- Auto Animation Tool - Tool Window Module +-- ============================================================================ +-- This module contains the main tool window (yun_autoAnim_tool rollout). +-- +-- IMPORTANT: UI MUST NOT BE MODIFIED! +-- This file should contain the EXACT original code from: +-- ref_code/自己动封装开始_v2.ms lines 1308-1630 +-- +-- MANUAL COPY REQUIRED: +-- 1. Open ref_code/自己动封装开始_v2.ms in a text editor with GBK encoding +-- 2. Copy lines 1308-1630 (the entire yun_autoAnim_tool section) +-- 3. Paste below this comment block, replacing the placeholder +-- +-- Version: 2.0.0 +-- ============================================================================ + +-- # region Tool Window Code (copied from original lines 1308-1630) + +Global yun_autoAnim_tool --界面 +Global iniPosTest_yun_autoAnim --位置 +if iniPosTest_yun_autoAnim==undefined then (iniPosTest_yun_autoAnim = [200,200]) +Global switchAnimButtonMouState_yun_autoAnim = false ---鼠标状态 +Global posAnimButtonMouMove_yun_autoAnim = [0,0] ---事变移动 + +ctrl_masterGrp =#() --控制器储存 +selectExpressionBones = undefined --骨骼储存 +waveLoc = #() --中间物储存 +try(destroyDialog yun_autoAnim_tool)catch() +rollout yun_autoAnim_tool "" width:218 height:43 +( + button ctrlZ "退货" pos:[175,3] width:38 height:35 tooltip:"恢复所有" + -----一键 + button btn_Sets1 "^O^" pos:[2,3] width:35 height:35 tooltip:"左键解君愁,右键赐惊喜" + checkbox axisSet_X "X" pos:[55,15] width:40 height:15 checked:false tooltip:"x" + checkbox axisSet_Y "Y" pos:[95,15] width:30 height:15 checked:true tooltip:"y" + checkbox axisSet_Z "Z" pos:[135,15] width:30 height:15 checked:false tooltip:"z" + + -- Twist control UI (placed below X/Y/Z checkboxes) + + checkbox chkEnableTwist "启用Twist" pos:[10,45] width:70 height:18 checked:true tooltip:"启用骨骼链Twist/Roll控制" + radioButtons rdoTwistMode "" labels:#("首尾 ", "单独 ") pos:[90,47] columns:2 default:1 visible:true + label split1 "︵︵︵︵︵︵︵︵︵︵︵︵︵︵︵︵︵︵︵︵" pos:[5,68] width:218 height:10 + ------详细设置 + button btn_Sets2 "^O^" pos:[2,80] width:35 height:35 tooltip:"左键解君愁,右键赐惊喜" + label lab_2 "前缀:" pos:[55,91] width:35 height:50 + editText prefixTx text:"" pos:[85,83] width:130 height:30 labelOnTop: false bold:true --readOnly:false + button creatCrl "1>加控制器" pos:[2,120] width:70 height:35 tooltip:"" + button creathierarchy "2>加中间层" pos:[75,120] width:70 height:35 tooltip:"" + button creathExp "2>加表达式" pos:[146,120] width:70 height:35 tooltip:"" + label split2 "︵︵︵︵︵︵︵︵︵︵︵︵︵︵︵︵︵︵︵︵" pos:[5,155] width:218 height:10 + label occuqied_temp "—————暂时什么都没有了(*゜ー゜*)" pos:[5,175] width:218 height:35 + -- Twist checkbox changed event: show/hide radioButtons + on chkEnableTwist changed state do ( + rdoTwistMode.visible = state + ) + + + + --恢复所有 + on ctrlZ pressed do ( + format "\n========== 恢复所有 按钮被点击 ==========\n" + max create mode + + format "[DEBUG] selectExpressionBones: %\n" selectExpressionBones + format "[DEBUG] selection.count: %\n" selection.count + + if selectExpressionBones == undefined or selectExpressionBones.count == 0 or not isValidNode(selectExpressionBones[1]) then ( + format "[DEBUG] 需要从选择获取骨骼\n" + if selection.count > 0 then ( + selectExpressionBones = selection as array + format "[DEBUG] 从选择获取骨骼: %\n" selectExpressionBones + ) else ( + messageBox "请先选择需要恢复的骨骼链" + return undefined + ) + ) + + undo on ( + -- Get selected bone names for matching + -- Strip "_Anim" suffix if present to match original bone names + local selectedBoneNames = for bone in selectExpressionBones collect ( + local bName = bone.name + if matchPattern bName pattern:"*_Anim" then ( + -- Remove "_Anim" suffix (5 characters) + substring bName 1 (bName.count - 5) + ) else ( + bName + ) + ) + format "[DEBUG] Selected bone names (stripped): %\n" selectedBoneNames + + -- Step 0: Find ALL controllers that match the selected bone chain + local matchedControllers = #() -- Array of: #(controller, caInst, spline) + local controllerCount = 0 + + -- Search for controllers with Custom Attributes (boneSineAniUsePos) + for obj in objects do ( + if obj.modifiers.count > 0 do ( + for i = 1 to obj.modifiers.count do ( + if classof obj.modifiers[i] == EmptyModifier do ( + local attrMod = obj.modifiers[i] + local custAttrDefs = custAttributes.getDefs attrMod + + if custAttrDefs != undefined and custAttrDefs.count > 0 do ( + for caDef in custAttrDefs do ( + if caDef.name == #boneSineAniUsePos do ( + controllerCount += 1 + local caInst = custAttributes.get attrMod caDef + + if caInst != undefined and (hasProperty caInst #boneInfo) do ( + local storedBoneInfoRaw = caInst.boneInfo + local displayLen = if storedBoneInfoRaw.count > 100 then 100 else storedBoneInfoRaw.count + format "[DEBUG] Controller '%' boneInfo (raw, first 100 chars): %\n" obj.name (substring storedBoneInfoRaw 1 displayLen) + + -- boneInfo is stored as a string, need to parse it + if storedBoneInfoRaw != undefined and storedBoneInfoRaw != "" do ( + -- Check if string is truncated (contains "...") + local isTruncated = (findString storedBoneInfoRaw "...") != undefined + local storedBoneInfo = undefined + + if isTruncated then ( + format "[DEBUG] WARNING: boneInfo is truncated, using fallback parsing\n" + -- Fallback: extract bone names manually + storedBoneInfo = #() + local tempStr = storedBoneInfoRaw + local startPos = 1 + while true do ( + local quoteStart = findString (substring tempStr startPos -1) "\"" + if quoteStart == undefined do exit + quoteStart = startPos + quoteStart - 1 + local quoteEnd = findString (substring tempStr (quoteStart + 1) -1) "\"" + if quoteEnd == undefined do exit + quoteEnd = quoteStart + quoteEnd + local boneName = substring tempStr (quoteStart + 1) (quoteEnd - quoteStart - 1) + append storedBoneInfo boneName + startPos = quoteEnd + 1 + ) + format "[DEBUG] Parsed % bone names from truncated string\n" storedBoneInfo.count + ) else ( + -- Normal parsing with execute + storedBoneInfo = execute storedBoneInfoRaw + ) + + if classof storedBoneInfo == Array and storedBoneInfo.count > 0 do ( + local isMatch = false + for selName in selectedBoneNames do ( + for storedName in storedBoneInfo do ( + if selName == storedName do ( + isMatch = true + exit + ) + ) + if isMatch do exit + ) + if isMatch do ( + -- Get associated spline + local splineNode = undefined + if hasProperty caInst #splineCurveNode do ( + splineNode = caInst.splineCurveNode + ) + -- Store matched controller info + append matchedControllers #(obj, caInst, splineNode) + format "[DEBUG] Matched controller: % (spline: %)\n" obj.name (if splineNode != undefined then splineNode.name else "none") + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + + format "[DEBUG] Total controllers in scene: %, Matched: %\n" controllerCount matchedControllers.count + + if matchedControllers.count == 0 do ( + messageBox "未找到该骨骼链对应的控制器" + return undefined + ) + + -- Step 1: Zero out ALL matched controllers to restore bones to bind pose + for ctrlData in matchedControllers do ( + local caInst = ctrlData[2] + if caInst != undefined do ( + caInst.range = 0 + caInst.rangeAdd = 0 + caInst.offsetV = 0 + format "[DEBUG] Controller '%' parameters zeroed\n" ctrlData[1].name + ) + ) + + -- Force viewport update to apply zero values + sliderTime = currentTime + redrawViews() + + -- Step 2: Restore bone controllers and transforms + for bone in selectExpressionBones do ( + if isValidNode bone do ( + local tm = bone.transform + bone.rotation.controller = Euler_XYZ() + bone.pos.controller = Position_XYZ() + bone.transform = tm + ) + ) + + -- Step 3: Delete the associated splines and their parent (reference grids) for ALL controllers + for ctrlData in matchedControllers do ( + local matchedSpline = ctrlData[3] + if matchedSpline != undefined and isValidNode matchedSpline do ( + local refGrid = matchedSpline.parent + if refGrid != undefined and isValidNode refGrid do ( + format "[DEBUG] Deleting refGrid and all children: %\n" refGrid.name + -- Collect all children using iterative approach (BFS) + local allChildren = #() + local queue = #(refGrid) + while queue.count > 0 do ( + local current = queue[1] + deleteItem queue 1 + for child in current.children do ( + append allChildren child + append queue child + ) + ) + format "[DEBUG] Found % children to delete\n" allChildren.count + -- Delete children first (in reverse order to handle hierarchy) + for i = allChildren.count to 1 by -1 do ( + if isValidNode allChildren[i] do delete allChildren[i] + ) + -- Then delete the parent + if isValidNode refGrid do delete refGrid + ) + ) + ) + + -- Step 4: Delete ALL controller objects (the SplineShape with boneSineAniUsePos) + for ctrlData in matchedControllers do ( + local ctrl = ctrlData[1] + if isValidNode ctrl do ( + format "[DEBUG] Deleting controller: %\n" ctrl.name + delete ctrl + ) + ) + + -- Step 5: Delete helper objects (_Anim_Master, _Anim_Wave) associated with this bone chain + -- Get the original bone names (without _Anim suffix) + -- IMPORTANT: Clear Wave rotation controllers BEFORE deleting Masters to avoid script errors + local helperObjectsToDelete = #() + local waveObjectsToDelete = #() + local masterObjectsToDelete = #() + for boneName in selectedBoneNames do ( + -- Find *_Anim_Master and *_Anim_Wave objects + local masterName = boneName + "_Anim_Master" + local waveName = boneName + "_Anim_Wave" + local masterObj = getNodeByName masterName + local waveObj = getNodeByName waveName + if waveObj != undefined do append waveObjectsToDelete waveObj + if masterObj != undefined do append masterObjectsToDelete masterObj + ) + + -- Clear Wave rotation controllers to prevent script errors when Masters are deleted + for obj in waveObjectsToDelete do ( + if isValidNode obj do ( + format "[DEBUG] Clearing rotation controller for: %\n" obj.name + obj.rotation.controller = Euler_XYZ() + ) + ) + + -- Delete Wave objects first + for obj in waveObjectsToDelete do ( + if isValidNode obj do ( + format "[DEBUG] Deleting helper: %\n" obj.name + delete obj + ) + ) + + -- Then delete Master objects + for obj in masterObjectsToDelete do ( + if isValidNode obj do ( + format "[DEBUG] Deleting helper: %\n" obj.name + delete obj + ) + ) + + -- Step 6: Delete copied bones (_Anim bones) + local animBonesToDelete = #() + for boneName in selectedBoneNames do ( + local animBoneName = boneName + "_Anim" + local animBone = getNodeByName animBoneName + if animBone != undefined do append animBonesToDelete animBone + ) + + -- Delete from last to first to handle hierarchy + for i = animBonesToDelete.count to 1 by -1 do ( + local bone = animBonesToDelete[i] + if isValidNode bone do ( + format "[DEBUG] Deleting anim bone: %\n" bone.name + delete bone + ) + ) + + -- Clear stored references for this bone chain + selectExpressionBones = undefined + waveLoc = #() + + format "[INFO] Bone chain restored. Controller and helper objects cleaned.\n" + ) + ) + + ---一键执行 + on btn_Sets1 pressed do + ( + yun_autoAnim_tool.creatCrl.pressed() + if selection.count>0 then ( + yun_autoAnim_tool.creathierarchy.pressed() + redrawViews() + yun_autoAnim_tool.creathExp.pressed() + ) + ) + --1>加控制器 + on creatCrl pressed do ( + ctrl_masterGrp =#() --控制器储存 + selectExpressionBones = undefined --骨骼储存 + waveLoc = #() --中间物储存 + if selection.count>0 then ( + animate off ( + ---获取选择 + selectExpressionBones = sortArrayByHeirarchy (selection as array) + ----根据骨骼大小调整控制器大小 + r = 1/5 + p = [0,0,0] + if selectExpressionBones.count > 0 and classof(selectExpressionBones[1])==BoneGeometry then + (r = selectExpressionBones[1].length;p = selectExpressionBones[1].transform.pos) + + -- 前缀设定 + prefix = "antoAnim" + if yun_autoAnim_tool.prefixTx.text=="" then (prefix = selectExpressionBones[1].name) else (prefix = yun_autoAnim_tool.prefixTx.text) + + -- Create grid group container for this bone chain's coordinate systems + -- All X/Y/Z grids will be stacked vertically within this group + local gridGroup = AA_CreateGridGroup prefix + + -- Get twist mode from UI for controller creation + local twistModeForCtrl = 0 -- Default: no twist + if yun_autoAnim_tool.chkEnableTwist.checked do ( + twistModeForCtrl = yun_autoAnim_tool.rdoTwistMode.state -- 1=FirstLast, 2=Individual + ) + + -- Track if twist has been assigned to a controller (only first one gets twist) + local twistAssigned = false + + --检查重复 + --ctrlCheck = getnodebyname (prefix+"EXP") + --if (ctrlCheck!=undefined) then + --(messageBox "已经存在同名控制,请删除或改前缀") + --else ( + --开始创建 (Priority: Y > X > Z for twist assignment) + if yun_autoAnim_tool.axisSet_Y.checked then( + local thisTwistMode = if twistAssigned then 0 else twistModeForCtrl + CC = creatAutoAnimMainCtrl (prefix + "_Y") selectExpressionBones r [p.x,p.y,(p.z+2*r)] gridGroup:gridGroup axisIndex:2 twistMode:thisTwistMode + append ctrl_masterGrp CC + ctrl_masterGrp[2] = CC + CC.modifiers[#Attribute_Holder].boneSineAniUsePos.frameFix = 80 + if twistModeForCtrl != 0 do twistAssigned = true + ) + if yun_autoAnim_tool.axisSet_X.checked then( + local thisTwistMode = if twistAssigned then 0 else twistModeForCtrl + CC = creatAutoAnimMainCtrl (prefix + "_X") selectExpressionBones r [p.x,p.y,(p.z+3*r)] gridGroup:gridGroup axisIndex:1 twistMode:thisTwistMode + append ctrl_masterGrp CC + ctrl_masterGrp[1] = CC + CC.modifiers[#Attribute_Holder].boneSineAniUsePos.frameFix = 40 + if twistModeForCtrl != 0 do twistAssigned = true + ) + if yun_autoAnim_tool.axisSet_Z.checked then( + local thisTwistMode = if twistAssigned then 0 else twistModeForCtrl + CC = creatAutoAnimMainCtrl (prefix + "_Z") selectExpressionBones r [p.x,p.y,(p.z+1*r)] gridGroup:gridGroup axisIndex:3 twistMode:thisTwistMode + append ctrl_masterGrp CC + ctrl_masterGrp[3] = CC + if twistModeForCtrl != 0 do twistAssigned = true + ) + --) + ) + select selectExpressionBones + )else ( messageBox "未选物体";) + + -- Controllers already added to layer in creatAutoAnimMainCtrl function + + + + ) + --2>加中间层 + on creathierarchy pressed do ( + max create mode + undo on( + sel = selection as array + animBones = #() + -- Get or create Helpers layer for hierarchy objects (child of autoAnim) + local helpersLayer = AA_GetOrCreateChildLayer AA_LAYER_HELPERS + + maxOps.cloneNodes sel cloneType:#copy newNodes:&animBones + for i in animBones do (helpersLayer.addnode i ) --加到层里 + for i=1 to animBones.count do ( + animBones[i].name = sel[i].name+"_Anim" + animBones[i].wirecolor = yellow -- Set _Anim bones to yellow for visibility + ) + waveLoc = #() + masterLoc = #() -- Collect masters for quaternion aim constraints + for i in animBones do + ( + animate off + ( + nameLoc = (i.name+"_Master") + nameWave = (i.name+"_Wave") + + yunDummyCheckCreate nameLoc + yunDummyCheckCreate nameWave + + waveObj = getNodeByName nameWave + append waveLoc waveObj + masterObj = getNodeByName nameLoc + append masterLoc masterObj -- Collect master for quaternion aim + waveObj.parent = masterObj + + helpersLayer.addnode waveObj --加到层里 + helpersLayer.addnode masterObj --加到层里 + + masterObj.transform = i.transform + --层级link给复制的骨骼 + masterObj.Transform.controller = Link_Constraint () + masterObj.transform.controller.AddTarget i ((sliderTime as string) as integer) + masterObj.isHidden = true -- Hide Master, keep Wave visible + + in coordsys parent waveObj.rotation = (quat 0 0 0 0) + in coordsys parent waveObj.pos = [0,0,0] + waveObj.axistripod = on + waveObj.cross = off + ) + ) + + --删除原先骨骼动画 + select sel + macros.run "Animation Tools" "DeleteSelectedAnimation" + for i in sel do ( -- i =sel[1] + PP = getNodeByName (i.name+"_Anim"+"_Wave") + --print PP + if PP!=undefined then( + --蒙皮骨骼link给loc + i.Transform.controller = prs () + yunPosconstraint i PP + yunRotconstraint i PP + ) + ) + ) + + + if ::SHLWAPI == undefined do SHLWAPI = getSHLWAPI() + -- Sort both arrays in same order (wave and master must match) + try (qsort waveLoc SHLWAPI.StrCmpLogicalW )catch() + try (qsort masterLoc SHLWAPI.StrCmpLogicalW )catch() + ReverseListFN waveLoc + ReverseListFN masterLoc + + -- Apply quaternion aim constraints (replaces LookAt_Constraint to avoid gimbal lock) + -- rootParent: first _Anim bone as rotation inheritance root + local rootParent = animBones[1] + if rootParent == undefined do rootParent = waveLoc[1].parent -- fallback + + -- Get twist mode from UI + local twistModeVal = 0 -- Default: no twist + if yun_autoAnim_tool.chkEnableTwist.checked do ( + twistModeVal = yun_autoAnim_tool.rdoTwistMode.state -- 1=FirstLast, 2=Individual + ) + + -- Get mainCtrl and bonelongPercent for FirstLast twist mode + local mainCtrlForTwist = undefined + local percentArray = #() + if twistModeVal == 1 and ctrl_masterGrp.count > 0 do ( + -- Use first available controller (Y axis preferred) + if ctrl_masterGrp[2] != undefined then ( + mainCtrlForTwist = ctrl_masterGrp[2] + ) else if ctrl_masterGrp[1] != undefined then ( + mainCtrlForTwist = ctrl_masterGrp[1] + ) else if ctrl_masterGrp[3] != undefined then ( + mainCtrlForTwist = ctrl_masterGrp[3] + ) + -- Parse bonelongPercent from controller + if mainCtrlForTwist != undefined do ( + local pctStr = mainCtrlForTwist.modifiers[1].boneSineAniUsePos.bonelongPercent + if pctStr != "" do percentArray = execute pctStr + ) + ) + + AA_ApplyQuaternionAimConstraints waveLoc masterLoc rootParent twistMode:twistModeVal mainCtrl:mainCtrlForTwist bonelongPercent:percentArray + ) + + --3>加表达式 + on creathExp pressed do ( + -->加脚本自己动 + ReverseListFN waveLoc + max create mode + if yun_autoAnim_tool.axisSet_X.checked then( + percentValue = execute(ctrl_masterGrp[1].modifiers[1].boneSineAniUsePos.bonelongPercent) + mackExpAnim 1 ctrl_masterGrp[1] waveLoc percentValue + ) + if yun_autoAnim_tool.axisSet_Y.checked then( + percentValue = execute(ctrl_masterGrp[2].modifiers[1].boneSineAniUsePos.bonelongPercent) + mackExpAnim 2 ctrl_masterGrp[2] waveLoc percentValue + ) + if yun_autoAnim_tool.axisSet_Z.checked then( + percentValue = execute(ctrl_masterGrp[3].modifiers[1].boneSineAniUsePos.bonelongPercent) + mackExpAnim 3 ctrl_masterGrp[3] waveLoc percentValue + ) + ) + + + + + ---展开1 + on btn_Sets1 rightclick do ( + if yun_autoAnim_tool.height==43 then (yun_autoAnim_tool.height=160;btn_Sets2.text = "⊙▽⊙") else (yun_autoAnim_tool.height=43;btn_Sets2.text = "^O^") + ) + ---展开3 + on btn_Sets2 rightclick do ( + if yun_autoAnim_tool.height==160 then (yun_autoAnim_tool.height=210;btn_Sets2.text = "⊙▽⊙") else (yun_autoAnim_tool.height = 160;btn_Sets2.text = "^O^") + ) + + + -- 窗口移动功能 + on yun_autoAnim_tool mousemove pos do UI_make.MoveWindow yun_autoAnim_tool + on yun_autoAnim_tool lbuttondown pos do UI_make.IsReadyToMove yun_autoAnim_tool pos + on yun_autoAnim_tool lbuttonup pos do UI_make.LetGoTheWindow() + +--关闭时记录位置 + on yun_autoAnim_tool close do -- 关闭记忆浮动窗口位置 + ( + iniPosTest_yun_autoAnim = (GetDialogPos yun_autoAnim_tool) + ) +---中间按下关闭窗口 + on yun_autoAnim_tool mbuttondown pos do + ( + try (destroydialog yun_autoAnim_tool) catch () + ) +-- 左键鼠标位置 + on yun_autoAnim_tool lbuttondown posMou do + ( + posAnimButtonMouMove_yun_autoAnim = posMou + switchAnimButtonMouState_yun_autoAnim = on + ) +---左键弹起 + on yun_autoAnim_tool lbuttonup posMou do + ( + switchAnimButtonMouState_yun_autoAnim = off + ) + +--移动鼠标则移动窗口 + on yun_autoAnim_tool mouseMove pos do + ( + if switchAnimButtonMouState_yun_autoAnim == on then + ( + SetDialogPos yun_autoAnim_tool (mouse.screenpos - posAnimButtonMouMove_yun_autoAnim) + ) + ) +) + +Createdialog yun_autoAnim_tool pos:iniPosTest_yun_autoAnim style:#() diff --git a/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/utils/naming.ms b/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/utils/naming.ms new file mode 100644 index 0000000..4202de0 --- /dev/null +++ b/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/utils/naming.ms @@ -0,0 +1,122 @@ +-- ============================================================================ +-- Auto Animation Tool - Naming Utilities Module +-- ============================================================================ +-- This module provides utility functions for naming and bone chain calculations. +-- These functions are used throughout the system for consistent naming and +-- bone percentage calculations. +-- +-- Version: 2.0.0 +-- Author: Auto Animation Tool Team +-- ============================================================================ + +-- # region Bone Chain Calculations + +-- Calculate bone chain length percentages +-- This function calculates the cumulative distance percentage for each bone in a chain +-- Used for phase offset calculations in sine wave expressions +-- +-- Parameters: +-- boneChain: Array of bone objects in hierarchical order +-- +-- Returns: +-- Array of float values representing cumulative distance percentage (0.0 to 1.0) +-- First bone is always 0.0, last bone is always 1.0 +-- +-- Example: +-- bones = #($Bone01, $Bone02, $Bone03) +-- percentages = AA_CalculateBonePercent bones +-- -- Returns something like: #(0.0, 0.35, 1.0) +-- +fn AA_CalculateBonePercent boneChain = ( + -- Record distances between bones + distanceRec = #() + disAll = 0 + append distanceRec 0 + + -- Calculate cumulative distances + for i = 1 to boneChain.count-1 do ( + dis = length(boneChain[i+1].transform.pos - boneChain[i].transform.pos) + disAll += dis + append distanceRec disAll + ) + + -- Convert to percentages (0.0 to 1.0) + longthPer = #() + -- Avoid divide by zero when bones are at same position or only one bone + if disAll == 0 then disAll = 1.0 + for i = 1 to boneChain.count do ( + append longthPer (distanceRec[i] / disAll) + ) + + return longthPer +) + +-- Get total bone chain length +-- Calculates the total distance from first bone to last bone +-- +-- Parameters: +-- boneChain: Array of bone objects +-- +-- Returns: +-- Float value representing total chain length in scene units +-- +fn AA_GetBoneChainLength boneChain = ( + if boneChain == undefined or boneChain.count < 2 then ( + return 0.0 + ) + + totalLength = 0.0 + for i = 1 to boneChain.count-1 do ( + dis = length(boneChain[i+1].transform.pos - boneChain[i].transform.pos) + totalLength += dis + ) + + return totalLength +) + +-- # endregion + +-- # region Naming Utilities + +-- Generate unique name with suffix +-- Creates a unique object name by appending a suffix and number if needed +-- +-- Parameters: +-- baseName: Base name for the object (e.g., "Wave") +-- suffix: Suffix to append (e.g., "_Locator") +-- +-- Returns: +-- String with unique name (e.g., "Wave_Locator01" if "Wave_Locator" exists) +-- +fn AA_GenerateUniqueName baseName suffix = ( + proposedName = baseName + suffix + + -- Check if name already exists + if (getnodebyname proposedName) == undefined then ( + return proposedName + ) + + -- Find unique number suffix + counter = 1 + while counter < 1000 do ( + numberedName = proposedName + (formattedprint counter format:"02d") + if (getnodebyname numberedName) == undefined then ( + return numberedName + ) + counter += 1 + ) + + -- Fallback: use timestamp + return proposedName + (timestamp() as string) +) + +-- # endregion + +-- # region Module Info + +format "========================================\n" +format "Naming Utilities Module Loaded\n" +format "========================================\n" + +-- # endregion + diff --git a/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/utils/startup_installer.ms b/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/utils/startup_installer.ms new file mode 100644 index 0000000..9966de7 --- /dev/null +++ b/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/utils/startup_installer.ms @@ -0,0 +1,209 @@ +-- ============================================================================ +-- Auto Animation Tool - Startup Script Installer +-- ============================================================================ +-- This module handles automatic installation of startup scripts to ensure +-- global functions are available when 3ds Max opens a scene. +-- +-- Features: +-- - Auto-detect 3ds Max version and startup directory +-- - Install loader script to startup folder +-- - Register scene callbacks for function reloading +-- - Immediate fix for current session +-- +-- Version: 2.0.0 +-- ============================================================================ + +-- # region Global Variables + +global AA_STARTUP_SCRIPT_NAME = "aa_autoanim_startup.ms" +global AA_CALLBACK_ID = #AA_AutoAnimSceneLoad + +-- # endregion + +-- # region Utility Functions + +-- Normalize path separators to forward slashes +fn AA_NormalizePath pathStr = ( + substituteString pathStr "\\" "/" +) + +-- Get 3ds Max version string for display +fn AA_GetMaxVersionString = ( + local ver = (maxVersion())[1] + local year = 1998 + (ver / 1000) + return ("3ds Max " + (year as string)) +) + +-- # endregion + +-- # region Core Installation Functions + +-- Get the best startup directory (prefer user directory over system directory) +fn AA_GetBestStartupDir = ( + -- Try user startup scripts first (AppData, no admin required) + local userStartup = getDir #userStartupScripts + if userStartup != undefined and userStartup != "" do ( + format "[AA] User startup dir available: %\n" userStartup + return userStartup + ) + + -- Fallback to system startup scripts (may need admin) + local sysStartup = getDir #startupScripts + format "[AA] Using system startup dir: %\n" sysStartup + return sysStartup +) + +-- Check if startup script is already installed (check both locations) +fn AA_IsStartupInstalled = ( + local userStartup = getDir #userStartupScripts + local sysStartup = getDir #startupScripts + + -- Check user startup first + if userStartup != undefined and userStartup != "" do ( + local userFile = userStartup + "\\" + AA_STARTUP_SCRIPT_NAME + if doesFileExist userFile do return true + ) + + -- Check system startup + local sysFile = sysStartup + "\\" + AA_STARTUP_SCRIPT_NAME + return (doesFileExist sysFile) +) + +-- Create the startup loader script content +fn AA_CreateLoaderContent corePath = ( + local normalizedPath = AA_NormalizePath corePath + local content = "" + content += "-- ============================================================================\n" + content += "-- Auto Animation Tool - Startup Loader\n" + content += "-- ============================================================================\n" + content += "-- Auto-generated file. Do not edit manually.\n" + content += "-- This script loads core functions when 3ds Max starts.\n" + content += "-- Version: " + AA_VERSION + "\n" + content += "-- Generated: " + (localTime) + "\n" + content += "-- ============================================================================\n\n" + content += "(\n" + content += " local corePath = @\"" + normalizedPath + "\"\n" + content += " if doesFileExist corePath then (\n" + content += " fileIn corePath quiet:true\n" + content += " ) else (\n" + content += " format \"[AA Warning] Core file not found: \\%\\n\" corePath\n" + content += " )\n" + content += ")\n" + return content +) + +-- Install startup script to 3ds Max startup directory +fn AA_InstallStartupScript scriptDir = ( + local startupDir = AA_GetBestStartupDir() + local targetFile = startupDir + "\\" + AA_STARTUP_SCRIPT_NAME + local corePath = scriptDir + "core\\spline_curve_sampler.ms" + + format "[AA] Installing startup script...\n" + format "[AA] 3ds Max Version: %\n" (AA_GetMaxVersionString()) + format "[AA] Target Directory: %\n" startupDir + format "[AA] Target File: %\n" targetFile + + -- Check if core file exists + if not doesFileExist corePath do ( + format "[AA] ERROR: Core file not found: %\n" corePath + return false + ) + + -- Ensure target directory exists + if not doesDirectoryExist startupDir do ( + makeDir startupDir all:true + ) + + -- Create loader content + local loaderContent = AA_CreateLoaderContent corePath + + -- Write to startup directory + local f = createFile targetFile + if f != undefined then ( + format "%" loaderContent to:f + close f + format "[AA] ✓ Startup script installed successfully!\n" + return true + ) else ( + -- Failed to write - permission issue, show detailed help + format "[AA] ERROR: Failed to write startup script (permission denied?)\n" + format "[AA] Trying alternative solutions...\n" + + local msg = "无法安装启动脚本到:\n" + targetFile + "\n\n" + msg += "原因: 可能没有写入权限 (Program Files 目录需要管理员权限)\n\n" + msg += "解决方案:\n" + msg += "1. 以管理员身份运行 3ds Max 后再试一次\n" + msg += "2. 或手动创建以下文件:\n\n" + msg += "文件路径: " + targetFile + "\n\n" + msg += "文件内容 (复制下面代码):\n" + msg += "fileIn @\"" + (AA_NormalizePath corePath) + "\"\n\n" + msg += "注意: 回调机制仍然有效,只是没有启动脚本。" + messageBox msg title:"AA 安装需要手动操作" + return false + ) +) + +-- Register scene load callback +fn AA_RegisterSceneCallback scriptDir = ( + local corePath = scriptDir + "core\\spline_curve_sampler.ms" + local normalizedPath = AA_NormalizePath corePath + + -- Remove existing callback + callbacks.removeScripts id:AA_CALLBACK_ID + + -- Create callback script + local callbackScript = "if doesFileExist @\"" + normalizedPath + "\" do fileIn @\"" + normalizedPath + "\" quiet:true" + + -- Register with persistent flag + callbacks.addScript #filePostOpen callbackScript id:AA_CALLBACK_ID persistent:true + callbacks.addScript #filePostMerge callbackScript id:AA_CALLBACK_ID persistent:true + + format "[AA] ✓ Scene load callbacks registered\n" +) + +-- Load core module immediately (quick fix for current session) +fn AA_LoadCoreImmediate scriptDir = ( + local corePath = scriptDir + "core\\spline_curve_sampler.ms" + if doesFileExist corePath do ( + fileIn corePath quiet:true + format "[AA] ✓ Core module loaded for current session\n" + return true + ) + return false +) + +-- # endregion + +-- # region Main Installation Entry Point + +-- Main function to ensure startup script is properly installed +fn AA_EnsureStartupInstalled scriptDir = ( + format "\n[AA] Checking startup script installation...\n" + + -- Step 1: Check if already installed + local isInstalled = AA_IsStartupInstalled() + + if not isInstalled then ( + format "[AA] Startup script not found, installing...\n" + AA_InstallStartupScript scriptDir + ) else ( + format "[AA] ✓ Startup script already installed\n" + ) + + -- Step 2: Register scene callbacks (always do this) + AA_RegisterSceneCallback scriptDir + + -- Step 3: Load core module immediately (quick fix) + AA_LoadCoreImmediate scriptDir + + format "[AA] Startup check complete\n\n" +) + +-- # endregion + +-- # region Module Info + +format "[OK] utils/startup_installer.ms loaded\n" + +-- # endregion + diff --git a/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/utils/validation.ms b/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/utils/validation.ms new file mode 100644 index 0000000..1d47e1f --- /dev/null +++ b/_BsKeyTools/Scripts/BulletScripts/Quote/final_auto_animator/utils/validation.ms @@ -0,0 +1,127 @@ +-- ============================================================================ +-- Auto Animation Tool - Validation Module +-- ============================================================================ +-- This module provides parameter validation functions to ensure data integrity +-- and provide meaningful error messages to users. +-- +-- Version: 2.0.0 +-- Author: Auto Animation Tool Team +-- ============================================================================ + +-- # region Bone Chain Validation + +-- Validate bone chain selection +-- Checks if the selected bones form a valid chain for auto animation +-- +-- Parameters: +-- bones: Array of bone objects to validate +-- +-- Returns: +-- true if valid, false otherwise (displays error message) +-- +fn AA_ValidateBoneChain bones = ( + -- Check if bones array exists and has elements + if bones == undefined or bones.count == 0 then ( + messageBox "错误:请选择至少一个骨骼" title:"自己动 - 验证错误" + return false + ) + + -- Check if at least 2 bones for meaningful animation + -- (removed warning popup - allow single bone silently) + -- if bones.count < 2 then ( + -- messageBox "警告:建议选择至少2个骨骼以获得更好的动画效果" title:"自己动 - 警告" + -- ) + + -- Validate each bone is actually a bone or helper object + for bone in bones do ( + if bone == undefined then ( + messageBox "错误:选择中包含无效对象" title:"自己动 - 验证错误" + return false + ) + ) + + return true +) + +-- # endregion + +-- # region Parameter Validation + +-- Validate numeric parameter range +-- Checks if a parameter value is within acceptable range +-- +-- Parameters: +-- paramName: Name of the parameter (for error message) +-- value: Value to validate +-- minVal: Minimum acceptable value +-- maxVal: Maximum acceptable value +-- +-- Returns: +-- true if valid, false otherwise (displays error message) +-- +fn AA_ValidateParameter paramName value minVal maxVal = ( + if value < minVal or value > maxVal then ( + errorMsg = paramName + " 必须在 " + (minVal as string) + " 到 " + (maxVal as string) + " 之间\n" + errorMsg += "当前值: " + (value as string) + messageBox errorMsg title:"自己动 - 参数错误" + return false + ) + return true +) + +-- Validate loop parameter (must be positive) +-- Loop value represents animation cycle length in frames +-- +-- Parameters: +-- loopValue: Loop value to validate +-- +-- Returns: +-- true if valid, false otherwise +-- +fn AA_ValidateLoop loopValue = ( + if loopValue <= 0 then ( + messageBox "循环周期必须大于0帧" title:"自己动 - 参数错误" + return false + ) + + if loopValue > 10000 then ( + messageBox "警告:循环周期过大 (>10000帧),可能导致性能问题" title:"自己动 - 警告" + -- Allow but warn + ) + + return true +) + +-- Validate range parameter (amplitude) +-- Range value represents animation amplitude +-- +-- Parameters: +-- rangeValue: Range value to validate +-- +-- Returns: +-- true if valid, false otherwise +-- +fn AA_ValidateRange rangeValue = ( + if rangeValue < 0 then ( + messageBox "振幅范围不能为负数" title:"自己动 - 参数错误" + return false + ) + + if rangeValue > 1000 then ( + messageBox "警告:振幅范围过大 (>1000),可能导致不自然的动画" title:"自己动 - 警告" + -- Allow but warn + ) + + return true +) + +-- # endregion + +-- # region Module Info + +format "========================================\n" +format "Validation Module Loaded\n" +format "========================================\n" + +-- # endregion +