From 3efb5830df1f521d979f0bd6a6275bbf4ddc9cab Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Fri, 18 Jul 2025 13:32:50 +0800 Subject: [PATCH] --- .gitignore | 1 + .vscode/settings.json | 3 + AttributeGrouper.cs | 546 +++++++++ CLAUDE.md | 92 ++ CoordinateConverter.cs | 4 + FloorDetector.cs | 553 +++++++++ LogViewer.bat | 98 ++ MainPlugin.cs | 55 + ModelSplitterDialog.cs | 897 ++++++++++++++ ModelSplitterManager.cs | 1054 +++++++++++++++++ NavisworksFileExporter.cs | 903 ++++++++++++++ NavisworksTransportPlugin.csproj | 16 + PathAnimationManager.cs | 97 +- PathPlanningManager.cs | 2 + TimeLinerIntegrationManager.cs | 426 +++++++ VERSION.md | 2 +- change_log.md | 154 ++- compile.bat | 58 +- doc/design/Navisworks插件集成联动方案.md | 377 ++++++ doc/design/model_splitter_design.md | 535 +++++++++ doc/design/navisworks_api_analysis.md | 702 +++++++++++ doc/guide/crash_fix_summary.md | 223 ++++ doc/guide/crash_prevention_guide.md | 229 ++++ doc/guide/development.md | 5 +- doc/guide/log_analysis_guide.md | 212 ++++ doc/guide/model_splitter_usage_guide.md | 208 ++++ doc/requirement/user_requiement.md | 61 +- .../model_splitter_implementation_summary.md | 342 ++++++ doc/working/timeliner_integration_plan.md | 245 ++++ 29 files changed, 8039 insertions(+), 61 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 AttributeGrouper.cs create mode 100644 CLAUDE.md create mode 100644 FloorDetector.cs create mode 100644 LogViewer.bat create mode 100644 ModelSplitterDialog.cs create mode 100644 ModelSplitterManager.cs create mode 100644 NavisworksFileExporter.cs create mode 100644 TimeLinerIntegrationManager.cs create mode 100644 doc/design/Navisworks插件集成联动方案.md create mode 100644 doc/design/model_splitter_design.md create mode 100644 doc/design/navisworks_api_analysis.md create mode 100644 doc/guide/crash_fix_summary.md create mode 100644 doc/guide/crash_prevention_guide.md create mode 100644 doc/guide/log_analysis_guide.md create mode 100644 doc/guide/model_splitter_usage_guide.md create mode 100644 doc/working/model_splitter_implementation_summary.md create mode 100644 doc/working/timeliner_integration_plan.md diff --git a/.gitignore b/.gitignore index df10859..bec3869 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ bin/ obj/ .vs/ +navisworks_api/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..013007b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotnet.preferCSharpExtension": true +} \ No newline at end of file diff --git a/AttributeGrouper.cs b/AttributeGrouper.cs new file mode 100644 index 0000000..4f0556b --- /dev/null +++ b/AttributeGrouper.cs @@ -0,0 +1,546 @@ +using Autodesk.Navisworks.Api; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace NavisworksTransport +{ + /// + /// 属性分组器 - 负责根据自定义属性对模型元素进行分组 + /// + public class AttributeGrouper + { + #region 数据结构 + + /// + /// 属性分组结果 + /// + public class AttributeGroup + { + public string GroupName { get; set; } + public string AttributeValue { get; set; } + public ModelItemCollection Items { get; set; } + public Dictionary Metadata { get; set; } + public int ItemCount => Items?.Count ?? 0; + + public AttributeGroup() + { + Items = new ModelItemCollection(); + Metadata = new Dictionary(); + } + } + + /// + /// 属性统计信息 + /// + public class AttributeStatistics + { + public string AttributeName { get; set; } + public Dictionary ValueCounts { get; set; } + public int TotalItems { get; set; } + public int ItemsWithAttribute { get; set; } + public int UniqueValues { get; set; } + public double Coverage => TotalItems > 0 ? (double)ItemsWithAttribute / TotalItems : 0.0; + + public AttributeStatistics() + { + ValueCounts = new Dictionary(); + } + } + + #endregion + + #region 公共方法 + + /// + /// 根据指定属性对模型元素进行分组 + /// + /// 要分组的模型元素集合 + /// 分组依据的属性名称 + /// 分组结果列表 + public List GroupByAttribute(ModelItemCollection items, string attributeName) + { + try + { + LogManager.Info($"[AttributeGrouper] 开始按属性分组,属性: {attributeName}, 元素数量: {items.Count}"); + + if (items == null || items.Count == 0) + { + LogManager.Warning("[AttributeGrouper] 输入的模型元素集合为空"); + return new List(); + } + + if (string.IsNullOrEmpty(attributeName)) + { + throw new ArgumentException("属性名称不能为空", nameof(attributeName)); + } + + var groups = new Dictionary>(); + int processedCount = 0; + int itemsWithAttribute = 0; + + foreach (ModelItem item in items) + { + string attributeValue = GetAttributeValue(item, attributeName); + + if (!string.IsNullOrEmpty(attributeValue)) + { + itemsWithAttribute++; + + if (!groups.ContainsKey(attributeValue)) + { + groups[attributeValue] = new List(); + } + groups[attributeValue].Add(item); + } + else + { + // 处理没有该属性的元素 + const string unknownGroup = "未定义"; + if (!groups.ContainsKey(unknownGroup)) + { + groups[unknownGroup] = new List(); + } + groups[unknownGroup].Add(item); + } + + processedCount++; + + // 每处理1000个元素记录一次进度 + if (processedCount % 1000 == 0) + { + LogManager.Info($"[AttributeGrouper] 已处理 {processedCount}/{items.Count} 个元素"); + } + } + + // 转换为AttributeGroup列表 + var result = new List(); + foreach (var kvp in groups.OrderBy(g => g.Key)) + { + var group = new AttributeGroup + { + GroupName = SanitizeGroupName(kvp.Key), + AttributeValue = kvp.Key, + Items = new ModelItemCollection(), + Metadata = new Dictionary + { + ["AttributeName"] = attributeName, + ["OriginalValue"] = kvp.Key, + ["ItemCount"] = kvp.Value.Count, + ["ProcessTime"] = DateTime.Now + } + }; + + group.Items.AddRange(kvp.Value); + result.Add(group); + } + + LogManager.Info($"[AttributeGrouper] 分组完成,共生成 {result.Count} 个组,覆盖率: {(double)itemsWithAttribute / items.Count:P2}"); + return result; + } + catch (Exception ex) + { + LogManager.Error($"[AttributeGrouper] 属性分组失败: {ex.Message}"); + throw; + } + } + + /// + /// 获取可用的属性列表 + /// + /// 模型元素集合 + /// 可用属性名称列表 + public List GetAvailableAttributes(ModelItemCollection items) + { + try + { + var attributes = new HashSet(); + int sampleSize = Math.Min(100, items.Count); // 采样前100个元素 + + LogManager.Info($"[AttributeGrouper] 开始获取可用属性,采样数量: {sampleSize}"); + + foreach (ModelItem item in items.Take(sampleSize)) + { + foreach (PropertyCategory category in item.PropertyCategories) + { + foreach (DataProperty property in category.Properties) + { + if (!string.IsNullOrEmpty(property.DisplayName)) + { + attributes.Add(property.DisplayName); + } + } + } + } + + var result = attributes.OrderBy(attr => attr).ToList(); + LogManager.Info($"[AttributeGrouper] 找到 {result.Count} 个可用属性"); + + return result; + } + catch (Exception ex) + { + LogManager.Error($"[AttributeGrouper] 获取可用属性失败: {ex.Message}"); + return new List(); + } + } + + /// + /// 获取指定属性的值分布统计 + /// + /// 模型元素集合 + /// 属性名称 + /// 属性值计数字典 + public Dictionary GetAttributeValueCounts(ModelItemCollection items, string attributeName) + { + try + { + LogManager.Info($"[AttributeGrouper] 开始统计属性值分布: {attributeName}"); + + var valueCounts = new Dictionary(); + + foreach (ModelItem item in items) + { + string value = GetAttributeValue(item, attributeName); + if (!string.IsNullOrEmpty(value)) + { + if (valueCounts.ContainsKey(value)) + valueCounts[value] = valueCounts[value] + 1; + else + valueCounts[value] = 1; + } + else + { + if (valueCounts.ContainsKey("未定义")) + valueCounts["未定义"] = valueCounts["未定义"] + 1; + else + valueCounts["未定义"] = 1; + } + } + + LogManager.Info($"[AttributeGrouper] 属性值统计完成,共 {valueCounts.Count} 个不同值"); + return valueCounts; + } + catch (Exception ex) + { + LogManager.Error($"[AttributeGrouper] 统计属性值分布失败: {ex.Message}"); + return new Dictionary(); + } + } + + /// + /// 获取属性的详细统计信息 + /// + /// 模型元素集合 + /// 属性名称 + /// 属性统计信息 + public AttributeStatistics GetAttributeStatistics(ModelItemCollection items, string attributeName) + { + try + { + var statistics = new AttributeStatistics + { + AttributeName = attributeName, + TotalItems = items.Count + }; + + var valueCounts = GetAttributeValueCounts(items, attributeName); + statistics.ValueCounts = valueCounts; + statistics.UniqueValues = valueCounts.Count; + statistics.ItemsWithAttribute = valueCounts.Sum(kvp => kvp.Value); + + // 移除"未定义"项来计算真实的覆盖率 + if (valueCounts.ContainsKey("未定义")) + { + statistics.ItemsWithAttribute -= valueCounts["未定义"]; + } + + LogManager.Info($"[AttributeGrouper] 属性统计完成: {attributeName}, 覆盖率: {statistics.Coverage:P2}"); + return statistics; + } + catch (Exception ex) + { + LogManager.Error($"[AttributeGrouper] 获取属性统计失败: {ex.Message}"); + return new AttributeStatistics { AttributeName = attributeName }; + } + } + + /// + /// 根据多个属性进行复合分组 + /// + /// 模型元素集合 + /// 属性名称列表 + /// 复合分组结果 + public List GroupByMultipleAttributes(ModelItemCollection items, List attributeNames) + { + try + { + LogManager.Info($"[AttributeGrouper] 开始多属性分组,属性数量: {attributeNames.Count}"); + + if (attributeNames == null || attributeNames.Count == 0) + { + throw new ArgumentException("属性名称列表不能为空", nameof(attributeNames)); + } + + var groups = new Dictionary>(); + + foreach (ModelItem item in items) + { + var attributeValues = new List(); + + foreach (string attributeName in attributeNames) + { + string value = GetAttributeValue(item, attributeName) ?? "未定义"; + attributeValues.Add(value); + } + + string compositeKey = string.Join(" | ", attributeValues); + + if (!groups.ContainsKey(compositeKey)) + { + groups[compositeKey] = new List(); + } + groups[compositeKey].Add(item); + } + + var result = new List(); + foreach (var kvp in groups.OrderBy(g => g.Key)) + { + var group = new AttributeGroup + { + GroupName = SanitizeGroupName(kvp.Key), + AttributeValue = kvp.Key, + Items = new ModelItemCollection(), + Metadata = new Dictionary + { + ["AttributeNames"] = attributeNames, + ["CompositeKey"] = kvp.Key, + ["ItemCount"] = kvp.Value.Count, + ["GroupingMethod"] = "MultipleAttributes" + } + }; + + group.Items.AddRange(kvp.Value); + result.Add(group); + } + + LogManager.Info($"[AttributeGrouper] 多属性分组完成,共生成 {result.Count} 个组"); + return result; + } + catch (Exception ex) + { + LogManager.Error($"[AttributeGrouper] 多属性分组失败: {ex.Message}"); + throw; + } + } + + /// + /// 根据属性值范围进行分组(适用于数值属性) + /// + /// 模型元素集合 + /// 数值属性名称 + /// 范围大小 + /// 范围分组结果 + public List GroupByAttributeRange(ModelItemCollection items, string attributeName, double rangeSize) + { + try + { + LogManager.Info($"[AttributeGrouper] 开始按属性范围分组: {attributeName}, 范围大小: {rangeSize}"); + + var rangeGroups = new Dictionary>(); + + foreach (ModelItem item in items) + { + string attributeValueStr = GetAttributeValue(item, attributeName); + + if (double.TryParse(attributeValueStr, out double numericValue)) + { + double rangeStart = Math.Floor(numericValue / rangeSize) * rangeSize; + double rangeEnd = rangeStart + rangeSize; + string rangeKey = $"{rangeStart:F1}-{rangeEnd:F1}"; + + if (!rangeGroups.ContainsKey(rangeKey)) + { + rangeGroups[rangeKey] = new List(); + } + rangeGroups[rangeKey].Add(item); + } + else + { + // 非数值的归入"其他"组 + const string otherGroup = "其他"; + if (!rangeGroups.ContainsKey(otherGroup)) + { + rangeGroups[otherGroup] = new List(); + } + rangeGroups[otherGroup].Add(item); + } + } + + var result = new List(); + foreach (var kvp in rangeGroups.OrderBy(g => g.Key)) + { + var group = new AttributeGroup + { + GroupName = $"Range_{kvp.Key}", + AttributeValue = kvp.Key, + Items = new ModelItemCollection(), + Metadata = new Dictionary + { + ["AttributeName"] = attributeName, + ["RangeSize"] = rangeSize, + ["RangeKey"] = kvp.Key, + ["GroupingMethod"] = "AttributeRange" + } + }; + + group.Items.AddRange(kvp.Value); + result.Add(group); + } + + LogManager.Info($"[AttributeGrouper] 属性范围分组完成,共生成 {result.Count} 个组"); + return result; + } + catch (Exception ex) + { + LogManager.Error($"[AttributeGrouper] 属性范围分组失败: {ex.Message}"); + throw; + } + } + + #endregion + + #region 私有方法 + + /// + /// 获取模型元素的指定属性值 + /// + /// 模型元素 + /// 属性名称 + /// 属性值字符串 + private string GetAttributeValue(ModelItem item, string attributeName) + { + try + { + foreach (PropertyCategory category in item.PropertyCategories) + { + foreach (DataProperty property in category.Properties) + { + if (string.Equals(property.DisplayName, attributeName, StringComparison.OrdinalIgnoreCase)) + { + return property.Value?.ToString()?.Trim(); + } + } + } + return null; + } + catch (Exception ex) + { + LogManager.Warning($"[AttributeGrouper] 获取属性值失败: {item.DisplayName}, 属性: {attributeName}, 错误: {ex.Message}"); + return null; + } + } + + /// + /// 清理分组名称,移除无效字符 + /// + /// 原始分组名称 + /// 清理后的分组名称 + private string SanitizeGroupName(string groupName) + { + if (string.IsNullOrEmpty(groupName)) + return "Unknown_Group"; + + // 替换空格和特殊字符为下划线 + string sanitized = System.Text.RegularExpressions.Regex.Replace(groupName.Trim(), @"\s+", "_"); + + // 移除或替换其他特殊字符,保留中文字符 + sanitized = System.Text.RegularExpressions.Regex.Replace(sanitized, @"[^\w\u4e00-\u9fa5\-\.]", "_"); + + // 移除多余的下划线 + sanitized = System.Text.RegularExpressions.Regex.Replace(sanitized, @"_+", "_"); + + // 移除首尾下划线 + sanitized = sanitized.Trim('_'); + + // 限制长度 + if (sanitized.Length > 50) + { + sanitized = sanitized.Substring(0, 50); + } + + return string.IsNullOrEmpty(sanitized) ? "Unknown_Group" : sanitized; + } + + /// + /// 验证属性名称是否有效 + /// + /// 属性名称 + /// 是否有效 + private bool IsValidAttributeName(string attributeName) + { + return !string.IsNullOrWhiteSpace(attributeName) && + attributeName.Length <= 100 && + !attributeName.Contains('\0'); + } + + /// + /// 获取属性的数据类型 + /// + /// 模型元素集合 + /// 属性名称 + /// 数据类型字符串 + public string GetAttributeDataType(ModelItemCollection items, string attributeName) + { + try + { + var sampleValues = new List(); + int sampleCount = 0; + const int maxSamples = 20; + + foreach (ModelItem item in items) + { + if (sampleCount >= maxSamples) break; + + string value = GetAttributeValue(item, attributeName); + if (!string.IsNullOrEmpty(value)) + { + sampleValues.Add(value); + sampleCount++; + } + } + + if (sampleValues.Count == 0) + return "Unknown"; + + // 检查是否为数值类型 + bool allNumeric = sampleValues.All(v => double.TryParse(v, out _)); + if (allNumeric) + return "Numeric"; + + // 检查是否为日期类型 + bool allDate = sampleValues.All(v => DateTime.TryParse(v, out _)); + if (allDate) + return "DateTime"; + + // 检查是否为布尔类型 + bool allBoolean = sampleValues.All(v => + v.Equals("true", StringComparison.OrdinalIgnoreCase) || + v.Equals("false", StringComparison.OrdinalIgnoreCase) || + v.Equals("yes", StringComparison.OrdinalIgnoreCase) || + v.Equals("no", StringComparison.OrdinalIgnoreCase)); + if (allBoolean) + return "Boolean"; + + return "Text"; + } + catch (Exception ex) + { + LogManager.Warning($"[AttributeGrouper] 获取属性数据类型失败: {attributeName}, 错误: {ex.Message}"); + return "Unknown"; + } + } + + #endregion + } +} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6f67ffe --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,92 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +NavisworksTransport is a Navisworks 2017 plugin (v0.1.8) for logistics path planning and transportation conflict detection in 3D building models. The plugin enables route optimization, collision detection, and animated object movement along defined paths. + +## Build Commands + +- **Build**: `compile.bat` - Automatically detects MSBuild (VS 2017/2019/2022) or falls back to `dotnet build` +- **Target**: .NET Framework 4.6.2, x64 platform +- **Output**: Direct deployment to `%PROGRAMFILES%\Autodesk\Navisworks Manage 2017\Plugins\NavisworksTransportPlugin\` + +## Architecture Overview + +### Core Plugin Structure +- **MainPlugin.cs**: Primary AddInPlugin entry point with ribbon UI +- **PathClickToolPlugin.cs**: ToolPlugin for 3D mouse interaction +- **PathPointRenderPlugin.cs**: RenderPlugin for 3D visualization + +### Manager Components +- **PathPlanningManager.cs**: Central path planning and route management logic +- **PathAnimationManager.cs**: TimeLiner integration for object movement animation +- **CoordinateConverter.cs**: 2D map overlay to 3D world coordinate conversion +- **CategoryAttributeManager.cs**: COM API wrapper for logistics attribute management +- **VisibilityManager.cs**: Layer visibility and model filtering control +- **ModelSplitterManager.cs**: Model layer separation and export functionality + +### Data and Utilities +- **PathPlanningModels.cs**: Core data structures (PathEditState, PathRoute, PathPoint) +- **PathDataManager.cs**: Serialization and persistence using Newtonsoft.Json +- **GeometryExtractor.cs**: 3D geometry analysis and bounding box calculations +- **LogManager.cs**: Centralized logging with global exception handling + +### UI Components +- **LogisticsPropertyEditDialog.cs**: Property editing interface +- **ModelSplitterDialog.cs**: Model splitting configuration UI + +## Key Technical Details + +### Navisworks API Integration +- Uses dual API approach: Native API (`Autodesk.Navisworks.Api`) + COM API (`Autodesk.Navisworks.ComApi`) +- COM API required for attribute persistence and TimeLiner operations +- Plugin types: AddInPlugin (main), ToolPlugin (interaction), RenderPlugin (visualization) + +### Exception Handling +Global exception handling implemented in MainPlugin with: +- AppDomain.CurrentDomain.UnhandledException +- Application.ThreadException +- Automatic recovery and user-friendly error reporting + +### Coordinate Systems +- Supports 2D map overlay on 3D models with dynamic zoom/pan +- Margin-based boundary calculations for click precision +- Transform chains for coordinate conversion between spaces + +### Logistics Categories +Eight predefined logistics element types: +- 门 (Doors), 电梯 (Elevators), 楼梯 (Stairs), 通道 (Channels) +- 障碍物 (Obstacles), 装卸区 (Loading Zones), 停车区 (Parking), 检查点 (Checkpoints) + +## Development Guidelines + +### File Organization +- Core managers handle specific functionality areas +- Models file contains shared data structures +- UI dialogs are separate form classes +- Utilities (logging, geometry, data) are standalone classes + +### Plugin Registration Pattern +```csharp +[Plugin("NavisworksTransport.PluginName", "YourDeveloperID")] +[AddInPlugin(AddInLocation.AddIn)] +``` + +### Error Handling Best Practices +- Use LogManager for consistent logging +- Implement try-catch blocks around Navisworks API calls +- Provide meaningful error messages to users +- Use COM API error codes for troubleshooting + +### Dependencies +- **System.Windows.Forms**: UI dialogs and controls +- **System.Drawing**: Graphics and coordinate operations + +## Testing and Deployment + +- Manual testing required through Navisworks Manage 2017 +- Plugin automatically deploys to Navisworks plugin directory during build +- Restart Navisworks after compilation to load new plugin version +- Use LogManager output for debugging and troubleshooting \ No newline at end of file diff --git a/CoordinateConverter.cs b/CoordinateConverter.cs index 5ffd828..b62090d 100644 --- a/CoordinateConverter.cs +++ b/CoordinateConverter.cs @@ -101,6 +101,10 @@ namespace NavisworksTransport CalculateBestFitView(); } + public CoordinateConverter() + { + } + /// /// 计算最佳适应视图 /// diff --git a/FloorDetector.cs b/FloorDetector.cs new file mode 100644 index 0000000..54f2c4a --- /dev/null +++ b/FloorDetector.cs @@ -0,0 +1,553 @@ +using Autodesk.Navisworks.Api; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace NavisworksTransport +{ + /// + /// 楼层检测器 - 负责从模型中识别和提取楼层信息 + /// + public class FloorDetector + { + #region 常量定义 + + private const double DEFAULT_FLOOR_HEIGHT_THRESHOLD = 2.5; // 默认最小楼层高度(米) + private const double DEFAULT_ELEVATION_TOLERANCE = 0.5; // 默认高程容差(米) + + // 常见的楼层属性名称 + private readonly string[] COMMON_FLOOR_ATTRIBUTES = { + "Level", "Floor", "Storey", "楼层", "层", "Level Name", "Story", + "Building Level", "Floor Level", "Elevation", "Z", "Height" + }; + + #endregion + + #region 公共方法 + + /// + /// 检测模型中的楼层信息 + /// + /// 要检测的模型元素集合 + /// 指定的楼层属性名称,为空时自动检测 + /// 检测到的楼层信息列表 + public List DetectFloors(ModelItemCollection items, string attributeName = null) + { + try + { + LogManager.Info($"[FloorDetector] 开始检测楼层,元素数量: {items.Count}"); + + if (items == null || items.Count == 0) + { + LogManager.Warning("[FloorDetector] 输入的模型元素集合为空"); + return new List(); + } + + List floors; + + if (!string.IsNullOrEmpty(attributeName)) + { + // 使用指定属性检测楼层 + floors = DetectFloorsByAttribute(items, attributeName); + LogManager.Info($"[FloorDetector] 使用属性 '{attributeName}' 检测到 {floors.Count} 个楼层"); + } + else + { + // 自动检测最佳楼层属性 + string bestAttribute = FindBestFloorAttribute(items); + if (!string.IsNullOrEmpty(bestAttribute)) + { + floors = DetectFloorsByAttribute(items, bestAttribute); + LogManager.Info($"[FloorDetector] 自动选择属性 '{bestAttribute}' 检测到 {floors.Count} 个楼层"); + } + else + { + // 使用高程检测 + floors = DetectFloorsByElevation(items); + LogManager.Info($"[FloorDetector] 使用高程检测到 {floors.Count} 个楼层"); + } + } + + // 验证和优化检测结果 + floors = ValidateAndOptimizeFloors(floors); + + LogManager.Info($"[FloorDetector] 楼层检测完成,最终识别 {floors.Count} 个楼层"); + return floors; + } + catch (Exception ex) + { + LogManager.Error($"[FloorDetector] 楼层检测失败: {ex.Message}"); + throw; + } + } + + /// + /// 获取可用的楼层属性列表 + /// + public List GetAvailableFloorAttributes(ModelItemCollection items) + { + try + { + var availableAttributes = new HashSet(); + int sampleSize = Math.Min(100, items.Count); // 采样前100个元素 + + foreach (ModelItem item in items.Take(sampleSize)) + { + foreach (PropertyCategory category in item.PropertyCategories) + { + foreach (DataProperty property in category.Properties) + { + string propName = property.DisplayName; + + // 检查是否为楼层相关属性 + if (IsFloorRelatedAttribute(propName)) + { + availableAttributes.Add(propName); + } + } + } + } + + var result = availableAttributes.OrderBy(attr => attr).ToList(); + LogManager.Info($"[FloorDetector] 找到 {result.Count} 个可用的楼层属性"); + + return result; + } + catch (Exception ex) + { + LogManager.Error($"[FloorDetector] 获取楼层属性失败: {ex.Message}"); + return new List(); + } + } + + /// + /// 根据高程获取楼层信息 + /// + public ModelSplitterManager.FloorInfo GetFloorByElevation(double elevation, List floors) + { + return floors?.FirstOrDefault(f => + Math.Abs(f.Elevation - elevation) <= DEFAULT_ELEVATION_TOLERANCE); + } + + #endregion + + #region 私有方法 - 基于属性的检测 + + private List DetectFloorsByAttribute(ModelItemCollection items, string attributeName) + { + var floorGroups = new Dictionary>(); + + foreach (ModelItem item in items) + { + string floorValue = GetAttributeValue(item, attributeName); + if (!string.IsNullOrEmpty(floorValue)) + { + if (!floorGroups.ContainsKey(floorValue)) + { + floorGroups[floorValue] = new List(); + } + floorGroups[floorValue].Add(item); + } + } + + var floors = new List(); + foreach (var kvp in floorGroups) + { + var floorItems = new ModelItemCollection(); + floorItems.AddRange(kvp.Value); + + var floorInfo = new ModelSplitterManager.FloorInfo + { + FloorName = SanitizeFloorName(kvp.Key), + Items = floorItems, + Elevation = CalculateAverageElevation(floorItems), + Bounds = CalculateBounds(floorItems), + Properties = new Dictionary + { + ["OriginalAttributeValue"] = kvp.Key, + ["AttributeName"] = attributeName, + ["DetectionMethod"] = "Attribute" + } + }; + + floors.Add(floorInfo); + } + + return floors.OrderBy(f => f.Elevation).ToList(); + } + + private string GetAttributeValue(ModelItem item, string attributeName) + { + try + { + foreach (PropertyCategory category in item.PropertyCategories) + { + foreach (DataProperty property in category.Properties) + { + if (string.Equals(property.DisplayName, attributeName, StringComparison.OrdinalIgnoreCase)) + { + return property.Value?.ToString()?.Trim(); + } + } + } + return null; + } + catch + { + return null; + } + } + + #endregion + + #region 私有方法 - 基于高程的检测 + + private List DetectFloorsByElevation(ModelItemCollection items) + { + var elevationGroups = new Dictionary>(); + + foreach (ModelItem item in items) + { + try + { + var bounds = item.BoundingBox(); + if (bounds.HasVolume) + { + double elevation = bounds.Min.Z; // 使用Z坐标最小值作为高程 + + // 查找相近的楼层组 + double floorElevation = FindNearestFloorElevation(elevationGroups.Keys, elevation); + + if (floorElevation == double.MinValue) + { + // 创建新楼层组 + elevationGroups[elevation] = new List { item }; + } + else + { + // 添加到现有楼层组 + elevationGroups[floorElevation].Add(item); + } + } + } + catch + { + // 忽略无法获取边界框的元素 + } + } + + // 转换为FloorInfo列表 + var floors = new List(); + int floorIndex = 1; + + foreach (var kvp in elevationGroups.OrderBy(x => x.Key)) + { + var floorItems = new ModelItemCollection(); + floorItems.AddRange(kvp.Value); + + var floorInfo = new ModelSplitterManager.FloorInfo + { + FloorName = GenerateFloorName(floorIndex, kvp.Key), + Elevation = kvp.Key, + Items = floorItems, + Bounds = CalculateBounds(floorItems), + Properties = new Dictionary + { + ["DetectionMethod"] = "Elevation", + ["ElevationTolerance"] = DEFAULT_ELEVATION_TOLERANCE, + ["FloorIndex"] = floorIndex + } + }; + + floors.Add(floorInfo); + floorIndex++; + } + + return floors; + } + + private double FindNearestFloorElevation(IEnumerable existingElevations, double targetElevation) + { + foreach (double elevation in existingElevations) + { + if (Math.Abs(elevation - targetElevation) <= DEFAULT_ELEVATION_TOLERANCE) + { + return elevation; + } + } + return double.MinValue; + } + + #endregion + + #region 私有方法 - 属性检测和验证 + + private string FindBestFloorAttribute(ModelItemCollection items) + { + var attributeScores = new Dictionary(); + int sampleSize = Math.Min(200, items.Count); // 增加采样数量以提高准确性 + + foreach (ModelItem item in items.Take(sampleSize)) + { + foreach (PropertyCategory category in item.PropertyCategories) + { + foreach (DataProperty property in category.Properties) + { + string propName = property.DisplayName; + + if (IsFloorRelatedAttribute(propName)) + { + string value = property.Value?.ToString()?.Trim(); + if (!string.IsNullOrEmpty(value) && IsValidFloorValue(value)) + { + // 根据属性名称的匹配度给分 + int score = CalculateAttributeScore(propName); + if (attributeScores.ContainsKey(propName)) + attributeScores[propName] = attributeScores[propName] + score; + else + attributeScores[propName] = score; + } + } + } + } + } + + // 返回得分最高的属性 + var bestAttribute = attributeScores.OrderByDescending(kvp => kvp.Value).FirstOrDefault(); + + if (bestAttribute.Value > 0) + { + LogManager.Info($"[FloorDetector] 选择最佳楼层属性: {bestAttribute.Key} (得分: {bestAttribute.Value})"); + return bestAttribute.Key; + } + + return null; + } + + private bool IsFloorRelatedAttribute(string attributeName) + { + if (string.IsNullOrEmpty(attributeName)) + return false; + + return COMMON_FLOOR_ATTRIBUTES.Any(attr => + attributeName.IndexOf(attr, StringComparison.OrdinalIgnoreCase) >= 0); + } + + private int CalculateAttributeScore(string attributeName) + { + // 根据属性名称的匹配度计算分数 + string lowerName = attributeName.ToLower(); + + if (lowerName == "level" || lowerName == "floor" || lowerName == "楼层") + return 10; + if (lowerName.Contains("level") || lowerName.Contains("floor")) + return 8; + if (lowerName.Contains("storey") || lowerName.Contains("story")) + return 7; + if (lowerName.Contains("elevation")) + return 6; + if (lowerName.Contains("层")) + return 5; + + return 3; // 默认分数 + } + + private bool IsValidFloorValue(string value) + { + if (string.IsNullOrEmpty(value)) + return false; + + // 检查是否为有效的楼层值 + // 数字楼层(如 "1", "2", "B1") + if (System.Text.RegularExpressions.Regex.IsMatch(value, @"^[B]?\d+[F]?$", + System.Text.RegularExpressions.RegexOptions.IgnoreCase)) + return true; + + // 文字楼层(如 "Ground Floor", "First Floor") + if (value.ToLower().Contains("floor") || value.ToLower().Contains("level")) + return true; + + // 中文楼层(如 "一层", "地下一层") + if (value.Contains("层")) + return true; + + return false; + } + + #endregion + + #region 私有方法 - 结果验证和优化 + + private List ValidateAndOptimizeFloors(List floors) + { + if (floors == null || floors.Count == 0) + return floors; + + var validatedFloors = new List(); + + foreach (var floor in floors) + { + // 验证楼层是否有效 + if (IsValidFloor(floor)) + { + // 优化楼层名称 + floor.FloorName = OptimizeFloorName(floor.FloorName); + validatedFloors.Add(floor); + } + else + { + LogManager.Warning($"[FloorDetector] 跳过无效楼层: {floor.FloorName}"); + } + } + + // 按高程排序 + validatedFloors = validatedFloors.OrderBy(f => f.Elevation).ToList(); + + // 重新编号(如果需要) + RenumberFloorsIfNeeded(validatedFloors); + + return validatedFloors; + } + + private bool IsValidFloor(ModelSplitterManager.FloorInfo floor) + { + // 检查楼层是否包含模型元素 + if (floor.Items == null || floor.Items.Count == 0) + return false; + + // 检查楼层名称是否有效 + if (string.IsNullOrEmpty(floor.FloorName)) + return false; + + // 检查边界框是否有效 - 简化检查 + // BoundingBox3D是结构体,总是有值 + + return true; + } + + private string OptimizeFloorName(string originalName) + { + if (string.IsNullOrEmpty(originalName)) + return "Unknown_Floor"; + + // 移除特殊字符,保留字母数字和下划线 + string optimized = System.Text.RegularExpressions.Regex.Replace(originalName, @"[^\w\u4e00-\u9fa5]", "_"); + + // 移除多余的下划线 + optimized = System.Text.RegularExpressions.Regex.Replace(optimized, @"_+", "_"); + + // 移除首尾下划线 + optimized = optimized.Trim('_'); + + return string.IsNullOrEmpty(optimized) ? "Unknown_Floor" : optimized; + } + + private void RenumberFloorsIfNeeded(List floors) + { + // 如果楼层名称都是自动生成的数字格式,重新编号 + bool needsRenumbering = floors.All(f => + System.Text.RegularExpressions.Regex.IsMatch(f.FloorName, @"^Floor_\d+$")); + + if (needsRenumbering) + { + for (int i = 0; i < floors.Count; i++) + { + floors[i].FloorName = $"Floor_{i + 1:D2}"; + floors[i].Properties["RenumberedIndex"] = i + 1; + } + } + } + + #endregion + + #region 私有方法 - 辅助计算 + + private double CalculateAverageElevation(ModelItemCollection items) + { + if (items == null || items.Count == 0) + return 0.0; + + double totalElevation = 0.0; + int validCount = 0; + + foreach (ModelItem item in items) + { + try + { + var bounds = item.BoundingBox(); + if (bounds.HasVolume) + { + totalElevation += bounds.Min.Z; + validCount++; + } + } + catch + { + // 忽略无法获取边界框的元素 + } + } + + return validCount > 0 ? totalElevation / validCount : 0.0; + } + + private BoundingBox3D CalculateBounds(ModelItemCollection items) + { + if (items == null || items.Count == 0) + return new BoundingBox3D(); + + BoundingBox3D combinedBounds = null; + + foreach (ModelItem item in items) + { + try + { + var itemBounds = item.BoundingBox(); + if (itemBounds.HasVolume) + { + combinedBounds = combinedBounds.Extend(itemBounds); + } + } + catch + { + // 忽略无法获取边界框的元素 + } + } + + return combinedBounds ?? new BoundingBox3D(); + } + + private string SanitizeFloorName(string floorName) + { + if (string.IsNullOrEmpty(floorName)) + return "Unknown_Floor"; + + // 替换空格和特殊字符为下划线 + string sanitized = System.Text.RegularExpressions.Regex.Replace(floorName.Trim(), @"\s+", "_"); + + // 移除或替换其他特殊字符 + sanitized = System.Text.RegularExpressions.Regex.Replace(sanitized, @"[^\w\u4e00-\u9fa5]", "_"); + + // 移除多余的下划线 + sanitized = System.Text.RegularExpressions.Regex.Replace(sanitized, @"_+", "_"); + + // 移除首尾下划线 + sanitized = sanitized.Trim('_'); + + return string.IsNullOrEmpty(sanitized) ? "Unknown_Floor" : sanitized; + } + + private string GenerateFloorName(int floorIndex, double elevation) + { + if (elevation < 0) + { + return $"Basement_{Math.Abs((int)Math.Round(elevation)):D2}"; + } + else + { + return $"Floor_{floorIndex:D2}"; + } + } + + #endregion + } +} \ No newline at end of file diff --git a/LogViewer.bat b/LogViewer.bat new file mode 100644 index 0000000..afcf4f3 --- /dev/null +++ b/LogViewer.bat @@ -0,0 +1,98 @@ +@echo off +echo ======================================== +echo Navisworks Transport Plugin 日志查看器 +echo ======================================== +echo. + +set LOGFILE=%USERPROFILE%\Desktop\NavisworksTransport_Debug.log + +if not exist "%LOGFILE%" ( + echo 日志文件不存在: %LOGFILE% + echo 请先运行分层拆分功能生成日志 + pause + exit /b +) + +echo 日志文件位置: %LOGFILE% +echo 文件大小: +for %%A in ("%LOGFILE%") do echo %%~zA 字节 +echo. + +:MENU +echo 请选择操作: +echo 1. 查看最新50行日志 +echo 2. 查看最新100行日志 +echo 3. 查看所有ERROR日志 +echo 4. 查看最新会话日志 +echo 5. 实时监控日志(按Ctrl+C停止) +echo 6. 清空日志文件 +echo 7. 退出 +echo. +set /p choice=请输入选择 (1-7): + +if "%choice%"=="1" goto TAIL50 +if "%choice%"=="2" goto TAIL100 +if "%choice%"=="3" goto ERRORS +if "%choice%"=="4" goto SESSION +if "%choice%"=="5" goto MONITOR +if "%choice%"=="6" goto CLEAR +if "%choice%"=="7" goto EXIT + +echo 无效选择,请重新输入 +goto MENU + +:TAIL50 +echo. +echo ========== 最新50行日志 ========== +powershell -Command "Get-Content '%LOGFILE%' -Tail 50" +echo. +pause +goto MENU + +:TAIL100 +echo. +echo ========== 最新100行日志 ========== +powershell -Command "Get-Content '%LOGFILE%' -Tail 100" +echo. +pause +goto MENU + +:ERRORS +echo. +echo ========== 所有ERROR日志 ========== +findstr /i "ERROR" "%LOGFILE%" +echo. +pause +goto MENU + +:SESSION +echo. +echo ========== 最新会话日志 ========== +powershell -Command "$content = Get-Content '%LOGFILE%'; $lastSession = ($content | Select-String 'SESSION.*新会话开始' | Select-Object -Last 1).LineNumber - 1; if($lastSession -ge 0) { $content[$lastSession..($content.Length-1)] } else { '没有找到会话标记' }" +echo. +pause +goto MENU + +:MONITOR +echo. +echo ========== 实时监控日志 (按Ctrl+C停止) ========== +echo 正在监控日志文件变化... +powershell -Command "Get-Content '%LOGFILE%' -Wait -Tail 10" +goto MENU + +:CLEAR +echo. +set /p confirm=确定要清空日志文件吗?(y/N): +if /i "%confirm%"=="y" ( + echo. > "%LOGFILE%" + echo 日志文件已清空 +) else ( + echo 操作已取消 +) +echo. +pause +goto MENU + +:EXIT +echo 再见! +pause \ No newline at end of file diff --git a/MainPlugin.cs b/MainPlugin.cs index 3df5e1f..f148a63 100644 --- a/MainPlugin.cs +++ b/MainPlugin.cs @@ -721,6 +721,18 @@ namespace NavisworksTransport int currentY = 10; + // 模型分层拆分 + GroupBox splitterGroupBox = new GroupBox + { + Text = "模型分层拆分", + Location = new Point(10, currentY), + Size = new Size(350, 80), + Font = new Font("微软雅黑", 8, FontStyle.Bold) + }; + scrollPanel.Controls.Add(splitterGroupBox); + CreateModelSplitterControls(splitterGroupBox); + currentY += 100; + // 日志管理 GroupBox logGroupBox = new GroupBox { @@ -759,6 +771,49 @@ namespace NavisworksTransport tabControl.TabPages.Add(systemTab); } + /// + /// 创建模型分层拆分控件 + /// + private void CreateModelSplitterControls(GroupBox groupBox) + { + Button splitterButton = new Button + { + Text = "模型分层拆分", + Location = new Point(15, 25), + Size = new Size(120, 30), + Font = new Font("微软雅黑", 8) + }; + groupBox.Controls.Add(splitterButton); + + splitterButton.Click += (sender, e) => { + GlobalExceptionHandler.SafeExecute(() => + { + // 检查是否有打开的模型 + if (NavisApplication.ActiveDocument?.Models?.Count == 0) + { + MessageBox.Show("请先打开一个Navisworks模型文件", "提示", + MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + // 显示模型分层对话框 + var splitterDialog = new ModelSplitterDialog(); + splitterDialog.ShowDialog(_controlPanelForm); + + }, "打开模型分层对话框"); + }; + + Label infoLabel = new Label + { + Text = "将大型模型按楼层或属性拆分为多个文件", + Location = new Point(15, 60), + Size = new Size(320, 15), + Font = new Font("微软雅黑", 7), + ForeColor = System.Drawing.Color.Gray + }; + groupBox.Controls.Add(infoLabel); + } + /// /// 创建路径列表管理控件 /// diff --git a/ModelSplitterDialog.cs b/ModelSplitterDialog.cs new file mode 100644 index 0000000..3f17b78 --- /dev/null +++ b/ModelSplitterDialog.cs @@ -0,0 +1,897 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Forms; +using Autodesk.Navisworks.Api; +using NavisApplication = Autodesk.Navisworks.Api.Application; + +namespace NavisworksTransport +{ + /// + /// 模型分层拆分对话框 + /// + public partial class ModelSplitterDialog : Form + { + #region 私有字段 + + private ModelSplitterManager _splitterManager; + private FloorDetector _floorDetector; + private AttributeGrouper _attributeGrouper; + private List _previewResults; + private bool _isProcessing = false; + + // UI控件 + private ComboBox _strategyComboBox; + private ComboBox _attributeComboBox; + private TextBox _outputDirectoryTextBox; + private Button _browseDirectoryButton; + private TextBox _fileNamePatternTextBox; + private CheckBox _includeEmptyLayersCheckBox; + private CheckBox _createSubDirectoriesCheckBox; + private CheckBox _generateReportCheckBox; + private ListView _previewListView; + private ProgressBar _progressBar; + private Label _statusLabel; + private Button _previewButton; + private Button _executeButton; + private Button _cancelButton; + private Button _helpButton; + + // 高级设置控件 + private NumericUpDown _elevationToleranceNumeric; + private NumericUpDown _minFloorHeightNumeric; + private ComboBox _fileFormatComboBox; + + #endregion + + #region 构造函数 + + public ModelSplitterDialog() + { + InitializeComponent(); + InitializeManagers(); + LoadInitialData(); + } + + #endregion + + #region 初始化方法 + + private void InitializeComponent() + { + this.Text = "模型分层拆分工具"; + this.Size = new Size(800, 720); + this.StartPosition = FormStartPosition.CenterParent; + this.FormBorderStyle = FormBorderStyle.FixedDialog; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.ShowInTaskbar = false; + + CreateControls(); + LayoutControls(); + AttachEventHandlers(); + } + + private void CreateControls() + { + // 分层策略 + var strategyLabel = new Label + { + Text = "分层策略:", + AutoSize = true, + Font = new Font("微软雅黑", 8, FontStyle.Bold) + }; + + _strategyComboBox = new ComboBox + { + DropDownStyle = ComboBoxStyle.DropDownList, + Font = new Font("微软雅黑", 8) + }; + _strategyComboBox.Items.AddRange(new object[] + { + "按楼层分层", + "按自定义属性分层", + "按类别分层", + "按高程范围分层" + }); + _strategyComboBox.SelectedIndex = 0; + + // 属性选择 + var attributeLabel = new Label + { + Text = "分层属性:", + AutoSize = true, + Font = new Font("微软雅黑", 8, FontStyle.Bold) + }; + + _attributeComboBox = new ComboBox + { + DropDownStyle = ComboBoxStyle.DropDownList, + Font = new Font("微软雅黑", 8) + }; + + // 输出目录 + var outputLabel = new Label + { + Text = "输出目录:", + AutoSize = true, + Font = new Font("微软雅黑", 8, FontStyle.Bold) + }; + + _outputDirectoryTextBox = new TextBox + { + Font = new Font("微软雅黑", 8), + Text = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "NavisworksSplit") + }; + + _browseDirectoryButton = new Button + { + Text = "浏览...", + Font = new Font("微软雅黑", 8), + Size = new Size(80, 25) + }; + + // 文件命名模式 + var patternLabel = new Label + { + Text = "文件命名:", + AutoSize = true, + Font = new Font("微软雅黑", 8, FontStyle.Bold) + }; + + _fileNamePatternTextBox = new TextBox + { + Font = new Font("微软雅黑", 8), + Text = "{ProjectName}_{LayerName}" + }; + + // 选项设置 + _includeEmptyLayersCheckBox = new CheckBox + { + Text = "包含空分层", + Font = new Font("微软雅黑", 8), + AutoSize = true + }; + + _createSubDirectoriesCheckBox = new CheckBox + { + Text = "创建子目录", + Font = new Font("微软雅黑", 8), + AutoSize = true, + Checked = true + }; + + _generateReportCheckBox = new CheckBox + { + Text = "生成报告", + Font = new Font("微软雅黑", 8), + AutoSize = true, + Checked = true + }; + + // 高级设置 + var toleranceLabel = new Label + { + Text = "高程容差(m):", + AutoSize = true, + Font = new Font("微软雅黑", 8) + }; + + _elevationToleranceNumeric = new NumericUpDown + { + Font = new Font("微软雅黑", 8), + DecimalPlaces = 1, + Minimum = 0.1m, + Maximum = 10.0m, + Value = 0.5m, + Increment = 0.1m + }; + + var floorHeightLabel = new Label + { + Text = "最小楼层高度(m):", + AutoSize = true, + Font = new Font("微软雅黑", 8) + }; + + _minFloorHeightNumeric = new NumericUpDown + { + Font = new Font("微软雅黑", 8), + DecimalPlaces = 1, + Minimum = 1.0m, + Maximum = 20.0m, + Value = 2.5m, + Increment = 0.5m + }; + + var formatLabel = new Label + { + Text = "文件格式:", + AutoSize = true, + Font = new Font("微软雅黑", 8) + }; + + _fileFormatComboBox = new ComboBox + { + DropDownStyle = ComboBoxStyle.DropDownList, + Font = new Font("微软雅黑", 8) + }; + _fileFormatComboBox.Items.AddRange(new object[] { "nwd", "nwf", "nwc" }); + _fileFormatComboBox.SelectedIndex = 0; + + // 预览列表 + var previewLabel = new Label + { + Text = "分层预览:", + AutoSize = true, + Font = new Font("微软雅黑", 8, FontStyle.Bold) + }; + + _previewListView = new ListView + { + View = System.Windows.Forms.View.Details, + FullRowSelect = true, + GridLines = true, + Font = new Font("微软雅黑", 8) + }; + _previewListView.Columns.Add("分层名称", 150); + _previewListView.Columns.Add("元素数量", 80); + _previewListView.Columns.Add("输出文件", 200); + _previewListView.Columns.Add("状态", 100); + + // 进度和状态 + _progressBar = new ProgressBar + { + Visible = false + }; + + _statusLabel = new Label + { + Text = "就绪", + Font = new Font("微软雅黑", 8), + ForeColor = System.Drawing.Color.DarkBlue, + AutoSize = true + }; + + // 按钮 + _previewButton = new Button + { + Text = "预览分层", + Font = new Font("微软雅黑", 8), + Size = new Size(100, 30) + }; + + _executeButton = new Button + { + Text = "开始拆分", + Font = new Font("微软雅黑", 8), + Size = new Size(100, 30), + Enabled = false + }; + + _cancelButton = new Button + { + Text = "取消", + Font = new Font("微软雅黑", 8), + Size = new Size(80, 30), + DialogResult = DialogResult.Cancel + }; + + _helpButton = new Button + { + Text = "帮助", + Font = new Font("微软雅黑", 8), + Size = new Size(80, 30) + }; + + // 添加所有控件到窗体 + this.Controls.AddRange(new Control[] + { + strategyLabel, _strategyComboBox, + attributeLabel, _attributeComboBox, + outputLabel, _outputDirectoryTextBox, _browseDirectoryButton, + patternLabel, _fileNamePatternTextBox, + _includeEmptyLayersCheckBox, _createSubDirectoriesCheckBox, _generateReportCheckBox, + toleranceLabel, _elevationToleranceNumeric, + floorHeightLabel, _minFloorHeightNumeric, + formatLabel, _fileFormatComboBox, + previewLabel, _previewListView, + _progressBar, _statusLabel, + _previewButton, _executeButton, _cancelButton, _helpButton + }); + } + + private void LayoutControls() + { + int margin = 15; + int currentY = margin; + int labelHeight = 20; + int controlHeight = 25; + int spacing = 8; + + // 分层策略 + var strategyLabel = this.Controls.OfType