MaxScript Spinner/progressBar

本文介绍了一个基于滚动球体的交互式模拟程序,通过调整球体半径和数量,实现球体的随机移动和进度条显示,为用户提供直观的可视化体验。

rollout unnamedRollout "Untitled" width:162 height:235
(
 spinner spn1 "球体半径" pos:[18,14] width:119 height:16


 spinner spn2 "球体数目" pos:[15,49] width:119 height:16 type:#integer
 progressBar pb1 "进度条" pos:[12,97] width:123 height:18 color:(color 255 0 0)
 button btn4 "随机移动" pos:[16,142] width:110 height:25 type:#integer


 on spn1 changed val do
 (
  for a in selection where classof a == sphere do a.radius = spn1.value
 )
 on spn2 changed val do
 (
  c=#()
  for a in selection where classof a==sphere do append c a
  spn2.value = c.count
 )
 on btn4 pressed  do
 (
  for a in selection do
    for t=0 to 100 by 1 do
      animate on
        at time t
        (
          a.pos.x=(t*(random 1.0 5.0))
          pb1.value=t
        )
    pb1.value=0
 )
)

createdialog unnamedRollout

转载于:https://www.cnblogs.com/JimmyCode/archive/2011/11/16/2250797.html

运行报错 -- Error occurred in anonymous codeblock; filename: ; position: 2947; line: 77 -- MAXScript Rollout Handler Exception: -- Runtime error: callbacks.addScript() unrecognized callback type: #timeChange -- MAXScript callstack: -- thread data: threadID:17536 -- ------------------------------------------------------ -- [stack level: 0] -- In registerTimeCallback(); filename: ; position: 2948; line: 77 -- member of: Rollout:GIFPlayer -- Locals: -- callbackStr: "fn timeCallbackFn = ( try ( local dlg = ::GIFPlayer if isValidObj dlg and dlg.gifFrames.count > 0 do ( -- 获取当前时间轴位置 local currentTime = sliderTime.frame as integer -- 仅当时间变化时更新 if currentTime != dlg.lastSliderTime do ( dlg.lastSliderTime = currentTime dlg.currentFrame = currentTime + 1 -- 确保帧号在范围内 if dlg.currentFrame > dlg.frameCount do dlg.currentFrame = dlg.frameCount if dlg.currentFrame < 1 do dlg.currentFrame = 1 -- 更新显示 dlg.picBox.image = dlg.gifFrames[dlg.currentFrame] dlg.pbFrame.value = (dlg.currentFrame as float / dlg.frameCount * 100) dlg.lblStatus.text = "帧: " + dlg.currentFrame as string + "/" + dlg.frameCount as string ) ) ) catch() )" -- Externals: -- owner: Rollout:GIFPlayer -- callbackID: "GIFPlayer_TimeCallback_88775" -- GIFPlayer: Rollout:GIFPlayer -- ------------------------------------------------------ -- [stack level: 1] -- called from GIFPlayer.open(); filename: ; position: 4554; line: 136 -- member of: Rollout:GIFPlayer -- Locals: -- Externals: -- registerTimeCallback: registerTimeCallback() -- picBox: RolloutControl:picBox in rollout:GIFPlayer : dotNetControl:picBox:System.Windows.Forms.PictureBox -- owner: Rollout:GIFPlayer -- GIFPlayer: Rollout:GIFPlayer -- ------------------------------------------------------ -- [stack level: 2] -- called from top-level
07-16
//classDiagram // class Bancai { // Integer id // Caizhi caizhi // Mupi mupi1 // Mupi mupi2 // Double houdu // Kucun kucun // } // // class Caizhi { // String name // List~Bancai~ bancai // } // // class Mupi { // String name // Boolean you // List~Bancai~ bancaisForMupi1 // List~Bancai~ bancaisForMupi2 // } // // class Chanpin { // String bianhao // List~Chanpin_zujian~ chanpin_zujian // List~Dingdan_chanpin~ dingdan_chanpin // } // // class Zujian { // String name // List~Chanpin_zujian~ chanping_zujian // } // // class Chanpin_zujian { // Bancai bancai // Double one_howmany // Double zujianshu // } // // class Dingdan { // String number // Date xiadan // Date jiaohuo // List~Dingdan_chanpin~ dingdan_chanpin // } // // class Dingdan_chanpin { // Integer shuliang // } // // class Kucun { // Integer shuliang // Dingdan reservedOrder // } // // class Jinhuo { // Integer shuliang // Date date // } // // class User { // String name // String andy // String pass // int role // } // // Bancai “1” – “1” Caizhi // Bancai “1” – “1” Mupi : mupi1 // Bancai “1” – “1” Mupi : mupi2 // Bancai “1” – “1” Kucun // Caizhi “1” – “" Bancai // Mupi “1” – "” Bancai : mupi1 // Mupi “1” – “" Bancai : mupi2 // Chanpin “1” – "” Chanpin_zujian // Chanpin “1” – “" Dingdan_chanpin // Zujian “1” – "” Chanpin_zujian // Chanpin_zujian “1” – “1” Bancai // Dingdan “1” – “*” Dingdan_chanpin // Dingdan_chanpin “1” – “1” Chanpin // Kucun “0…1” – “1” Dingdan : reservedOrder // Jinhuo “1” – “1” Dingdan_bancai // Jinhuo “1” – “1” User function resolveDataReferences(data) { // 获取 data 对象的所有顶层键 const keys = Object.keys(data); // 遍历每个顶层键(如 users, posts 等) for (const key of keys) { const entities = data[key]; // 遍历该顶层键下的每个实体(如每个 user 或 post) for (const entity of entities) { // 遍历实体的每个属性 for (const attribute in entity) { if (entity?.hasOwnProperty(attribute)) { var trpe=attribute?.replace(/\d/g, ''); // 确保属性属于当前实体 if (Array.isArray(entity[attribute])) { if(data[trpe]==null){ trpe+="s" } // 如果属性是一个数组,则将数组中的每个 ID 替换为对应的实际对象 entity[attribute] = entity[attribute].map(item => data[trpe ]?.find(updateItem => updateItem.id === item.id) || item ); } else if (typeof entity[attribute] === "object" && entity[attribute] !== null) { // 如果属性是一个对象,则将其替换为对应的实际对象 entity[attribute] = data[trpe + "s"]?.find(updateItem => updateItem.id === entity[attribute].id); } } } } } console.log(data) return data; } /** 数据管理器类,负责与后端API通信并管理数据 */ class DataManager { constructor(baseUrl) { this.baseUrl = baseUrl; this.data = { bancais: [], dingdans: [], mupis: [], chanpins: [], kucuns: [], dingdan_bancais:[], chanpin_zujians: [], zujians: [], caizhis: [], dingdan_chanpins: [], users: [], jinhuos: [] }; this.isSyncing = false; this.lastSync = null; this.callbacks = { all: [], bancais: [], dingdan: [], mupi: [], chanpin: [], kucun: [], chanpin_zujian: [], dingdan_bancai:[], zujian: [], caizhi: [], dingdan_chanpin: [], user: [], jinhuo: [] }; this.syncQueue = Promise.resolve(); } /** 获取所有数据 @returns {Promise} 是否成功 */ async fetchAll() { console.log(this) try { const response = await fetch(${this.baseUrl}/app/all); if (!response.ok) throw new Error(‘Network response was not ok’); const result = await response.json(); if (result.status !== 200) throw new Error(result.text || ‘API error’); const resolvedData = resolveDataReferences(result.data); // 更新本地数据 Object.keys(this.data).forEach(key => { if (resolvedData[key]) { this.data[key] = resolvedData[key]; } }); this.lastSync = new Date(); // 关键改进:数据更新后触发刷新回调 this.triggerCallbacks(‘refresh’, ‘all’, this.data); return true; } catch (error) { console.error(‘Fetch error:’, error); // 触发错误回调 this.triggerCallbacks(‘fetch_error’, ‘all’, { error }); return false; } } /** 注册回调函数 @param {string} entity - 实体类型(如’bancai’)或’all’表示全局回调 @param {Function} callback - 回调函数,参数为(operation, data) */ registerCallback(entity, callback) { if (!this.callbacks[entity]) { this.callbacks[entity] = []; } this.callbacks[entity].push(callback); } /** 移除回调函数 @param {string} entity - 实体类型单数性质 @param {Function} callback - 要移除的回调函数 */ unregisterCallback(entity, callback) { if (!this.callbacks[entity]) return; const index = this.callbacks[entity].indexOf(callback); if (index !== -1) { this.callbacks[entity].splice(index, 1); } } /** 触发回调 @param {string} operation - 操作类型(‘add’, ‘update’, ‘delete’) @param {string} entity - 实体类型单数性质 @param {Object} data - 相关数据 */ triggerCallbacks(operation, entity, data) { // 触发全局回调 this.callbacks.all.forEach(cb => cb(operation, entity, data)); // 触发特定实体回调 if (this.callbacks[entity]) { this.callbacks[entity].forEach(cb => cb(operation, data)); } } /** 执行CRUD操作并触发回调 */ async crudOperation(operation, entity, data) { try { const response = await fetch(${this.baseUrl}/app/${operation}/${entity}, { method: ‘POST’, headers: {‘Content-Type’: ‘application/json’}, body: JSON.stringify(data) }); if (!response.ok) throw new Error(‘Network response was not ok’); const result = await response.json(); if (result.status !== 200) throw new Error(result.text || ‘API error’); // 自动同步数据 this.syncData(); // 触发操作成功的回调 this.triggerCallbacks(operation, entity, data); return result; } catch (error) { console.error(‘CRUD error:’, error); // 触发操作失败的回调 this.triggerCallbacks(${operation}_error, entity, { data, error: error.message }); throw error; } } /** 执行CRUD操作 @param {string} operation - ‘add’, ‘delete’, ‘update’ @param {string} entity - 实体名称单数性质(小写) @param {Object} data - 要发送的数据 后端要求数据格式为{属性: “值”, 关联对象: {id:0}, 关联对象集: [{id:0}]} @returns {Promise} 响应结果 */ async crudOperation(operation, entity, data) { try { const response = await fetch(${this.baseUrl}/app/${operation}/${entity}, { method: ‘POST’, headers: {‘Content-Type’: ‘application/json’}, body: JSON.stringify(data) }); if (!response.ok) throw new Error('Network response was not ok'); const result = await response.json(); if (result.status !== 200) throw new Error(result.text || 'API error'); // 自动同步数据 this.syncQueue = this.syncQueue.then(async () => { await this.syncData(); // 同步完成后触发操作回调 this.triggerCallbacks(operation, entity, data); }); return result; } catch (error) { console.error(‘CRUD error:’, error); // 触发操作失败的回调 this.triggerCallbacks(${operation}_error, entity, { data, error: error.message }); throw error; } } /** 自动同步数据(防止频繁请求) */ async syncData() { if (this.isSyncing) { this.pendingSync = true; return; } this.isSyncing = true; try { await this.fetchAll(); } catch (error) { console.error(‘Sync failed:’, error); } finally { this.isSyncing = false; // 处理等待中的同步请求 if (this.pendingSync) { this.pendingSync = false; setTimeout(() => this.syncData(), 1000); } } } /** 添加实体 @param {string} entity - 实体名称单数性质 @param {Object} data - 实体数据 */ async addEntity(entity, data) { return this.crudOperation(‘add’, entity, data); } /** 更新实体 @param {string} entity - 实体名称单数性质 @param {Object} data - 实体数据(必须包含id) */ async updateEntity(entity, data) { return this.crudOperation(‘update’, entity, data); } /** 删除实体 @param {string} entity - 实体名称单数性质 @param {number} id - 实体ID */ async deleteEntity(entity, id) { return this.crudOperation(‘delete’, entity, {id}); } /** 新增方法:手动触发数据刷新 */ async refreshData() { return this.syncQueue = this.syncQueue.then(() => this.syncData()); } } export { DataManager }; // 创建单例实例 //const dataManager = new DataManager(‘http://127.0.0.1:8080/KuCun2’); //// 初始化时获取所有数据 //dataManager.fetchAll().then(() => { // console.log(‘Initial data loaded’); //}); // 导出数据对象,外部可以直接访问 data.bancais, data.dingdans 等 //export const data = dataManager.data; //// 导出操作方法 //export const addEntity = dataManager.addEntity.bind(dataManager); //export const updateEntity = dataManager.updateEntity.bind(dataManager); //export const deleteEntity = dataManager.deleteEntity.bind(dataManager); //export const fetchAll = dataManager.fetchAll.bind(dataManager); <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>板材库存查询系统</title> <!-- 引入外部CSS --> <link rel="stylesheet" href="../css/dingdan.css"> <!-- 引入jQuery --> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <!-- 引入Bootstrap --> <link href="../js/bootstrap-5.3.0-alpha1-dist/css/bootstrap.min.css" rel="stylesheet"> <script src="../js/bootstrap-5.3.0-alpha1-dist/umd/popper.min.js"></script> <script src="../js/bootstrap-5.3.0-alpha1-dist/js/bootstrap.min.js"></script> <!-- 引入Bootstrap Icons --> <link rel="stylesheet" href="../js/bootstrap-icons-1.8.1/bootstrap-icons.css"> <!-- 引入主JS文件 --> <script src="../js/main.js"></script> </head> <body> <div class="container py-4"> <!-- 标题部分 --> <div class="text-center mb-4"> <h1 class="text-primary"><i class="bi bi-boxes"></i> 板材库存管理系统</h1> <p class="text-muted">查询订单、产品、板材及库存信息</p> </div> <!-- 统计卡片 --> <div class="row mb-4"> <div class="col-md-3"> <div class="card stats-card border-primary"> <div class="card-body"> <h5 class="card-title">订单总数</h5> <p class="card-text fs-3 text-primary" id="orderCount">0</p> </div> </div> </div> <div class="col-md-3"> <div class="card stats-card border-info"> <div class="card-body"> <h5 class="card-title">产品种类</h5> <p class="card-text fs-3 text-info" id="productCount">0</p> </div> </div> </div> <div class="col-md-3"> <div class="card stats-card border-success"> <div class="card-body"> <h5 class="card-title">板材库存</h5> <p class="card-text fs-3 text-success" id="materialCount">0</p> </div> </div> </div> <div class="col-md-3"> <div class="card stats-card border-warning"> <div class="card-body"> <h5 class="card-title">库存总量</h5> <p class="card-text fs-3 text-warning" id="totalStock">0</p> </div> </div> </div> </div> <!-- 搜索区域 --> <div class="card search-section mb-4"> <div class="card-header"> <h5 class="mb-0"><i class="bi bi-search me-2"></i>高级搜索</h5> </div> <div class="card-body"> <div class="row g-3"> <!-- 订单搜索 --> <div class="col-md-4"> <div class="search-control"> <i class="bi bi-clipboard-search search-icon"></i> <input type="text" class="form-control with-icon" id="orderSearch" placeholder="搜索订单号..." aria-label="订单号搜索"> </div> </div> <!-- 产品搜索 --> <div class="col-md-4"> <div class="search-control"> <i class="bi bi-grid search-icon"></i> <input type="text" class="form-control with-icon" id="productSearch" placeholder="搜索产品编号..." aria-label="产品编号搜索"> </div> </div> <!-- 板材搜索 --> <div class="col-md-4"> <div class="search-control"> <i class="bi bi-box search-icon"></i> <input type="text" class="form-control with-icon" id="materialSearch" placeholder="搜索板材ID或材质..." aria-label="板材搜索"> </div> </div> <!-- 木皮搜索 --> <div class="col-md-4"> <div class="search-control"> <i class="bi bi-tree search-icon"></i> <input type="text" class="form-control with-icon" id="woodSearch" placeholder="搜索木皮名称..." aria-label="木皮搜索"> </div> </div> <!-- 厚度搜索 --> <div class="col-md-4"> <div class="search-control"> <i class="bi bi-arrows-vertical search-icon"></i> <input type="number" class="form-control with-icon" id="thicknessSearch" placeholder="厚度(mm)" min="0" step="0.1"> </div> </div> <!-- 库存范围搜索 --> <div class="col-md-4"> <div class="input-group"> <span class="input-group-text"><i class="bi bi-box"></i></span> <input type="number" class="form-control" id="minStock" placeholder="最小库存" min="0"> <input type="number" class="form-control" id="maxStock" placeholder="最大库存" min="0"> <button class="btn btn-primary" type="button" id="stockStatusBtn"> <i class="bi bi-search"></i> </button> </div> </div> </div> </div> </div> <!-- 结果区域 --> <div class="card"> <div class="card-header d-flex justify-content-between align-items-center"> <h5 class="mb-0"><i class="bi bi-table me-2"></i>查询结果</h5> <div class="text-secondary"> <span id="resultCount">0</span> 条记录 <span class="ms-2"><i class="bi bi-info-circle"></i> <small>实时数据更新时间: <span id="lastUpdate">--:--:--</span></small></span> </div> </div> <div class="card-body result-section"> <div class="table-responsive"> <table class="table table-hover" id="resultsTable"> <thead class="table-light sticky-top"> <tr> <th data-sortable>订单号 <span class="sort-indicator"></span></th> <th data-sortable>产品信息 <span class="sort-indicator"></span></th> <th data-sortable>产品数量 <span class="sort-indicator"></span></th> <th data-sortable>组件 <span class="sort-indicator"></span></th> <th data-sortable>板材 <span class="sort-indicator"></span></th> <th data-sortable>单件用量 <span class="sort-indicator"></span></th> <th data-sortable>订单用量 <span class="sort-indicator"></span></th> <th data-sortable>库存数量 <span class="sort-indicator"></span></th> <th>操作</th> </tr> </thead> <tbody id="resultBody"> <!-- 数据加载中 --> <tr id="loadingRow"> <td colspan="9" class="text-center py-5"> <div class="d-flex align-items-center justify-content-center"> <div class="spinner-border text-primary" role="status"> <span class="visually-hidden">加载中...</span> </div> <div class="ms-3">正在加载数据,请稍候...</div> </div> </td> </tr> </tbody> </table> </div> <!-- 空结果提示 --> <div id="noResults" class="no-results text-center py-5" style="display: none;"> <div> <i class="bi bi-inboxes text-muted" style="font-size: 3rem;"></i> <h4 class="mt-3 text-muted">没有找到匹配的记录</h4> <p class="text-muted">请尝试调整您的搜索条件</p> </div> </div> </div> </div> </div> <!-- 引入订单JS --> <script src="../js/test.js"></script> </body> </html> 实现页面功能,DataManager.js已经在父级页面加载过了,直接到父级调用,js和css分开文件 业务基本逻辑 一个订单有很多产品不同数量,每个产品可以再很多订单中,一个产品有很多组件,一个组件可以在很多产品中,因为每个组件因为在不同的产品中有不同的生产工艺,所以使用不同的板材和板材能生产组件数量,每个板材有不同的材质和两面木皮,木皮表面可能有油漆, 订购时可能直接购入板材,也可能按订单和产品订购板材,也用可能按订单产品组件订购板材,每次采购不标准,一个订单可能订购几次,用户有姓名 账号 密码 权限, 一个记录进货和消耗,查看的时候会查看订单下有多少板材可用
06-24
2个脚本整合。一个批量渲染序列帧工具、一个批量生成GIF工具。 批量渲染完成后 去掉渲染完成提示 去掉打开文件夹 直接自动批量生成GIF,GIF输出目录 默认为 批量渲染序列帧文件夹 的 父目录。接下来我会给两个脚本。 脚本1:try(destroyDialog renderSeqRollout)catch() rollout renderSeqRollout "批量渲染序列帧工具 V5.4" width:423 height:500 ( -- 单场景渲染控件 groupBox grpSingle "单场景渲染设置" width:390 height:70 pos:[15, 10] checkbox chk_useActiveRange "使用时间滑块范围" checked:true align:#left offset:[10,-50] across:2 checkbox chk_openFolder "渲染后打开文件夹" checked:true align:#right offset:[-120,-50] spinner spn_startFrame "起始帧:" range:[-99999,99999,0] type:#integer fieldWidth:50 offset:[10,0] across:2 spinner spn_endFrame "结束帧:" range:[-99999,99999,100] type:#integer fieldWidth:50 offset:[-90,0] -- 渲染尺寸设置 groupBox grpRenderSize "渲染尺寸设置" width:390 height:90 pos:[15, 85] checkbox chk_customSize "使用自定义尺寸" checked:false align:#left offset:[10,-30] checkbox chk_lockAspect "锁定宽高比" checked:false align:#right offset:[-120,-30] across:2 spinner spn_width "宽度:" range:[1,10000,renderWidth] type:#integer fieldWidth:50 offset:[10,0] across:2 spinner spn_height "高度:" range:[1,10000,renderHeight] type:#integer fieldWidth:50 offset:[-90,0] -- 批量渲染控件 groupBox grpBatch "批量渲染设置" width:390 height:200 pos:[15, 180] checkbox chk_batchMode "批量渲染模式" checked:false align:#left offset:[10,-185] label lbl_batchPath "当前文件夹路径:" align:#left offset:[10,10] across:2 editText edt_batchPath "" width:230 height:20 align:#right offset:[-150,8] readOnly:true button btn_refresh "刷新" width:50 height:20 align:#right offset:[-90,-25] listBox lst_files "MAX文件列表" height:6 width:370 pos:[20, 265] -- 通用控件 button btn_render "渲染序列帧" width:390 height:40 pos:[15, 405] progressBar pb_progress "进度条:" color:(color 0 128 255) height:15 pos:[10, 455] label lbl_status "状态: 准备就绪" align:#left offset:[0,5] pos:[15, 475] -- 局部变量 local frameCount = 0 local renderedFrames = 0 local outputDir = "" local outputPattern = "" local sceneName = "" local batchFiles = #() local currentBatchIndex = 1 local batchTotalFiles = 0 local originalScenePath = "" local originalSceneFile = "" local aspectRatio = 1.0 -- 用于存储宽高比 -- 扫描当前文件夹中的MAX文件 fn scanMaxFiles = ( local currentPath = maxFilePath if currentPath != "" then ( edt_batchPath.text = currentPath batchFiles = getFiles (currentPath + "\\*.max") lst_files.items = for f in batchFiles collect filenameFromPath f lbl_status.text = "状态: 找到 " + (batchFiles.count as string) + " 个MAX文件" ) else ( lbl_status.text = "状态: 场景未保存,请先保存场景" edt_batchPath.text = "" lst_files.items = #() ) ) -- 初始化时设置正确的帧范围和渲染尺寸 on renderSeqRollout open do ( spn_startFrame.value = animationRange.start.frame as integer spn_endFrame.value = animationRange.end.frame as integer spn_width.value = renderWidth spn_height.value = renderHeight -- 计算初始宽高比 if renderHeight > 0 do aspectRatio = renderWidth as float / renderHeight as float -- 设置控件状态 spn_startFrame.enabled = not chk_useActiveRange.checked spn_endFrame.enabled = not chk_useActiveRange.checked spn_width.enabled = chk_customSize.checked spn_height.enabled = chk_customSize.checked scanMaxFiles() -- 自动扫描当前文件夹 ) on chk_useActiveRange changed state do ( spn_startFrame.enabled = not state spn_endFrame.enabled = not state if state do ( spn_startFrame.value = animationRange.start.frame as integer spn_endFrame.value = animationRange.end.frame as integer ) ) -- 自定义尺寸复选框事件 on chk_customSize changed state do ( spn_width.enabled = state spn_height.enabled = state chk_lockAspect.enabled = state ) -- 更新宽高比锁定 fn updateAspectLock = ( if chk_lockAspect.checked and spn_height.value > 0 do ( aspectRatio = spn_width.value as float / spn_height.value as float ) ) -- 宽度变化事件 on spn_width changed val do ( if chk_lockAspect.checked and aspectRatio > 0 and chk_customSize.checked then ( local newHeight = (val / aspectRatio) as integer if newHeight > 0 do ( spn_height.value = newHeight ) ) else ( updateAspectLock() ) ) -- 高度变化事件 on spn_height changed val do ( if chk_lockAspect.checked and aspectRatio > 0 and chk_customSize.checked then ( local newWidth = (val * aspectRatio) as integer if newWidth > 0 do ( spn_width.value = newWidth ) ) else ( updateAspectLock() ) ) -- 锁定宽高比复选框事件 on chk_lockAspect changed state do ( if state and spn_height.value > 0 do ( aspectRatio = spn_width.value as float / spn_height.value as float ) ) -- 获取时间滑块的实际范围 fn getActualTimeRange = ( local actualStart = animationRange.start.frame as integer local actualEnd = animationRange.end.frame as integer return #(actualStart, actualEnd) ) -- 刷新文件列表按钮事件 on btn_refresh pressed do ( scanMaxFiles() ) -- 渲染单帧函数 fn renderSingleFrame frameIndex = ( -- 设置当前帧 sliderTime = frameIndex -- 生成当前帧的文件名 frameNumber = formattedPrint frameIndex format:"06d" frameFile = substituteString outputPattern "######" frameNumber -- 执行渲染(禁用所有窗口) render outputfile:frameFile vfb:false progressbar:false quiet:true -- 更新进度 renderedFrames += 1 pb_progress.value = (100.0 * renderedFrames / frameCount) lbl_status.text = "状态: 渲染中 (" + renderedFrames as string + "/" + frameCount as string + ")" ) -- 渲染当前场景 fn renderCurrentScene = ( -- 检查场景是否保存 if maxFileName == "" then ( lbl_status.text = "状态: 错误 - 场景未保存!" messageBox "请先保存场景文件!" title:"错误" return false ) -- 获取场景信息 sceneName = getFilenameFile maxFileName local scenePath = maxFilePath -- 创建输出目录 outputDir = scenePath + sceneName if not doesFileExist outputDir then ( makeDir outputDir lbl_status.text = "状态: 创建目录 - " + outputDir ) -- 获取实际时间范围 local actualRange = getActualTimeRange() -- 设置渲染范围 local startFrame, endFrame if chk_useActiveRange.checked then ( startFrame = actualRange[1] endFrame = actualRange[2] ) else ( startFrame = spn_startFrame.value endFrame = spn_endFrame.value ) -- 计算帧数 frameCount = endFrame - startFrame + 1 if frameCount <= 0 then ( lbl_status.text = "状态: 错误 - 无效帧范围!" messageBox "帧范围无效!起始帧不能大于结束帧。" title:"错误" return false ) -- 创建输出文件名模式(JPG格式) outputPattern = outputDir + "\\" + sceneName + "_" + "######.jpg" -- 保存当前渲染器设置 local oldRenderer = renderers.current -- 将渲染器设置为Scanline Renderer renderers.current = Default_Scanline_Renderer() -- 检查渲染器 if renderers.current == undefined then ( lbl_status.text = "状态: 错误 - 没有激活的渲染器!" messageBox "没有激活的渲染器!请选择渲染器。" title:"错误" return false ) -- 保存当前渲染设置 local oldOutput = rendOutputFilename local oldSaveFile = rendSaveFile local oldTimeType = rendTimeType local oldNThFrame = rendNThFrame local oldStart = rendStart local oldEnd = rendEnd -- 保存当前渲染尺寸 local oldWidth = renderWidth local oldHeight = renderHeight -- 如果启用自定义尺寸,设置新的渲染尺寸 if chk_customSize.checked then ( renderWidth = spn_width.value renderHeight = spn_height.value ) -- 设置渲染参数 rendOutputFilename = outputPattern rendSaveFile = true rendTimeType = 3 -- 渲染范围 rendNThFrame = 1 -- 每帧渲染 rendStart = startFrame rendEnd = endFrame -- 提交渲染设置 renderSceneDialog.update() renderSceneDialog.commit() -- 逐帧渲染 renderedFrames = 0 local success = true for f = startFrame to endFrame do ( try ( renderSingleFrame f ) catch ( lbl_status.text = "状态: 错误 - 渲染帧 " + f as string + " 时失败" messageBox ("渲染帧 " + f as string + " 时失败:\n" + getCurrentException()) title:"渲染错误" success = false exit ) ) -- 恢复原始渲染设置 rendOutputFilename = oldOutput rendSaveFile = oldSaveFile rendTimeType = oldTimeType rendNThFrame = oldNThFrame rendStart = oldStart rendEnd = oldEnd renderers.current = oldRenderer -- 恢复原始渲染尺寸 renderWidth = oldWidth renderHeight = oldHeight renderSceneDialog.commit() return success ) -- 渲染按钮事件 on btn_render pressed do ( -- 重置状态 pb_progress.value = 0 renderedFrames = 0 setWaitCursor() if chk_batchMode.checked then ( -- 批量渲染模式 if batchFiles.count == 0 then ( lbl_status.text = "状态: 错误 - 未找到MAX文件!" messageBox "当前文件夹中没有找到MAX文件!" title:"错误" return false ) -- 保存当前场景信息 originalScenePath = maxFilePath originalSceneFile = maxFileName batchTotalFiles = batchFiles.count currentBatchIndex = 1 lbl_status.text = "状态: 开始批量渲染 (" + batchTotalFiles as string + " 个文件)..." for i = 1 to batchFiles.count do ( local currentFile = batchFiles[i] currentBatchIndex = i -- 更新文件进度 pb_progress.value = (100.0 * (i-1) / batchTotalFiles) lbl_status.text = "状态: 正在渲染文件 " + i as string + "/" + batchTotalFiles as string + ": " + filenameFromPath currentFile -- 加载MAX文件 loadMaxFile currentFile quiet:true -- 渲染当前场景 local success = renderCurrentScene() if not success then ( messageBox ("渲染失败: " + currentFile) title:"错误" exit ) ) -- 恢复原始场景 if originalSceneFile != "" then ( local originalFilePath = originalScenePath + originalSceneFile if doesFileExist originalFilePath then ( loadMaxFile originalFilePath quiet:true lbl_status.text = "状态: 已恢复原始场景" ) ) pb_progress.value = 100 lbl_status.text = "状态: 批量渲染完成! (" + batchTotalFiles as string + " 个文件)" messageBox ("批量渲染成功完成!\n共渲染了 " + batchTotalFiles as string + " 个场景") title:"渲染成功" ) else ( -- 单场景渲染模式 lbl_status.text = "状态: 开始渲染当前场景..." local success = renderCurrentScene() if success then ( pb_progress.value = 100 lbl_status.text = "状态: 渲染完成! (" + frameCount as string + " 帧)" -- 可选:打开输出目录 if chk_openFolder.checked and doesFileExist outputDir then ( shellLaunch "explorer.exe" outputDir ) messageBox ("渲染成功完成!\n共渲染了 " + frameCount as string + " 帧\n输出目录: " + outputDir) title:"渲染成功" ) else ( pb_progress.value = 0 ) ) setArrowCursor() ) ) createdialog renderSeqRollout 脚本2:rollout gifMakerRollout "GIF生成工具 - 多文件夹处理" width:400 height:420 ( -- UI控件声明 groupBox grpSettings "设置" pos:[10,10] width:380 height:200 editText edtFFmpegPath "FFmpeg路径:" pos:[20,35] fieldWidth:300 labelOnTop:true text:"" -- 初始为空 button btnBrowseFFmpeg "浏览..." pos:[330,35] width:40 height:20 editText edtParentFolder "父文件夹路径:" pos:[20,75] fieldWidth:300 labelOnTop:true text:(getDir #export + "\\") -- 默认导出目录 button btnBrowseParentFolder "浏览..." pos:[330,75] width:40 height:20 label lblPatternHint "图片序列格式:" pos:[20,115] width:100 height:20 dropdownlist ddlPattern "序列格式" pos:[20,135] width:300 height:40 items:#("自动检测序列帧", "手动输入格式...") editText edtCustomPattern "" pos:[20,175] fieldWidth:300 height:20 visible:false button btnTestPattern "测试格式" pos:[330,175] width:40 height:20 visible:false groupBox grpAnimation "动画设置" pos:[10,220] width:380 height:80 spinner spnFrameRate "帧率 (fps):" pos:[20,240] range:[1,60,30] type:#integer fieldWidth:50 spinner spnSpeedFactor "速度因子:" pos:[150,240] range:[0.1,10.0,1.0] type:#float scale:0.1 fieldWidth:50 label lblSpeedHint "1.0=正常 | 2.0=2倍速 | 0.5=半速" pos:[210,240] width:150 height:20 checkbox chkAutoOpen "生成后打开文件夹" pos:[20,280] checked:true progressBar pbProgress "" pos:[10,310] width:380 height:20 value:0 color:(color 0 255 0) label lblStatus "" pos:[10,335] width:380 height:20 button btnGenerate "批量生成GIF" pos:[100,360] width:100 height:30 button btnCancel "取消" pos:[220,360] width:100 height:30 -- 自动查找脚本目录下的FFmpeg fn findFFmpegInScriptDir = ( -- 获取脚本文件路径 local scriptPath = getSourceFileName() if scriptPath == undefined do return "" -- 获取脚本所在目录 local scriptDir = getFilenamePath scriptPath -- 检查常见位置 local possiblePaths = #( scriptDir + "ffmpeg.exe", -- 直接在脚本目录 scriptDir + "ffmpeg\\bin\\ffmpeg.exe", -- 在ffmpeg子目录 scriptDir + "bin\\ffmpeg.exe", -- 在bin子目录 scriptDir + "tools\\ffmpeg.exe" -- 在tools子目录 ) -- 查找存在的路径 for path in possiblePaths do ( if doesFileExist path do return path ) return "" ) -- 辅助函数:验证路径是否存在 fn validatePath path isFile:false = ( if path == "" do ( messageBox "路径不能为空!" title:"错误" return false ) if isFile then ( if not doesFileExist path then ( messageBox ("路径不存在: \n" + path) title:"错误" return false ) ) else ( if not doesDirectoryExist path then ( messageBox ("文件夹不存在: \n" + path) title:"错误" return false ) ) return true ) -- 获取图片分辨率 - 更可靠的方法 fn getImageResolution imgPath = ( if doesFileExist imgPath then ( try ( -- 使用openBitmap方法获取分辨率 local bmp = openBitmap imgPath if bmp != undefined then ( local w = bmp.width local h = bmp.height close bmp return [w, h] ) ) catch ( format "无法读取图片分辨率: %\n" imgPath ) ) return [0,0] ) -- 检测序列帧模式并获取分辨率 fn detectSequencePatternAndResolution folderPath = ( local sequenceFiles = #() if not doesDirectoryExist folderPath do return #("", [0,0]) -- 获取所有图片文件 imgFiles = getFiles (folderPath + "*.jpg") join imgFiles (getFiles (folderPath + "*.jpeg")) join imgFiles (getFiles (folderPath + "*.png")) join imgFiles (getFiles (folderPath + "*.bmp")) join imgFiles (getFiles (folderPath + "*.tga")) join imgFiles (getFiles (folderPath + "*.tif")) join imgFiles (getFiles (folderPath + "*.tiff")) if imgFiles.count == 0 do return #("", [0,0]) -- 按文件名排序 sort imgFiles -- 分析文件名模式 patterns = #() resolutions = #() for i = 1 to imgFiles.count do ( fileName = filenameFromPath imgFiles[i] baseName = getFilenameFile fileName fileExt = getFilenameType fileName -- 查找数字序列部分 digits = "" prefix = "" suffix = "" digitCount = 0 digitStart = 0 -- 查找数字序列 for j = baseName.count to 1 by -1 do ( char = baseName[j] if (findString "0123456789" char) != undefined then ( digits = char + digits digitCount += 1 digitStart = j ) else ( if digitCount > 0 do exit ) ) -- 如果找到数字序列 if digitCount > 0 then ( prefix = substring baseName 1 (digitStart - 1) suffix = substring baseName (digitStart + digitCount) -1 -- 生成序列模式 pattern = prefix + "%0" + (digitCount as string) + "d" + suffix + fileExt append patterns pattern -- 获取图片分辨率(只取第一张) if i == 1 then ( resolution = getImageResolution imgFiles[i] append resolutions resolution ) ) ) if patterns.count == 0 do return #("", [0,0]) -- 找出最常见的模式 patternCounts = #() for p in patterns do ( index = findItem patternCounts p if index == 0 then ( append patternCounts p append patternCounts 1 ) else ( patternCounts[index+1] += 1 ) ) -- 选择出现次数最多的模式 maxCount = 0 bestPattern = "" for i = 1 to patternCounts.count by 2 do ( if patternCounts[i+1] > maxCount then ( maxCount = patternCounts[i+1] bestPattern = patternCounts[i] ) ) -- 转换为FFmpeg格式 (使用两个%表示单个%) if bestPattern != "" do ( bestPattern = substituteString bestPattern "%0" "%%0" ) -- 返回最佳模式和分辨率 return #(bestPattern, resolutions[1]) ) -- 测试序列格式 fn testSequencePattern folderPath pattern = ( if not doesDirectoryExist folderPath do return false -- 转换为搜索模式 searchPattern = substituteString pattern "%%0" "*" searchPattern = substituteString searchPattern "d" "*" -- 查找匹配文件 matchedFiles = getFiles (folderPath + searchPattern) return matchedFiles.count > 0 ) -- 浏览FFmpeg路径 on btnBrowseFFmpeg pressed do ( local path = getOpenFileName caption:"选择FFmpeg可执行文件" \ filename:edtFFmpegPath.text \ types:"可执行文件(*.exe)|*.exe" if path != undefined do edtFFmpegPath.text = path ) -- 浏览父文件夹 on btnBrowseParentFolder pressed do ( local path = getSavePath caption:"选择父文件夹" \ initialDir:edtParentFolder.text if path != undefined do ( folderPath = path + "\\" edtParentFolder.text = folderPath ) ) -- 序列格式下拉列表改变事件 on ddlPattern selected sel do ( if sel == 2 then ( edtCustomPattern.visible = true btnTestPattern.visible = true edtCustomPattern.text = "例如: image_%%03d.jpg" ) else ( edtCustomPattern.visible = false btnTestPattern.visible = false ) ) -- 测试序列格式按钮 on btnTestPattern pressed do ( folderPath = edtParentFolder.text if not doesDirectoryExist folderPath do ( messageBox "文件夹不存在!" title:"错误" return false ) -- 获取第一个子文件夹进行测试 subFolders = getDirectories (folderPath + "*") if subFolders.count == 0 do ( messageBox "父文件夹中没有子文件夹!" title:"错误" return false ) testFolder = subFolders[1] pattern = edtCustomPattern.text if testSequencePattern testFolder pattern then ( messageBox ("格式有效!\n在第一个子文件夹中检测到匹配文件。") title:"测试结果" ) else ( messageBox ("未找到匹配文件!\n请检查格式是否正确。") title:"测试结果" ) ) -- 生成GIF (使用setpts滤镜实现真正的速度控制) on btnGenerate pressed do ( -- 获取UI中的值 ffmpegPath = edtFFmpegPath.text parentFolder = edtParentFolder.text frameRate = spnFrameRate.value speedFactor = spnSpeedFactor.value -- 获取序列格式 if ddlPattern.selection == 1 then ( useAutoPattern = true ) else ( useAutoPattern = false manualPattern = edtCustomPattern.text ) -- 验证FFmpeg路径 if ffmpegPath == "" do ( messageBox "请设置FFmpeg路径!" title:"错误" return false ) if not validatePath ffmpegPath isFile:true do return false if not validatePath parentFolder do return false -- 确保文件夹以反斜杠结尾 if not matchPattern parentFolder pattern:"*\\" do parentFolder += "\\" -- 获取所有子文件夹 subFolders = getDirectories (parentFolder + "*") if subFolders.count == 0 do ( messageBox "父文件夹中没有子文件夹!" title:"错误" return false ) -- 初始化进度条 pbProgress.value = 0 pbProgress.color = (color 0 255 0) lblStatus.text = "准备开始处理..." totalFolders = subFolders.count processedFolders = 0 skippedFolders = 0 generatedGifs = #() -- 处理每个子文件夹 for folder in subFolders do ( -- 更新状态 folderName = getFilenameFile (trimRight folder "\\") lblStatus.text = "处理中: " + folderName processedFolders += 1 pbProgress.value = (100.0 * processedFolders / totalFolders) -- 强制更新UI windows.processPostedMessages() -- 检测序列模式并获取分辨率 local patternAndRes if useAutoPattern then ( patternAndRes = detectSequencePatternAndResolution folder imgPattern = patternAndRes[1] resolution = patternAndRes[2] if imgPattern == "" then ( format "跳过文件夹 [%] - 未检测到序列帧\n" folderName skippedFolders += 1 continue ) if resolution.x == 0 or resolution.y == 0 then ( format "跳过文件夹 [%] - 无法获取分辨率\n" folderName skippedFolders += 1 continue ) ) else ( imgPattern = manualPattern -- 尝试获取第一张图片的分辨率 searchPattern = substituteString imgPattern "%%0" "000" searchPattern = substituteString searchPattern "d" "*" imgFiles = getFiles (folder + searchPattern) if imgFiles.count > 0 then ( resolution = getImageResolution imgFiles[1] if resolution.x == 0 or resolution.y == 0 then ( format "跳过文件夹 [%] - 无法获取分辨率\n" folderName skippedFolders += 1 continue ) ) else ( format "跳过文件夹 [%] - 没有匹配的图片文件\n" folderName skippedFolders += 1 continue ) ) -- 构建输入输出路径 fullInputPath = folder + imgPattern fullOutputPath = parentFolder + folderName + ".gif" -- 删除已存在的GIF文件以确保覆盖 if doesFileExist fullOutputPath do ( deleteFile fullOutputPath format "已删除旧文件: %\n" fullOutputPath ) -- 使用setpts滤镜实现真正的速度控制 setptsFilter = "setpts=PTS/" + (speedFactor as string) scaleFilter = "scale=" + (resolution.x as string) + ":" + (resolution.y as string) + ":flags=lanczos" -- 构建完整的ffmpeg命令 ffmpegCmd = "\"" + ffmpegPath + "\" -y -framerate " + (frameRate as string) + \ " -i \"" + fullInputPath + "\" " + \ "-vf \"" + setptsFilter + "," + scaleFilter + "\" " + \ "-r " + (frameRate as string) + " " + \ "-loop 0 \"" + fullOutputPath + "\"" -- 输出命令到监听器以便调试 format "处理文件夹 [%]:\n命令: %\n" folderName ffmpegCmd lblStatus.text = "处理中: " + folderName + " (" + resolution.x as string + "x" + resolution.y as string + ")" -- 创建临时批处理文件 tempBatFile = getDir #temp + "\\make_gif_" + (random 1 10000) as string + ".bat" batContent = "@echo off\n" + ffmpegCmd -- 创建并写入批处理文件 success = false f = createFile tempBatFile if f != undefined then ( format "%" batContent to:f close f success = true ) else ( format "无法创建临时批处理文件!\n" ) -- 执行批处理文件 (隐藏窗口) if success do ( exitCode = HiddenDosCommand ("\"" + tempBatFile + "\"") format "退出代码: %\n" exitCode -- 检查输出文件是否存在 if doesFileExist fullOutputPath then ( append generatedGifs fullOutputPath format "成功生成GIF: %\n" fullOutputPath ) else ( format "生成GIF失败: %\n" fullOutputPath ) -- 清理临时文件 if doesFileExist tempBatFile do deleteFile tempBatFile ) ) -- 完成进度条 pbProgress.value = 100 lblStatus.text = "处理完成! 已生成 " + (generatedGifs.count as string) + " 个GIF" -- 如果需要,打开输出文件夹 if chkAutoOpen.checked and generatedGifs.count > 0 do ( shellLaunch "explorer.exe" ("/e,\"" + parentFolder + "\"") ) messageBox ("成功生成 " + (generatedGifs.count as string) + " 个GIF!\n跳过 " + (skippedFolders as string) + " 个文件夹。") title:"完成" ) -- 取消按钮 on btnCancel pressed do ( DestroyDialog gifMakerRollout ) -- 初始化UI on gifMakerRollout open do ( ddlPattern.items = #("自动检测序列帧", "手动输入格式...") -- 自动查找FFmpeg路径 local autoFFmpegPath = findFFmpegInScriptDir() if autoFFmpegPath != "" then ( edtFFmpegPath.text = autoFFmpegPath format "自动设置FFmpeg路径: %\n" autoFFmpegPath ) else ( messageBox "请设置FFmpeg路径!\n点击'浏览...'按钮选择ffmpeg.exe文件。" title:"提示" ) ) ) -- 创建UI窗口 CreateDialog gifMakerRollout
最新发布
08-15
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值