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