二次开发实战案例

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

12.1 实战案例概述

本章将通过完整的实战案例,将前面章节学习的内容融会贯通。我们将开发一个"停车场"模块,包含:

  • 停车位元素
  • 停车场区域
  • 二维绘制和三维显示
  • 属性编辑
  • 命令和UI

12.2 案例一:停车位元素开发

12.2.1 需求分析

停车位元素需要:

  • 标准尺寸(可配置宽度、长度)
  • 编号标注
  • 状态(空闲、占用)
  • 类型(普通、残疾人专用、充电桩)
  • 二维和三维显示

12.2.2 元素类型定义

// LayoutElementType.cs 中添加
public static ElementType ParkingSpace = new ElementType
{
    Guid = Guid.ParseExact("{8A2B3C4D-5E6F-7890-ABCD-EF1234567890}", "B").ToLcGuid(),
    Name = "ParkingSpace",
    DispalyName = "停车位",
    ClassType = typeof(QdParkingSpace)
};

// 更新 All 数组
public static ElementType[] All = new ElementType[]
{
    Lawn, FoundationPit, ..., ParkingSpace
};

12.2.3 停车位元素类

// ParkingSpace/QdParkingSpace.cs
namespace QdLayout
{
    public class QdParkingSpace : DirectComponent
    {
        /// <summary>
        /// 停车位中心位置
        /// </summary>
        public Vector2 Position { get; set; }
        
        /// <summary>
        /// 旋转角度(弧度)
        /// </summary>
        public double Rotation
        {
            get => Properties.GetValue<double>("Rotation");
            set => SetProps((GetPropId(nameof(Rotation)), value));
        }
        
        /// <summary>
        /// 停车位宽度(默认2500mm)
        /// </summary>
        public double Width
        {
            get => Properties.GetValue<double>("Width");
            set => SetProps((GetPropId(nameof(Width)), value));
        }
        
        /// <summary>
        /// 停车位长度(默认5000mm)
        /// </summary>
        public double Length
        {
            get => Properties.GetValue<double>("Length");
            set => SetProps((GetPropId(nameof(Length)), value));
        }
        
        /// <summary>
        /// 停车位编号
        /// </summary>
        public string Number
        {
            get => Properties.GetValue<string>("Number");
            set => SetProps((GetPropId(nameof(Number)), value));
        }
        
        /// <summary>
        /// 停车位类型
        /// </summary>
        public ParkingSpaceType SpaceType
        {
            get => (ParkingSpaceType)Properties.GetValue<int>("SpaceType");
            set => SetProps((GetPropId(nameof(SpaceType)), (int)value));
        }
        
        /// <summary>
        /// 占用状态
        /// </summary>
        public bool IsOccupied
        {
            get => Properties.GetValue<bool>("IsOccupied");
            set => SetProps((GetPropId(nameof(IsOccupied)), value));
        }
        
        public QdParkingSpace(QdParkingSpaceDef def) : base(def)
        {
            Type = LayoutElementType.ParkingSpace;
            Width = 2500;
            Length = 5000;
            SpaceType = ParkingSpaceType.Normal;
            IsOccupied = false;
            Number = "";
        }
        
        /// <summary>
        /// 获取停车位轮廓
        /// </summary>
        public override Curve2dGroupCollection GetShapes()
        {
            var shapes = new Curve2dGroupCollection();
            var group = new Curve2dGroup();
            
            // 计算四个角点
            var halfW = Width / 2;
            var halfL = Length / 2;
            
            var corners = new Vector2[]
            {
                new Vector2(-halfW, -halfL),
                new Vector2(halfW, -halfL),
                new Vector2(halfW, halfL),
                new Vector2(-halfW, halfL)
            };
            
            // 应用旋转和平移
            var transform = new Matrix3()
                .MakeRotation(Rotation)
                .Multiply(new Matrix3().MakeTranslation(Position.X, Position.Y));
            
            for (int i = 0; i < corners.Length; i++)
            {
                corners[i].ApplyMatrix3(transform);
            }
            
            // 创建轮廓线
            for (int i = 0; i < corners.Length; i++)
            {
                var next = (i + 1) % corners.Length;
                group.Curve2ds.Add(new Line2d(corners[i], corners[next]));
            }
            
            shapes.Add(group);
            return shapes;
        }
        
        public override Box2 GetBoundingBox()
        {
            var points = GetShapes()[0].Curve2ds.SelectMany(c => c.GetPoints()).ToArray();
            return new Box2().ExpandByPoints(points);
        }
        
        public override LcElement Clone()
        {
            var clone = new QdParkingSpace(Definition as QdParkingSpaceDef);
            clone.Copy(this);
            return clone;
        }
        
        public override void Copy(LcElement src)
        {
            base.Copy(src);
            var ps = src as QdParkingSpace;
            Position = ps.Position.Clone();
        }
    }
    
    public class QdParkingSpaceDef : LcComponentDefinition { }
    
    public enum ParkingSpaceType
    {
        Normal = 0,         // 普通
        Disabled = 1,       // 残疾人专用
        Charging = 2,       // 充电桩
        Reserved = 3        // 预留
    }
}

12.2.4 停车位操作类

// ParkingSpace/ParkingSpaceAction.cs
namespace QdLayout
{
    public class ParkingSpaceAction : DirectComponentAction
    {
        public ParkingSpaceAction() { }
        
        public ParkingSpaceAction(IDocumentEditor docEditor) : base(docEditor)
        {
            commandCtrl.WriteInfo("命令:ParkingSpace");
        }
        
        /// <summary>
        /// 单个停车位创建
        /// </summary>
        public async void ExecCreate(string[] args = null)
        {
            var pointInputer = new PointInputer(docEditor);
            
            commandCtrl.WriteInfo("指定停车位中心点:");
            var result1 = await pointInputer.Execute();
            if (result1.Status != InputStatus.OK) return;
            
            commandCtrl.WriteInfo("指定停车位方向点或输入角度:");
            var result2 = await pointInputer.Execute();
            if (result2.Status != InputStatus.OK) return;
            
            // 计算旋转角度
            var rotation = Math.Atan2(
                result2.Point.Y - result1.Point.Y,
                result2.Point.X - result1.Point.X
            );
            
            CreateParkingSpace(result1.Point, rotation);
        }
        
        /// <summary>
        /// 批量创建停车位(沿线)
        /// </summary>
        public async void ExecCreateBatch(string[] args = null)
        {
            var pointInputer = new PointInputer(docEditor);
            
            commandCtrl.WriteInfo("指定起点:");
            var result1 = await pointInputer.Execute();
            if (result1.Status != InputStatus.OK) return;
            
            commandCtrl.WriteInfo("指定终点:");
            var result2 = await pointInputer.Execute();
            if (result2.Status != InputStatus.OK) return;
            
            var textInputer = new CmdTextInputer(docEditor);
            commandCtrl.WriteInfo("输入停车位数量:");
            var countStr = await textInputer.Execute();
            if (!int.TryParse(countStr, out int count) || count < 1)
            {
                commandCtrl.WriteError("无效的数量");
                return;
            }
            
            CreateParkingSpaceBatch(result1.Point, result2.Point, count);
        }
        
        private void CreateParkingSpace(Vector2 position, double rotation)
        {
            var doc = docRt.Document;
            var def = docRt.GetUseComDef($"{NamespaceKey}.交通设施", "停车位", null) as QdParkingSpaceDef;
            
            var parkingSpace = new QdParkingSpace(def)
            {
                Position = position,
                Rotation = rotation,
                Number = GenerateNumber()
            };
            
            parkingSpace.Initilize(doc);
            parkingSpace.ResetBoundingBox();
            parkingSpace.Layer = GetLayer().Name;
            
            vportRt.ActiveElementSet.InsertElement(parkingSpace);
            commandCtrl.WriteInfo($"停车位 {parkingSpace.Number} 创建成功");
        }
        
        private void CreateParkingSpaceBatch(Vector2 start, Vector2 end, int count)
        {
            var direction = end - start;
            var length = direction.Length();
            var rotation = Math.Atan2(direction.Y, direction.X) + Math.PI / 2;
            var step = length / count;
            var normalDir = direction.Normalize();
            
            for (int i = 0; i < count; i++)
            {
                var position = start + normalDir * (step * (i + 0.5));
                CreateParkingSpace(position, rotation);
            }
            
            commandCtrl.WriteInfo($"批量创建 {count} 个停车位完成");
        }
        
        private string GenerateNumber()
        {
            var existingNumbers = docRt.Document.Elements
                .OfType<QdParkingSpace>()
                .Select(p => p.Number)
                .Where(n => !string.IsNullOrEmpty(n) && n.StartsWith("P"))
                .Select(n => int.TryParse(n.Substring(1), out int num) ? num : 0)
                .ToList();
            
            int nextNum = existingNumbers.Count > 0 ? existingNumbers.Max() + 1 : 1;
            return $"P{nextNum:D3}";
        }
        
        /// <summary>
        /// 绘制停车位
        /// </summary>
        public override void Draw(LcCanvas2d canvas, LcElement element, Matrix3 matrix)
        {
            var ps = element as QdParkingSpace;
            var shapes = ps.GetShapes();
            
            // 根据状态和类型选择颜色
            var color = GetDisplayColor(ps);
            var pen = new LcPaint
            {
                Color = color,
                Width = ps.IsSelected ? 3 : 2,
                StrokeStyle = StrokeStyle.Solid
            };
            
            // 绘制轮廓
            foreach (var curve in shapes[0].Curve2ds)
            {
                canvas.DrawCurve(pen, curve, matrix);
            }
            
            // 绘制编号
            if (!string.IsNullOrEmpty(ps.Number))
            {
                var textPaint = new LcTextPaint
                {
                    Color = color,
                    FontName = "Arial",
                    Size = 400,
                    Position = ps.Position,
                    HorizontalAlign = TextAlign.Center,
                    VerticalAlign = TextVAlign.Middle
                };
                canvas.DrawText(textPaint, ps.Number, matrix, out _);
            }
            
            // 绘制类型标识
            if (ps.SpaceType != ParkingSpaceType.Normal)
            {
                DrawTypeIcon(canvas, ps, matrix);
            }
        }
        
        private Color GetDisplayColor(QdParkingSpace ps)
        {
            if (ps.IsOccupied)
                return new Color(0xFF0000);  // 红色-占用
            
            return ps.SpaceType switch
            {
                ParkingSpaceType.Disabled => new Color(0x0000FF),   // 蓝色-残疾人
                ParkingSpaceType.Charging => new Color(0x00FF00),   // 绿色-充电
                ParkingSpaceType.Reserved => new Color(0xFFFF00),   // 黄色-预留
                _ => new Color(0x808080)                            // 灰色-普通
            };
        }
        
        private void DrawTypeIcon(LcCanvas2d canvas, QdParkingSpace ps, Matrix3 matrix)
        {
            var iconPos = ps.Position + new Vector2(0, ps.Length / 4);
            iconPos.ApplyMatrix3(matrix);
            
            var text = ps.SpaceType switch
            {
                ParkingSpaceType.Disabled => "♿",
                ParkingSpaceType.Charging => "⚡",
                ParkingSpaceType.Reserved => "R",
                _ => ""
            };
            
            var textPaint = new LcTextPaint
            {
                Color = GetDisplayColor(ps),
                FontName = "Segoe UI Symbol",
                Size = 600,
                Position = iconPos
            };
            canvas.DrawText(textPaint, text, new Matrix3(), out _);
        }
        
        /// <summary>
        /// 获取控制夹点
        /// </summary>
        public override ControlGrip[] GetControlGrips(LcElement element)
        {
            var ps = element as QdParkingSpace;
            return new ControlGrip[]
            {
                new ControlGrip
                {
                    Element = ps,
                    Name = "Center",
                    Position = ps.Position.Clone(),
                    Type = GripType.Move
                },
                new ControlGrip
                {
                    Element = ps,
                    Name = "Rotate",
                    Position = ps.Position + new Vector2(0, ps.Length / 2 + 500)
                        .RotateAround(new Vector2(), ps.Rotation),
                    Type = GripType.Rotate
                }
            };
        }
        
        public override void SetDragGrip(LcElement element, ControlGrip grip, Vector2 position, bool isEnd)
        {
            if (!isEnd) return;
            
            var ps = element as QdParkingSpace;
            
            switch (grip.Name)
            {
                case "Center":
                    var offset = position - grip.Position;
                    ps.Position.Add(offset);
                    break;
                    
                case "Rotate":
                    ps.Rotation = Math.Atan2(
                        position.Y - ps.Position.Y,
                        position.X - ps.Position.X
                    ) - Math.PI / 2;
                    break;
            }
            
            ps.ResetBoundingBox();
        }
        
        /// <summary>
        /// 获取属性编辑器
        /// </summary>
        public override List<PropertyObserver> GetPropertyObservers()
        {
            return new List<PropertyObserver>
            {
                new PropertyObserver
                {
                    Name = "Number",
                    DisplayName = "编号",
                    CategoryName = "Basic",
                    CategoryDisplayName = "基本",
                    PropType = PropertyType.String,
                    Getter = (ele) => (ele as QdParkingSpace).Number,
                    Setter = (ele, value) => (ele as QdParkingSpace).Number = value.ToString()
                },
                new PropertyObserver
                {
                    Name = "Width",
                    DisplayName = "宽度",
                    CategoryName = "Size",
                    CategoryDisplayName = "尺寸",
                    PropType = PropertyType.Double,
                    Getter = (ele) => (ele as QdParkingSpace).Width,
                    Setter = (ele, value) =>
                    {
                        if (double.TryParse(value.ToString(), out var w) && w > 0)
                            (ele as QdParkingSpace).Width = w;
                    }
                },
                new PropertyObserver
                {
                    Name = "Length",
                    DisplayName = "长度",
                    CategoryName = "Size",
                    CategoryDisplayName = "尺寸",
                    PropType = PropertyType.Double,
                    Getter = (ele) => (ele as QdParkingSpace).Length,
                    Setter = (ele, value) =>
                    {
                        if (double.TryParse(value.ToString(), out var l) && l > 0)
                            (ele as QdParkingSpace).Length = l;
                    }
                },
                new PropertyObserver
                {
                    Name = "SpaceType",
                    DisplayName = "类型",
                    CategoryName = "Basic",
                    CategoryDisplayName = "基本",
                    PropType = PropertyType.Array,
                    Source = (ele) => new[] { "普通", "残疾人专用", "充电桩", "预留" },
                    Getter = (ele) => ((int)(ele as QdParkingSpace).SpaceType) switch
                    {
                        0 => "普通",
                        1 => "残疾人专用",
                        2 => "充电桩",
                        3 => "预留",
                        _ => "普通"
                    },
                    Setter = (ele, value) =>
                    {
                        var type = value.ToString() switch
                        {
                            "残疾人专用" => ParkingSpaceType.Disabled,
                            "充电桩" => ParkingSpaceType.Charging,
                            "预留" => ParkingSpaceType.Reserved,
                            _ => ParkingSpaceType.Normal
                        };
                        (ele as QdParkingSpace).SpaceType = type;
                    }
                },
                new PropertyObserver
                {
                    Name = "IsOccupied",
                    DisplayName = "占用",
                    CategoryName = "Status",
                    CategoryDisplayName = "状态",
                    PropType = PropertyType.Bool,
                    Getter = (ele) => (ele as QdParkingSpace).IsOccupied,
                    Setter = (ele, value) => (ele as QdParkingSpace).IsOccupied = Convert.ToBoolean(value)
                }
            };
        }
        
        private LcLayer GetLayer()
        {
            var layer = docRt.Document.Layers.FirstOrDefault(l => l.Name == "Layout_ParkingSpace");
            if (layer == null)
            {
                layer = docRt.Document.CreateObject<LcLayer>();
                layer.Name = "Layout_ParkingSpace";
                layer.Color = 0x808080;
                layer.SetLineType(new LcLineType("ByLayer"));
                docRt.Document.Layers.Add(layer);
            }
            return layer;
        }
    }
}

12.2.5 三维操作类

// ParkingSpace/ParkingSpace3dAction.cs
namespace QdLayout
{
    public class ParkingSpace3dAction : IElement3dAction
    {
        public Object3D Get3dObject(LcElement element, DocumentRuntime docRt)
        {
            var ps = element as QdParkingSpace;
            var root = new Object3D();
            
            // 创建地面标线
            var lineMarking = CreateLineMarking(ps);
            root.Add(lineMarking);
            
            // 如果是充电桩,添加充电桩模型
            if (ps.SpaceType == ParkingSpaceType.Charging)
            {
                var charger = CreateChargingStation(ps);
                root.Add(charger);
            }
            
            // 设置位置和旋转
            root.Position.X = ps.Position.X;
            root.Position.Z = ps.Position.Y;
            root.Rotation.Y = -ps.Rotation;
            
            return root;
        }
        
        private Object3D CreateLineMarking(QdParkingSpace ps)
        {
            var marking = new Object3D();
            var lineWidth = 100;  // 100mm宽的标线
            
            var material = new MeshBasicMaterial
            {
                Color = new ThreeJs4Net.Color(0xFFFFFF)
            };
            
            // 左侧线
            var leftLine = new Mesh(
                new BoxGeometry(lineWidth, 10, ps.Length),
                material
            );
            leftLine.Position.X = -ps.Width / 2;
            marking.Add(leftLine);
            
            // 右侧线
            var rightLine = new Mesh(
                new BoxGeometry(lineWidth, 10, ps.Length),
                material
            );
            rightLine.Position.X = ps.Width / 2;
            marking.Add(rightLine);
            
            // 底部线
            var bottomLine = new Mesh(
                new BoxGeometry(ps.Width + lineWidth, 10, lineWidth),
                material
            );
            bottomLine.Position.Z = -ps.Length / 2;
            marking.Add(bottomLine);
            
            return marking;
        }
        
        private Object3D CreateChargingStation(QdParkingSpace ps)
        {
            var station = new Object3D();
            
            // 充电桩柱子
            var pillar = new Mesh(
                new BoxGeometry(200, 1500, 200),
                new MeshLambertMaterial { Color = new ThreeJs4Net.Color(0x00AA00) }
            );
            pillar.Position.Y = 750;
            pillar.Position.Z = ps.Length / 2 - 300;
            station.Add(pillar);
            
            // 充电桩显示屏
            var screen = new Mesh(
                new BoxGeometry(300, 400, 50),
                new MeshLambertMaterial { Color = new ThreeJs4Net.Color(0x333333) }
            );
            screen.Position.Y = 1200;
            screen.Position.Z = ps.Length / 2 - 200;
            station.Add(screen);
            
            return station;
        }
    }
}

12.3 命令和UI注册

12.3.1 添加命令

// LayoutCmds.cs 中添加
[CommandMethod(Name = "ParkingSpace", ShortCuts = "PS")]
public CommandResult DrawParkingSpace(IDocumentEditor docEditor, string[] args)
{
    var action = new ParkingSpaceAction(docEditor);
    action.ExecCreate(args);
    return CommandResult.Succ();
}

[CommandMethod(Name = "ParkingSpaceBatch", ShortCuts = "PSB")]
public CommandResult DrawParkingSpaceBatch(IDocumentEditor docEditor, string[] args)
{
    var action = new ParkingSpaceAction(docEditor);
    action.ExecCreateBatch(args);
    return CommandResult.Succ();
}

12.3.2 更新UI

// LayoutPlugin.cs 中更新 TabItem
new TabButtonGroup
{
    Name = "ParkingGroup",
    Buttons = new List<TabButton>
    {
        new TabButton
        {
            Name = "ParkingSpace",
            Text = "停车位",
            Icon = Properties.Resources.停车位,
            IsCommand = true,
            DropDowns = new List<TabButton>
            {
                new TabButton { Name = "ParkingSpace", Text = "单个停车位", IsCommand = true },
                new TabButton { Name = "ParkingSpaceBatch", Text = "批量创建", IsCommand = true }
            }
        }
    }
}

12.3.3 注册元素和操作

// LayoutPlugin.cs Loaded() 中添加
public void Loaded()
{
    // ... 现有代码
    
    // 注册停车位
    LcDocument.ElementActions.Add(LayoutElementType.ParkingSpace, new ParkingSpaceAction());
    LcDocument.Element3dActions.Add(LayoutElementType.ParkingSpace, new ParkingSpace3dAction());
}

12.4 测试与验证

12.4.1 功能测试清单

12.4.2 边界测试

// 测试用例
// 1. 创建宽度为0的停车位 - 应该被拒绝
// 2. 创建负数长度的停车位 - 应该被拒绝
// 3. 批量创建0个停车位 - 应该被拒绝
// 4. 旋转360度以上 - 应该正常处理

12.5 总结

通过本章的实战案例,我们完成了:

  1. 元素类型定义:使用GUID唯一标识
  2. 元素类实现:继承DirectComponent,定义属性和几何方法
  3. 操作类实现:处理创建、编辑、绘制、夹点
  4. 三维渲染:实现IElement3dAction生成3D模型
  5. 命令集成:使用CommandMethod特性定义命令
  6. UI集成:添加工具栏按钮

这个停车位模块展示了完整的FY_Layout二次开发流程。您可以参考这个案例开发其他自定义元素。

12.6 进阶建议

  1. 性能优化:对于大量停车位,考虑使用实例化渲染
  2. 数据交互:可以连接数据库实时更新占用状态
  3. 统计功能:添加停车场容量统计和利用率分析
  4. 导出功能:支持导出停车场数据为Excel或其他格式
  5. 配置模板:创建可复用的停车场布置模板

恭喜您完成了FY_Layout的完整学习!现在您已经具备了开发LightCAD插件的全面能力。


posted @ 2026-01-31 16:03  我才是银古  阅读(2)  评论(0)    收藏  举报