znlgis 博客

GIS开发与技术分享

第二十章:实战案例与二次开发指南

20.1 实战概述

20.1.1 本章目标

本章通过多个完整的实战案例,展示如何基于LightCAD进行二次开发。从简单的工具插件到完整的行业应用,逐步深入地介绍二次开发的方法和技巧。

20.1.2 案例列表

案例 难度 内容
案例一:批量绘图工具 ★★☆ 批量生成标准图框
案例二:参数化构件 ★★★ 创建参数化的建筑构件
案例三:自动标注插件 ★★★ 自动添加尺寸标注
案例四:文件格式转换器 ★★★★ 实现自定义文件格式导入
案例五:行业应用插件 ★★★★★ 开发完整的行业CAD插件

20.2 案例一:批量绘图工具

20.2.1 需求描述

开发一个批量生成标准图框的工具,支持A0-A4五种纸张规格,自动绘制图框线、标题栏和图签信息。

20.2.2 实现代码

public class DrawingFramePlugin : IPlugin, ICommandPlugin
{
    public string Name => "图框生成器";
    public string Version => "1.0.0";
    public string Description => "批量生成标准工程图框";
    public string Author => "LightCAD开发者";

    public void Initialize() { }
    public void Shutdown() { }

    public void RegisterCommands(CommandRegistry registry)
    {
        registry.Register(new CommandInfo
        {
            Name = "FRAME",
            DisplayName = "生成图框",
            Category = "工具",
            Action = GenerateFrame
        });
    }

    private void GenerateFrame()
    {
        // 获取用户选择的纸张规格
        var paperSize = SelectPaperSize();
        if (paperSize == null) return;

        var document = DocumentManager.Current;

        // 创建图框图层
        var frameLayer = document.Layers.Create("图框");
        frameLayer.Color = LcColor.White;

        // 绘制外框
        var outerFrame = CreateRectangle(
            new Point2d(0, 0),
            new Point2d(paperSize.Width, paperSize.Height));
        outerFrame.LayerName = "图框";
        document.Entities.Add(outerFrame);

        // 绘制内框(留出装订边距)
        var innerFrame = CreateRectangle(
            new Point2d(25, 10),  // 左边25mm装订边
            new Point2d(paperSize.Width - 10, paperSize.Height - 10));
        innerFrame.LayerName = "图框";
        document.Entities.Add(innerFrame);

        // 绘制标题栏
        DrawTitleBlock(document, paperSize);

        // 添加图签信息
        AddTitleInfo(document, paperSize);
    }

    private void DrawTitleBlock(LcDocument document, PaperSize paperSize)
    {
        var titleWidth = 180;
        var titleHeight = 56;

        var x = paperSize.Width - 10 - titleWidth;
        var y = 10;

        // 标题栏外框
        var titleFrame = CreateRectangle(
            new Point2d(x, y),
            new Point2d(x + titleWidth, y + titleHeight));
        titleFrame.LayerName = "图框";
        titleFrame.LineWeight = 0.5;
        document.Entities.Add(titleFrame);

        // 标题栏内部分割线
        var divisions = new[]
        {
            // 水平线(从下到上)
            (new Point2d(x, y + 14), new Point2d(x + titleWidth, y + 14)),
            (new Point2d(x, y + 28), new Point2d(x + titleWidth, y + 28)),
            (new Point2d(x, y + 42), new Point2d(x + titleWidth, y + 42)),
            // 垂直线
            (new Point2d(x + 30, y), new Point2d(x + 30, y + titleHeight)),
            (new Point2d(x + 60, y), new Point2d(x + 60, y + titleHeight)),
            (new Point2d(x + 120, y), new Point2d(x + 120, y + titleHeight)),
        };

        foreach (var (start, end) in divisions)
        {
            var line = new LcLine
            {
                StartPoint = start,
                EndPoint = end,
                LayerName = "图框"
            };
            document.Entities.Add(line);
        }
    }

    private void AddTitleInfo(LcDocument document, PaperSize paperSize)
    {
        var x = paperSize.Width - 10 - 180;
        var y = 10;

        // 添加文字标签
        AddText(document, "设计", new Point2d(x + 5, y + 3), 3);
        AddText(document, "校对", new Point2d(x + 5, y + 17), 3);
        AddText(document, "审核", new Point2d(x + 5, y + 31), 3);
        AddText(document, "批准", new Point2d(x + 5, y + 45), 3);
        AddText(document, "工程名称", new Point2d(x + 65, y + 45), 3);
        AddText(document, "图纸编号", new Point2d(x + 125, y + 3), 3);
    }

    private void AddText(LcDocument document, string content,
                         Point2d position, double height)
    {
        var text = new LcText
        {
            Content = content,
            InsertionPoint = position,
            Height = height,
            FontName = "SimSun",
            LayerName = "图框"
        };
        document.Entities.Add(text);
    }
}

public class PaperSize
{
    public string Name { get; set; }
    public double Width { get; set; }
    public double Height { get; set; }

    public static readonly PaperSize A0 = new PaperSize { Name = "A0", Width = 1189, Height = 841 };
    public static readonly PaperSize A1 = new PaperSize { Name = "A1", Width = 841, Height = 594 };
    public static readonly PaperSize A2 = new PaperSize { Name = "A2", Width = 594, Height = 420 };
    public static readonly PaperSize A3 = new PaperSize { Name = "A3", Width = 420, Height = 297 };
    public static readonly PaperSize A4 = new PaperSize { Name = "A4", Width = 297, Height = 210 };
}

20.3 案例二:参数化构件

20.3.1 需求描述

创建一个参数化的门窗构件,支持通过修改参数(宽度、高度、开启方向等)自动更新几何形状。

20.3.2 实现代码

public class ParametricDoor : LcEntity, IUpdateObject
{
    // 参数
    public double Width { get; set; } = 900;
    public double Height { get; set; } = 2100;
    public double WallThickness { get; set; } = 240;
    public DoorOpenDirection OpenDirection { get; set; }
        = DoorOpenDirection.LeftInward;
    public Point2d InsertionPoint { get; set; }
    public double Rotation { get; set; }

    public override string TypeName => "ParametricDoor";

    /// <summary>
    /// 生成门的平面图表示
    /// </summary>
    public List<LcEntity> GeneratePlanView()
    {
        var entities = new List<LcEntity>();

        // 门洞(在墙上的开口)
        var doorOpening = new LcLine
        {
            StartPoint = new Point2d(0, 0),
            EndPoint = new Point2d(Width, 0)
        };

        // 门扇
        var doorPanel = new LcLine
        {
            StartPoint = new Point2d(0, 0),
            EndPoint = GetDoorEndPoint()
        };
        doorPanel.LineWeight = 0.5;
        entities.Add(doorPanel);

        // 开门弧线
        var arc = CreateOpeningArc();
        entities.Add(arc);

        // 应用变换(位置和旋转)
        var transform = Matrix4d.CreateRotationZ(Rotation)
            * Matrix4d.CreateTranslation(
                InsertionPoint.X, InsertionPoint.Y, 0);

        foreach (var entity in entities)
        {
            entity.TransformBy(transform);
        }

        return entities;
    }

    private Point2d GetDoorEndPoint()
    {
        return OpenDirection switch
        {
            DoorOpenDirection.LeftInward =>
                new Point2d(0, Width),
            DoorOpenDirection.RightInward =>
                new Point2d(Width, Width),
            DoorOpenDirection.LeftOutward =>
                new Point2d(0, -Width),
            DoorOpenDirection.RightOutward =>
                new Point2d(Width, -Width),
            _ => new Point2d(0, Width)
        };
    }

    private LcArc CreateOpeningArc()
    {
        var isInward = OpenDirection == DoorOpenDirection.LeftInward
            || OpenDirection == DoorOpenDirection.RightInward;
        var isLeft = OpenDirection == DoorOpenDirection.LeftInward
            || OpenDirection == DoorOpenDirection.LeftOutward;

        var center = isLeft ? new Point2d(0, 0) : new Point2d(Width, 0);
        var startAngle = isInward ? 0 : Math.PI;
        var endAngle = isInward ? Math.PI / 2 : Math.PI * 3 / 2;

        return new LcArc
        {
            Center = center,
            Radius = Width,
            StartAngle = startAngle,
            EndAngle = endAngle,
            IsCounterClockwise = isLeft
        };
    }

    public override BoundingBox2d Bounds
    {
        get
        {
            return new BoundingBox2d
            {
                Min = new Point2d(
                    InsertionPoint.X - Width,
                    InsertionPoint.Y - Width),
                Max = new Point2d(
                    InsertionPoint.X + Width * 2,
                    InsertionPoint.Y + Width)
            };
        }
    }

    public override void TransformBy(Matrix4d matrix)
    {
        var p = new Point3d(InsertionPoint.X, InsertionPoint.Y, 0);
        var transformed = matrix.Transform(p);
        InsertionPoint = new Point2d(transformed.X, transformed.Y);
    }

    public override LcEntity Clone()
    {
        return new ParametricDoor
        {
            Width = Width,
            Height = Height,
            WallThickness = WallThickness,
            OpenDirection = OpenDirection,
            InsertionPoint = InsertionPoint,
            Rotation = Rotation
        };
    }

    public override double DistanceTo(Point2d point)
    {
        return point.DistanceTo(InsertionPoint);
    }

    public bool NeedsUpdate { get; private set; }
    public void OnUpdate(UpdateContext context) { NeedsUpdate = false; }
}

public enum DoorOpenDirection
{
    LeftInward,     // 左开内推
    RightInward,    // 右开内推
    LeftOutward,    // 左开外推
    RightOutward    // 右开外推
}

20.4 案例三:自动标注插件

20.4.1 需求描述

开发一个自动标注插件,能够为选中的图元自动添加适当的尺寸标注。

20.4.2 实现代码

public class AutoDimensionPlugin : IPlugin, ICommandPlugin
{
    public string Name => "自动标注";
    public string Version => "1.0.0";
    public string Description => "自动为选中的图元添加标注";
    public string Author => "LightCAD开发者";

    public void Initialize() { }
    public void Shutdown() { }

    public void RegisterCommands(CommandRegistry registry)
    {
        registry.Register(new CommandInfo
        {
            Name = "AUTODIM",
            DisplayName = "自动标注",
            Category = "标注",
            Action = AutoDimension
        });
    }

    private void AutoDimension()
    {
        var document = DocumentManager.Current;
        var selection = SelectionManager.Current.SelectedEntities;

        if (selection.Count == 0)
        {
            StatusBar.SetPrompt("请先选择要标注的图元");
            return;
        }

        var dimLayer = document.Layers.Create("标注");
        dimLayer.Color = new LcColor(0, 255, 0);

        foreach (var entity in selection)
        {
            var dimensions = GenerateDimensions(entity);
            foreach (var dim in dimensions)
            {
                dim.LayerName = "标注";
                document.Entities.Add(dim);
            }
        }

        StatusBar.SetPrompt($"已为 {selection.Count} 个图元添加标注");
    }

    /// <summary>
    /// 为指定实体生成标注
    /// </summary>
    private List<LcDimension> GenerateDimensions(LcEntity entity)
    {
        var dimensions = new List<LcDimension>();

        switch (entity)
        {
            case LcLine line:
                // 线段标注长度
                var offset = CalculateOffset(line);
                dimensions.Add(new LcDimLinear
                {
                    DefPoint1 = line.StartPoint,
                    DefPoint2 = line.EndPoint,
                    DimLinePoint = line.MidPoint + offset,
                    LinearType = DimLinearType.Aligned,
                    Style = DimStyle.Default
                });
                break;

            case LcCircle circle:
                // 圆标注直径
                dimensions.Add(new LcDimDiameter
                {
                    Center = circle.Center,
                    ChordPoint = circle.PointAt(Math.PI / 4),
                    Style = DimStyle.Default
                });
                break;

            case LcArc arc:
                // 圆弧标注半径
                dimensions.Add(new LcDimRadial
                {
                    Center = arc.Center,
                    ChordPoint = arc.MidPoint,
                    Style = DimStyle.Default
                });
                break;

            case LcPolyline polyline:
                // 多段线标注每一段
                for (int i = 0; i < polyline.SegmentCount; i++)
                {
                    var (start, end) = polyline.GetSegment(i);
                    if (!start.IsArc)
                    {
                        var segOffset = CalculateSegOffset(
                            start.Position, end.Position, polyline);
                        dimensions.Add(new LcDimLinear
                        {
                            DefPoint1 = start.Position,
                            DefPoint2 = end.Position,
                            DimLinePoint = start.Position.MidPoint(
                                end.Position) + segOffset,
                            LinearType = DimLinearType.Aligned,
                            Style = DimStyle.Default
                        });
                    }
                }
                break;
        }

        return dimensions;
    }

    private Vector2d CalculateOffset(LcLine line)
    {
        var perpendicular = line.Direction.Perpendicular();
        return perpendicular * 15; // 15mm的偏移
    }
}

20.5 案例四:文件格式转换器

20.5.1 需求描述

实现一个CSV坐标文件到LightCAD多段线的转换器,用于将测量数据导入到CAD中。

20.5.2 实现代码

public class CsvImportPlugin : IPlugin, ICommandPlugin
{
    public string Name => "CSV导入";
    public string Version => "1.0.0";
    public string Description => "从CSV文件导入坐标数据";
    public string Author => "LightCAD开发者";

    public void Initialize() { }
    public void Shutdown() { }

    public void RegisterCommands(CommandRegistry registry)
    {
        registry.Register(new CommandInfo
        {
            Name = "CSVIMPORT",
            DisplayName = "导入CSV",
            Category = "导入导出",
            Action = ImportCsv
        });
    }

    private void ImportCsv()
    {
        // 选择CSV文件
        using var dialog = new OpenFileDialog
        {
            Filter = "CSV文件|*.csv|文本文件|*.txt",
            Title = "选择坐标文件"
        };

        if (dialog.ShowDialog() != DialogResult.OK) return;

        try
        {
            var entities = ParseCsvFile(dialog.FileName);
            var document = DocumentManager.Current;

            // 创建导入图层
            var layer = document.Layers.Create("CSV导入");
            layer.Color = new LcColor(255, 128, 0);

            foreach (var entity in entities)
            {
                entity.LayerName = "CSV导入";
                document.Entities.Add(entity);
            }

            // 缩放到全部
            ViewportManager.Current.ActiveViewport
                .PanZoomController.ZoomExtents(
                    document.CalculateBounds());

            StatusBar.SetPrompt(
                $"成功导入 {entities.Count} 个图元");
        }
        catch (Exception ex)
        {
            MessageBox.Show($"导入失败:{ex.Message}",
                "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    }

    private List<LcEntity> ParseCsvFile(string filePath)
    {
        var entities = new List<LcEntity>();
        var lines = File.ReadAllLines(filePath);

        var currentPolyline = new LcPolyline();
        var currentGroup = "";

        foreach (var line in lines)
        {
            if (string.IsNullOrWhiteSpace(line)) continue;
            if (line.StartsWith("#")) continue; // 跳过注释

            var parts = line.Split(',');
            if (parts.Length < 2) continue;

            if (parts.Length >= 3 && !double.TryParse(parts[2], out _))
            {
                // 第三列是组名
                if (currentPolyline.VertexCount > 0)
                {
                    entities.Add(currentPolyline);
                    currentPolyline = new LcPolyline();
                }
                currentGroup = parts[2].Trim();
                continue;
            }

            if (double.TryParse(parts[0], out var x) &&
                double.TryParse(parts[1], out var y))
            {
                currentPolyline.AddVertex(new Point2d(x, y));
            }
        }

        if (currentPolyline.VertexCount > 0)
        {
            entities.Add(currentPolyline);
        }

        return entities;
    }
}

20.6 案例五:行业应用插件

20.6.1 需求描述

开发一个简化的结构设计插件,支持绘制标准截面的钢柱和钢梁,并自动生成连接节点。

20.6.2 插件框架

public class StructuralPlugin : IPlugin, ICommandPlugin,
                                 IElementPlugin, IUIPlugin
{
    public string Name => "结构设计工具";
    public string Version => "1.0.0";
    public string Description => "钢结构设计辅助工具";
    public string Author => "LightCAD开发者";

    public void Initialize()
    {
        // 加载截面库
        SectionLibrary.Load("Sections/steel_sections.json");
    }

    public void Shutdown() { }

    public void RegisterCommands(CommandRegistry registry)
    {
        registry.Register(new CommandInfo
        {
            Name = "STEELCOLUMN",
            DisplayName = "钢柱",
            Category = "结构",
            Action = DrawSteelColumn
        });

        registry.Register(new CommandInfo
        {
            Name = "STEELBEAM",
            DisplayName = "钢梁",
            Category = "结构",
            Action = DrawSteelBeam
        });
    }

    public void RegisterElementTypes(ElementTypeRegistry registry)
    {
        registry.Register(new ElementType
        {
            Guid = new LcGuid("struct-steel-column"),
            Name = "SteelColumn",
            DisplayName = "钢柱",
            Category = "结构",
            Is3D = true,
            Creator = () => new SteelColumnElement(),
            Properties = new List<PropertyDefinition>
            {
                new PropertyDefinition("SectionType",
                    typeof(string), "H300x300x10x15"),
                new PropertyDefinition("Height",
                    typeof(double), 3600.0),
                new PropertyDefinition("Material",
                    typeof(string), "Q345B")
            }
        });
    }

    public void RegisterMenuItems(MenuRegistry registry)
    {
        registry.AddMenuItem("结构", "钢柱", DrawSteelColumn);
        registry.AddMenuItem("结构", "钢梁", DrawSteelBeam);
        registry.AddSeparator("结构");
        registry.AddMenuItem("结构", "截面库", ShowSectionLibrary);
    }

    public void RegisterToolbarItems(ToolbarRegistry registry)
    {
        registry.AddButton("结构工具栏", "钢柱",
            "Icons/column.png", DrawSteelColumn);
        registry.AddButton("结构工具栏", "钢梁",
            "Icons/beam.png", DrawSteelBeam);
    }

    public void RegisterPanels(PanelRegistry registry)
    {
        registry.AddPanel("截面属性",
            typeof(SectionPropertiesPanel));
    }

    private void DrawSteelColumn()
    {
        // 获取插入点
        var insertPoint = InputManager.GetPoint("指定柱底中心点:");

        // 选择截面
        var sectionType = ShowSectionSelector();
        if (sectionType == null) return;

        // 获取高度
        var height = InputManager.GetDistance("指定柱高:", 3600);

        // 创建钢柱
        var column = new SteelColumnElement
        {
            InsertionPoint = insertPoint,
            SectionType = sectionType,
            Height = height
        };

        DocumentManager.Current.Entities.Add(column);
    }

    private void DrawSteelBeam()
    {
        var startPoint = InputManager.GetPoint("指定梁起点:");
        var endPoint = InputManager.GetPoint("指定梁终点:");

        var sectionType = ShowSectionSelector();
        if (sectionType == null) return;

        var beam = new SteelBeamElement
        {
            StartPoint = startPoint,
            EndPoint = endPoint,
            SectionType = sectionType
        };

        DocumentManager.Current.Entities.Add(beam);
    }
}

/// <summary>
/// 截面库
/// </summary>
public class SectionLibrary
{
    private static Dictionary<string, SteelSection> sections = new();

    public static void Load(string filePath)
    {
        if (File.Exists(filePath))
        {
            var json = File.ReadAllText(filePath);
            sections = JsonSerializer
                .Deserialize<Dictionary<string, SteelSection>>(json);
        }
    }

    public static SteelSection GetSection(string type)
    {
        return sections.TryGetValue(type, out var section)
            ? section : null;
    }

    public static IEnumerable<string> GetAllTypes()
    {
        return sections.Keys;
    }
}

public class SteelSection
{
    public string Type { get; set; }
    public double Height { get; set; }      // 截面高度
    public double Width { get; set; }       // 翼缘宽度
    public double WebThickness { get; set; }    // 腹板厚度
    public double FlangeThickness { get; set; } // 翼缘厚度
    public double Area { get; set; }        // 截面面积
    public double Ix { get; set; }          // X轴惯性矩
    public double Iy { get; set; }          // Y轴惯性矩
    public double Weight { get; set; }      // 线重量(kg/m)
}

20.7 二次开发最佳实践

20.7.1 代码组织

MyPlugin/
├── MyPlugin.csproj              # 项目文件
├── Plugin.cs                    # 插件入口
├── Commands/                    # 命令实现
│   ├── DrawCommands.cs
│   └── EditCommands.cs
├── Elements/                    # 自定义图元
│   └── MyCustomElement.cs
├── UI/                          # UI组件
│   ├── MyPanel.axaml
│   └── MyPanel.axaml.cs
├── Data/                        # 数据文件
│   └── sections.json
└── Resources/                   # 资源文件
    └── Icons/

20.7.2 错误处理

// 所有命令应该有完善的错误处理
public void ExecuteCommand()
{
    try
    {
        // 验证前置条件
        if (DocumentManager.Current == null)
        {
            StatusBar.SetPrompt("请先打开或创建文档");
            return;
        }

        // 执行操作
        DoWork();

        // 记录撤销
        UndoManager.Record(new MyUndoAction());
    }
    catch (OperationCanceledException)
    {
        // 用户取消,不需要报错
        StatusBar.SetPrompt("操作已取消");
    }
    catch (Exception ex)
    {
        Logger.Error("命令执行失败", ex);
        MessageBox.Show($"操作失败:{ex.Message}");
    }
}

20.7.3 性能考虑

// 批量操作时禁止逐个刷新
using (document.BeginBatchUpdate())
{
    for (int i = 0; i < 1000; i++)
    {
        document.Entities.Add(CreateEntity(i));
    }
} // 结束时一次性刷新

// 大量计算时显示进度
using (var progress = new ProgressDialog("正在处理...", totalSteps))
{
    for (int i = 0; i < totalSteps; i++)
    {
        ProcessStep(i);
        progress.Update(i + 1);

        if (progress.IsCancelled)
            break;
    }
}

20.8 总结与展望

20.8.1 LightCAD的技术总结

通过20个章节的学习,我们全面了解了LightCAD的技术体系:

  1. 架构设计:6层分层架构,职责清晰,依赖单向
  2. 数学基础:完整的向量/矩阵/曲线/交集计算库
  3. 数据模型:灵活的实体类层次和元素类型系统
  4. 二维图元:7种基本图元覆盖常见绘图需求
  5. 三维建模:拉伸/旋转/放样/布尔等参数化建模
  6. 渲染系统:Three.js4Net + OpenTK双引擎渲染
  7. 视图构建:正交投影、隐藏线消除、剖面生成
  8. 交互系统:捕捉、选择、夹点编辑、命令行
  9. UI框架:WinForms + Avalonia混合界面
  10. 文件格式:DWG/DXF/SketchUp多格式兼容
  11. 插件系统:基于Weikio的灵活插件架构
  12. 行业扩展:建筑/结构/暖通/电气/给排水五大插件

20.8.2 学习建议

  1. 打好基础:先彻底理解MathLib和Core模块
  2. 动手实践:从简单的插件开始,逐步增加复杂度
  3. 阅读源码:深入阅读ViewBuilder等核心模块的源码
  4. 参考FY_Layout:学习FY_Layout如何基于LightCAD构建行业应用
  5. 参与社区:通过GitHub Issue和PR参与LightCAD的开发

20.8.3 未来展望

LightCAD作为一个活跃的开源项目,未来可能的发展方向包括:

  • 跨平台支持:充分利用.NET 8.0和Avalonia实现Linux/macOS支持
  • WebGL版本:基于Three.js实现浏览器端CAD
  • AI辅助设计:集成AI模型辅助设计和规范检查
  • 云端协同:增强SignalR协同功能,支持多人实时设计
  • BIM深度集成:更完善的IFC格式支持和BIM工作流

上一章第十九章:调试测试与性能优化

返回教程目录