二次开发实战案例
第十二章:二次开发实战案例
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 总结
通过本章的实战案例,我们完成了:
- 元素类型定义:使用GUID唯一标识
- 元素类实现:继承DirectComponent,定义属性和几何方法
- 操作类实现:处理创建、编辑、绘制、夹点
- 三维渲染:实现IElement3dAction生成3D模型
- 命令集成:使用CommandMethod特性定义命令
- UI集成:添加工具栏按钮
这个停车位模块展示了完整的FY_Layout二次开发流程。您可以参考这个案例开发其他自定义元素。
12.6 进阶建议
- 性能优化:对于大量停车位,考虑使用实例化渲染
- 数据交互:可以连接数据库实时更新占用状态
- 统计功能:添加停车场容量统计和利用率分析
- 导出功能:支持导出停车场数据为Excel或其他格式
- 配置模板:创建可复用的停车场布置模板
恭喜您完成了FY_Layout的完整学习!现在您已经具备了开发LightCAD插件的全面能力。

浙公网安备 33010602011771号