mirror of
https://gitee.com/ThingsGateway/ThingsGateway.git
synced 2025-10-24 20:28:59 +08:00
feat: 上传 网关冗余/更新以及规则引擎 源码
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "9.0.1",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
],
|
||||
"rollForward": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,11 +39,7 @@
|
||||
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<PackageReference Include="Mapster" Version="7.4.0" />
|
||||
<PackageReference Include="MiniProfiler.AspNetCore.Mvc" Version="4.5.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<PluginVersion>10.0.0.29</PluginVersion>
|
||||
<ProPluginVersion>10.0.0.29</ProPluginVersion>
|
||||
<PluginVersion>10.0.1.1</PluginVersion>
|
||||
<ProPluginVersion>10.0.1.1</ProPluginVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Components.Controls;
|
||||
using ThingsGateway.Blazor.Diagrams.Core;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Controls.Default;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
using ThingsGateway.Blazor.Diagrams.Options;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams;
|
||||
|
||||
public class BlazorDiagram : Diagram
|
||||
{
|
||||
private readonly Dictionary<Type, Type> _componentsMapping;
|
||||
|
||||
public BlazorDiagram(BlazorDiagramOptions? options = null, bool registerDefaultBehaviors = true) : base(registerDefaultBehaviors)
|
||||
{
|
||||
_componentsMapping = new Dictionary<Type, Type>
|
||||
{
|
||||
[typeof(RemoveControl)] = typeof(RemoveControlWidget),
|
||||
[typeof(BoundaryControl)] = typeof(BoundaryControlWidget),
|
||||
[typeof(DragNewLinkControl)] = typeof(DragNewLinkControlWidget),
|
||||
[typeof(ArrowHeadControl)] = typeof(ArrowHeadControlWidget)
|
||||
};
|
||||
|
||||
Options = options ?? new BlazorDiagramOptions();
|
||||
}
|
||||
|
||||
public override BlazorDiagramOptions Options { get; }
|
||||
|
||||
public void RegisterComponent<TModel, TComponent>(bool replace = false)
|
||||
{
|
||||
RegisterComponent(typeof(TModel), typeof(TComponent), replace);
|
||||
}
|
||||
|
||||
public void RegisterComponent(Type modelType, Type componentType, bool replace = false)
|
||||
{
|
||||
if (!replace && _componentsMapping.ContainsKey(modelType))
|
||||
throw new Exception($"Component already registered for model '{modelType.Name}'.");
|
||||
|
||||
_componentsMapping[modelType] = componentType;
|
||||
}
|
||||
|
||||
public Type? GetComponent(Type modelType, bool checkSubclasses = true)
|
||||
{
|
||||
if (_componentsMapping.TryGetValue(modelType, out Type? value))
|
||||
return value;
|
||||
|
||||
if (!checkSubclasses)
|
||||
return null;
|
||||
|
||||
foreach (var rmt in _componentsMapping.Keys)
|
||||
{
|
||||
if (modelType.IsSubclassOf(rmt))
|
||||
return _componentsMapping[rmt];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Type? GetComponent<TModel>(bool checkSubclasses = true)
|
||||
{
|
||||
return GetComponent(typeof(TModel), checkSubclasses);
|
||||
}
|
||||
|
||||
public Type? GetComponent(Model model, bool checkSubclasses = true)
|
||||
{
|
||||
return GetComponent(model.GetType(), checkSubclasses);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ThingsGateway.Blazor.Diagrams;
|
||||
|
||||
public class BlazorDiagramsException : Exception
|
||||
{
|
||||
public BlazorDiagramsException(string? message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
<g transform="rotate(@Control.Angle.ToInvariantString())">
|
||||
<path d="@Control.Marker.Path" fill="black" cursor="move"></path>
|
||||
</g>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter]
|
||||
public ArrowHeadControl Control { get; set; } = null!;
|
||||
|
||||
[Parameter]
|
||||
public BaseLinkModel Model { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
@if (Model.IsSvg())
|
||||
{
|
||||
<rect fill="none"
|
||||
stroke="gray"
|
||||
stroke-width="1"
|
||||
stroke-dasharray="5"
|
||||
pointer-events="none"
|
||||
class="boundary"
|
||||
width="@Control.Bounds.Width.ToInvariantString()"
|
||||
height="@Control.Bounds.Height.ToInvariantString()">
|
||||
</rect>
|
||||
}
|
||||
else
|
||||
{
|
||||
<svg>
|
||||
<rect fill="none"
|
||||
stroke="gray"
|
||||
stroke-width="1"
|
||||
stroke-dasharray="5"
|
||||
pointer-events="none"
|
||||
class="boundary"
|
||||
width="@Control.Bounds.Width.ToInvariantString()"
|
||||
height="@Control.Bounds.Height.ToInvariantString()">
|
||||
</rect>
|
||||
</svg>
|
||||
}
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter]
|
||||
public BoundaryControl Control { get; set; } = null!;
|
||||
|
||||
[Parameter]
|
||||
public Model Model { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
@foreach (var model in BlazorDiagram.Controls.Models)
|
||||
{
|
||||
var controls = BlazorDiagram.Controls.GetFor(model)!;
|
||||
if (!controls.Visible || controls.Count == 0)
|
||||
continue;
|
||||
|
||||
if (Svg && model.IsSvg())
|
||||
{
|
||||
<g class="controls" data-model-type="@model.GetType().Name" data-model-id="@model.Id">
|
||||
@foreach (var control in controls)
|
||||
{
|
||||
var position = control.GetPosition(model);
|
||||
if (position == null)
|
||||
continue;
|
||||
|
||||
@RenderControl(model, control, position, true)
|
||||
}
|
||||
</g>
|
||||
}
|
||||
else if (!Svg && !model.IsSvg())
|
||||
{
|
||||
<div class="controls" data-model-type="@model.GetType().Name" data-model-id="@model.Id">
|
||||
@foreach (var control in controls)
|
||||
{
|
||||
var position = control.GetPosition(model);
|
||||
if (position == null)
|
||||
continue;
|
||||
|
||||
@RenderControl(model, control, position, false)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Controls;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Extensions;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
using ThingsGateway.Blazor.Diagrams.Extensions;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Components.Controls;
|
||||
|
||||
public partial class ControlsLayerRenderer : IDisposable
|
||||
{
|
||||
private bool _shouldRender;
|
||||
|
||||
[CascadingParameter] public BlazorDiagram BlazorDiagram { get; set; } = null!;
|
||||
|
||||
[Parameter] public bool Svg { get; set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
BlazorDiagram.Controls.ChangeCaused -= OnControlsChangeCaused;
|
||||
}
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
BlazorDiagram.Controls.ChangeCaused += OnControlsChangeCaused;
|
||||
}
|
||||
|
||||
protected override bool ShouldRender()
|
||||
{
|
||||
if (!_shouldRender)
|
||||
return false;
|
||||
|
||||
_shouldRender = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnControlsChangeCaused(Model cause)
|
||||
{
|
||||
if (Svg != cause.IsSvg())
|
||||
return;
|
||||
|
||||
_shouldRender = true;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private RenderFragment RenderControl(Model model, Control control, Point position, bool svg)
|
||||
{
|
||||
var componentType = BlazorDiagram.GetComponent(control.GetType());
|
||||
if (componentType == null)
|
||||
throw new BlazorDiagramsException(
|
||||
$"A component couldn't be found for the user action {control.GetType().Name}");
|
||||
|
||||
return builder =>
|
||||
{
|
||||
builder.OpenElement(0, svg ? "g" : "div");
|
||||
builder.AddAttribute(1, "class",
|
||||
$"{(control is ExecutableControl ? "executable " : "")}diagram-control {control.GetType().Name}");
|
||||
if (svg)
|
||||
builder.AddAttribute(2, "transform",
|
||||
$"translate({position.X.ToInvariantString()} {position.Y.ToInvariantString()})");
|
||||
else
|
||||
builder.AddAttribute(2, "style",
|
||||
$"top: {position.Y.ToInvariantString()}px; left: {position.X.ToInvariantString()}px");
|
||||
|
||||
if (control is ExecutableControl ec)
|
||||
{
|
||||
builder.AddAttribute(3, "onpointerdown",
|
||||
EventCallback.Factory.Create<PointerEventArgs>(this, e => OnPointerDown(e, model, ec)));
|
||||
builder.AddEventStopPropagationAttribute(4, "onpointerdown", true);
|
||||
}
|
||||
|
||||
builder.OpenComponent(5, componentType);
|
||||
builder.AddAttribute(6, "Control", control);
|
||||
builder.AddAttribute(7, "Model", model);
|
||||
builder.CloseComponent();
|
||||
builder.CloseElement();
|
||||
};
|
||||
}
|
||||
|
||||
private async Task OnPointerDown(PointerEventArgs e, Model model, ExecutableControl control)
|
||||
{
|
||||
if (e.Button == 0 || e.Buttons == 1) await control.OnPointerDown(BlazorDiagram, model, e.ToCore()).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
@if (Model is SvgNodeModel)
|
||||
{
|
||||
<circle r="14" fill="lightgray"></circle>
|
||||
<path d="M 6.79 -1.26 L 2.17 -5.46 C 1.82 -5.81 1.4 -5.46 1.4 -4.9 V -2.8 C -1.89 -2.8 -4.69 -0.77 -6.02 1.96 C -6.51 2.87 -6.79 3.85 -7 4.83 C -7.14 5.53 -6.09 5.88 -5.67 5.25 C -4.13 2.8 -1.54 1.19 1.4 1.19 V 3.5 C 1.4 4.06 1.82 4.41 2.17 4.06 L 6.79 -0.14 C 7.07 -0.42 7.07 -0.98 6.79 -1.26 Z"
|
||||
fill="black">
|
||||
</path>
|
||||
}
|
||||
else
|
||||
{
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" style="overflow: visible;">
|
||||
<circle r="14" fill="lightgray"></circle>
|
||||
<path d="M 6.79 -1.26 L 2.17 -5.46 C 1.82 -5.81 1.4 -5.46 1.4 -4.9 V -2.8 C -1.89 -2.8 -4.69 -0.77 -6.02 1.96 C -6.51 2.87 -6.79 3.85 -7 4.83 C -7.14 5.53 -6.09 5.88 -5.67 5.25 C -4.13 2.8 -1.54 1.19 1.4 1.19 V 3.5 C 1.4 4.06 1.82 4.41 2.17 4.06 L 6.79 -0.14 C 7.07 -0.42 7.07 -0.98 6.79 -1.26 Z"
|
||||
fill="black">
|
||||
</path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter]
|
||||
public DragNewLinkControl Control { get; set; } = null!;
|
||||
|
||||
[Parameter]
|
||||
public Model Model { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
@if (Model is SvgNodeModel)
|
||||
{
|
||||
<circle r="10" fill="red"></circle>
|
||||
<path d="M -5 -5 L 5 5 M -5 5 L 5 -5" stroke="white" stroke-width="2"></path>
|
||||
}
|
||||
else
|
||||
{
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" style="overflow: visible;">
|
||||
<circle r="10" fill="red"></circle>
|
||||
<path d="M -5 -5 L 5 5 M -5 5 L 5 -5" stroke="white" stroke-width="2"></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter]
|
||||
public RemoveControl Control { get; set; } = null!;
|
||||
|
||||
[Parameter]
|
||||
public Model Model { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
@code {
|
||||
|
||||
[Parameter]
|
||||
public GroupModel Group { get; set; } = null!;
|
||||
|
||||
}
|
||||
|
||||
<GroupNodes Group="Group"/>
|
||||
|
||||
@foreach (var port in Group.Ports)
|
||||
{
|
||||
<PortRenderer @key="port" Port="port" Class="group-port"></PortRenderer>
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<div class="blazor-diagram-link-label">
|
||||
@Label.Content
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
[Parameter]
|
||||
public LinkLabelModel Label { get; set; } = null!;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<div class="diagram-canvas @Class"
|
||||
tabindex="-1"
|
||||
@ref="elementReference"
|
||||
@onpointerdown="OnPointerDown"
|
||||
@onpointermove="OnPointerMove"
|
||||
@onpointerup="OnPointerUp"
|
||||
@onkeydown="OnKeyDown"
|
||||
@onwheel="OnWheel"
|
||||
@onpointermove:preventDefault
|
||||
@onwheel:stopPropagation>
|
||||
|
||||
<svg class="diagram-svg-layer" style="@GetLayerStyle(BlazorDiagram.Options.LinksLayerOrder)">
|
||||
|
||||
@foreach (var model in BlazorDiagram.OrderedSelectables)
|
||||
{
|
||||
if (model is SvgNodeModel node && node.Group == null)
|
||||
{
|
||||
<NodeRenderer @key="node" Node="node" />
|
||||
}
|
||||
else if (model is SvgGroupModel group && group.Group == null)
|
||||
{
|
||||
<GroupRenderer @key="group" Group="group" />
|
||||
}
|
||||
else if (model is BaseLinkModel link)
|
||||
{
|
||||
<LinkRenderer @key="link" Link="link" />
|
||||
}
|
||||
}
|
||||
|
||||
<ControlsLayerRenderer Svg="@true"></ControlsLayerRenderer>
|
||||
|
||||
@AdditionalSvg
|
||||
</svg>
|
||||
|
||||
<div class="diagram-html-layer" style="@GetLayerStyle(BlazorDiagram.Options.NodesLayerOrder)">
|
||||
|
||||
@foreach (var model in BlazorDiagram.OrderedSelectables)
|
||||
{
|
||||
if (model is GroupModel group)
|
||||
{
|
||||
if (group.Group == null && group is not SvgGroupModel)
|
||||
{
|
||||
<GroupRenderer @key="group" Group="group" />
|
||||
}
|
||||
}
|
||||
else if (model is NodeModel node && node.Group == null && node is not SvgNodeModel)
|
||||
{
|
||||
<NodeRenderer @key="node" Node="node" />
|
||||
}
|
||||
}
|
||||
|
||||
<ControlsLayerRenderer Svg="@false"></ControlsLayerRenderer>
|
||||
|
||||
@AdditionalHtml
|
||||
</div>
|
||||
|
||||
@Widgets
|
||||
</div>
|
||||
@@ -0,0 +1,111 @@
|
||||
#pragma warning disable CA2007 // 考虑对等待的任务调用 ConfigureAwait
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
using ThingsGateway.Blazor.Diagrams.Extensions;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Components;
|
||||
|
||||
public partial class DiagramCanvas : IDisposable
|
||||
{
|
||||
private DotNetObjectReference<DiagramCanvas>? _reference;
|
||||
private bool _shouldRender;
|
||||
|
||||
protected ElementReference elementReference;
|
||||
|
||||
[CascadingParameter] public BlazorDiagram BlazorDiagram { get; set; } = null!;
|
||||
|
||||
[Parameter] public RenderFragment? Widgets { get; set; }
|
||||
|
||||
[Parameter] public RenderFragment? AdditionalSvg { get; set; }
|
||||
|
||||
[Parameter] public RenderFragment? AdditionalHtml { get; set; }
|
||||
|
||||
[Parameter] public string? Class { get; set; }
|
||||
|
||||
[Inject] public IJSRuntime JSRuntime { get; set; } = null!;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
BlazorDiagram.Changed -= OnDiagramChanged;
|
||||
|
||||
if (_reference == null)
|
||||
return;
|
||||
|
||||
if (elementReference.Id != null)
|
||||
_ = JSRuntime.UnobserveResizes(elementReference);
|
||||
|
||||
_reference.Dispose();
|
||||
}
|
||||
|
||||
private string GetLayerStyle(int order)
|
||||
{
|
||||
return FormattableString.Invariant(
|
||||
$"transform: translate({BlazorDiagram.Pan.X}px, {BlazorDiagram.Pan.Y}px) scale({BlazorDiagram.Zoom}); z-index: {order};");
|
||||
}
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
base.OnInitialized();
|
||||
|
||||
_reference = DotNetObjectReference.Create(this);
|
||||
BlazorDiagram.Changed += OnDiagramChanged;
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
|
||||
if (firstRender)
|
||||
{
|
||||
BlazorDiagram.SetContainer(await JSRuntime.GetBoundingClientRect(elementReference));
|
||||
await JSRuntime.ObserveResizes(elementReference, _reference!);
|
||||
}
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public void OnResize(Rectangle rect)
|
||||
{
|
||||
BlazorDiagram.SetContainer(rect);
|
||||
}
|
||||
|
||||
protected override bool ShouldRender()
|
||||
{
|
||||
if (!_shouldRender) return false;
|
||||
|
||||
_shouldRender = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnPointerDown(PointerEventArgs e)
|
||||
{
|
||||
BlazorDiagram.TriggerPointerDown(null, e.ToCore());
|
||||
}
|
||||
|
||||
private void OnPointerMove(PointerEventArgs e)
|
||||
{
|
||||
BlazorDiagram.TriggerPointerMove(null, e.ToCore());
|
||||
}
|
||||
|
||||
private void OnPointerUp(PointerEventArgs e)
|
||||
{
|
||||
BlazorDiagram.TriggerPointerUp(null, e.ToCore());
|
||||
}
|
||||
|
||||
private void OnKeyDown(KeyboardEventArgs e)
|
||||
{
|
||||
BlazorDiagram.TriggerKeyDown(e.ToCore());
|
||||
}
|
||||
|
||||
private void OnWheel(WheelEventArgs e)
|
||||
{
|
||||
BlazorDiagram.TriggerWheel(e.ToCore());
|
||||
}
|
||||
|
||||
private void OnDiagramChanged()
|
||||
{
|
||||
_shouldRender = true;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Events;
|
||||
|
||||
using MouseEventArgs = Microsoft.AspNetCore.Components.Web.MouseEventArgs;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Extensions;
|
||||
|
||||
public static class EventsExtensions
|
||||
{
|
||||
public static PointerEventArgs ToCore(this Microsoft.AspNetCore.Components.Web.PointerEventArgs e)
|
||||
{
|
||||
return new PointerEventArgs(e.ClientX, e.ClientY, e.Button, e.Buttons, e.CtrlKey, e.ShiftKey, e.AltKey,
|
||||
e.PointerId, e.Width, e.Height, e.Pressure, e.TiltX, e.TiltY, e.PointerType, e.IsPrimary);
|
||||
}
|
||||
|
||||
public static PointerEventArgs ToCore(this MouseEventArgs e)
|
||||
{
|
||||
return new PointerEventArgs(e.ClientX, e.ClientY, e.Button, e.Buttons, e.CtrlKey, e.ShiftKey, e.AltKey,
|
||||
0, 0, 0, 0, 0, 0, string.Empty, false);
|
||||
}
|
||||
|
||||
public static KeyboardEventArgs ToCore(this Microsoft.AspNetCore.Components.Web.KeyboardEventArgs e)
|
||||
{
|
||||
return new KeyboardEventArgs(e.Key, e.Code, e.Location, e.CtrlKey, e.ShiftKey, e.AltKey);
|
||||
}
|
||||
|
||||
public static WheelEventArgs ToCore(this Microsoft.AspNetCore.Components.Web.WheelEventArgs e)
|
||||
{
|
||||
return new WheelEventArgs(e.ClientX, e.ClientY, e.Button, e.Buttons, e.CtrlKey, e.ShiftKey, e.AltKey, e.DeltaX,
|
||||
e.DeltaY, e.DeltaZ, e.DeltaMode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Extensions;
|
||||
|
||||
public static class JSRuntimeExtensions
|
||||
{
|
||||
public static async Task<Rectangle> GetBoundingClientRect(this IJSRuntime jsRuntime, ElementReference element)
|
||||
{
|
||||
return await jsRuntime.InvokeAsync<Rectangle>("BlazorDiagrams.getBoundingClientRect", element).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static async Task ObserveResizes<T>(this IJSRuntime jsRuntime, ElementReference element,
|
||||
DotNetObjectReference<T> reference) where T : class
|
||||
{
|
||||
try
|
||||
{
|
||||
await jsRuntime.InvokeVoidAsync("BlazorDiagrams.observe", element, reference, element.Id).ConfigureAwait(false);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Ignore, DotNetObjectReference was likely disposed
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task UnobserveResizes(this IJSRuntime jsRuntime, ElementReference element)
|
||||
{
|
||||
await jsRuntime.InvokeVoidAsync("BlazorDiagrams.unobserve", element, element.Id).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
using ThingsGateway.Blazor.Diagrams.Models;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Extensions;
|
||||
|
||||
public static class ModelExtensions
|
||||
{
|
||||
public static bool IsSvg(this Model model)
|
||||
{
|
||||
return model is SvgNodeModel or SvgGroupModel or BaseLinkModel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Text;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Extensions;
|
||||
|
||||
public static class StringBuilderExtensions
|
||||
{
|
||||
public static StringBuilder AppendIf(this StringBuilder builder, string str, bool condition)
|
||||
{
|
||||
if (condition) builder.Append(str);
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
@code {
|
||||
|
||||
[Parameter]
|
||||
public GroupModel Group { get; set; } = null!;
|
||||
|
||||
// Remove duplicated code
|
||||
}
|
||||
|
||||
@if (Group is SvgGroupModel)
|
||||
{
|
||||
<g class="children" transform="translate(@((-Group.Position.X).ToInvariantString()) @((-Group.Position.Y).ToInvariantString()))">
|
||||
@foreach (var node in Group.Children)
|
||||
{
|
||||
if (node is GroupModel g)
|
||||
{
|
||||
<GroupRenderer @key="g.Id" Group="g"></GroupRenderer>
|
||||
}
|
||||
else
|
||||
{
|
||||
<NodeRenderer @key="node.Id" Node="node"></NodeRenderer>
|
||||
}
|
||||
}
|
||||
</g>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="children" style="top: @((-Group.Position.Y).ToInvariantString())px; left: @((-Group.Position.X).ToInvariantString())px">
|
||||
@foreach (var node in Group.Children)
|
||||
{
|
||||
if (node is GroupModel g)
|
||||
{
|
||||
<GroupRenderer @key="g.Id" Group="g"></GroupRenderer>
|
||||
}
|
||||
else
|
||||
{
|
||||
<NodeRenderer @key="node.Id" Node="node"></NodeRenderer>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
@using ThingsGateway.Blazor.Diagrams.Core.Behaviors;
|
||||
|
||||
@{
|
||||
var color = Link.Selected ? Link.SelectedColor ?? BlazorDiagram.Options.Links.DefaultSelectedColor : Link.Color ?? BlazorDiagram.Options.Links.DefaultColor;
|
||||
var result = Link.PathGeneratorResult;
|
||||
if (result == null)
|
||||
return;
|
||||
|
||||
var dnlb = BlazorDiagram.GetBehavior<DragNewLinkBehavior>();
|
||||
var d = result.FullPath.ToString();
|
||||
}
|
||||
|
||||
<path d="@d"
|
||||
stroke-width="@Link.Width.ToInvariantString()"
|
||||
fill="none"
|
||||
stroke="@color" />
|
||||
|
||||
@if (dnlb!.OngoingLink == null || dnlb.OngoingLink != Link)
|
||||
{
|
||||
@if (Link.Vertices.Count == 0)
|
||||
{
|
||||
@GetSelectionHelperPath(color, d, 0)
|
||||
}
|
||||
else
|
||||
{
|
||||
@for (var i = 0; i < result.Paths.Length; i++)
|
||||
{
|
||||
d = result.Paths[i].ToString();
|
||||
var index = i;
|
||||
@GetSelectionHelperPath(color, d, index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@if (Link.SourceMarker != null && result.SourceMarkerAngle != null && result.SourceMarkerPosition != null)
|
||||
{
|
||||
<g transform="@(FormattableString.Invariant($"translate({result.SourceMarkerPosition.X}, {result.SourceMarkerPosition.Y}) rotate({result.SourceMarkerAngle})"))">
|
||||
<path d="@Link.SourceMarker.Path" fill="@color"></path>
|
||||
</g>
|
||||
}
|
||||
|
||||
@if (Link.TargetMarker != null && result.TargetMarkerAngle != null && result.TargetMarkerPosition != null)
|
||||
{
|
||||
<g transform="@(FormattableString.Invariant($"translate({result.TargetMarkerPosition.X}, {result.TargetMarkerPosition.Y}) rotate({result.TargetMarkerAngle})"))">
|
||||
<path d="@Link.TargetMarker.Path" fill="@color"></path>
|
||||
</g>
|
||||
}
|
||||
|
||||
@if (Link.Vertices.Count > 0)
|
||||
{
|
||||
var selectedColor = Link.SelectedColor ?? BlazorDiagram.Options.Links.DefaultSelectedColor;
|
||||
var normalColor = Link.Color ?? BlazorDiagram.Options.Links.DefaultColor;
|
||||
@foreach (var vertex in Link.Vertices)
|
||||
{
|
||||
<LinkVertexRenderer @key="vertex.Id"
|
||||
Vertex="vertex"
|
||||
Color="@normalColor"
|
||||
SelectedColor="@selectedColor" />
|
||||
}
|
||||
}
|
||||
|
||||
@foreach (var label in Link.Labels)
|
||||
{
|
||||
<LinkLabelRenderer @key="label.Id" Label="@label" Path="@result.FullPath" />
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models;
|
||||
using ThingsGateway.Blazor.Diagrams.Extensions;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Components;
|
||||
|
||||
public partial class LinkWidget
|
||||
{
|
||||
private bool _hovered;
|
||||
|
||||
[CascadingParameter] public BlazorDiagram BlazorDiagram { get; set; } = null!;
|
||||
[Parameter] public LinkModel Link { get; set; } = null!;
|
||||
|
||||
private RenderFragment GetSelectionHelperPath(string color, string d, int index)
|
||||
{
|
||||
return builder =>
|
||||
{
|
||||
builder.OpenElement(0, "path");
|
||||
builder.AddAttribute(1, "class", "selection-helper");
|
||||
builder.AddAttribute(2, "stroke", color);
|
||||
builder.AddAttribute(3, "stroke-width", 12);
|
||||
builder.AddAttribute(4, "d", d);
|
||||
builder.AddAttribute(5, "stroke-linecap", "butt");
|
||||
builder.AddAttribute(6, "stroke-opacity", _hovered ? "0.05" : "0");
|
||||
builder.AddAttribute(7, "fill", "none");
|
||||
builder.AddAttribute(8, "onmouseenter", EventCallback.Factory.Create<MouseEventArgs>(this, OnMouseEnter));
|
||||
builder.AddAttribute(9, "onmouseleave", EventCallback.Factory.Create<MouseEventArgs>(this, OnMouseLeave));
|
||||
builder.AddAttribute(10, "onpointerdown", EventCallback.Factory.Create<Microsoft.AspNetCore.Components.Web.PointerEventArgs>(this, e => OnPointerDown(e, index)));
|
||||
builder.AddEventStopPropagationAttribute(11, "onpointerdown", Link.Segmentable);
|
||||
builder.CloseElement();
|
||||
};
|
||||
}
|
||||
|
||||
private void OnPointerDown(PointerEventArgs e, int index)
|
||||
{
|
||||
if (!Link.Segmentable)
|
||||
return;
|
||||
|
||||
var vertex = CreateVertex(e.ClientX, e.ClientY, index);
|
||||
BlazorDiagram.TriggerPointerDown(vertex, e.ToCore());
|
||||
}
|
||||
|
||||
private void OnMouseEnter(MouseEventArgs e)
|
||||
{
|
||||
_hovered = true;
|
||||
}
|
||||
|
||||
private void OnMouseLeave(MouseEventArgs e)
|
||||
{
|
||||
_hovered = false;
|
||||
}
|
||||
|
||||
private LinkVertexModel CreateVertex(double clientX, double clientY, int index)
|
||||
{
|
||||
var rPt = BlazorDiagram.GetRelativeMousePoint(clientX, clientY);
|
||||
var vertex = new LinkVertexModel(Link, rPt);
|
||||
Link.Vertices.Insert(index, vertex);
|
||||
Link.Refresh();
|
||||
return vertex;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Models;
|
||||
|
||||
public class SvgGroupModel : GroupModel
|
||||
{
|
||||
public SvgGroupModel(IEnumerable<NodeModel> children, byte padding = 30, bool autoSize = true) : base(children, padding, autoSize)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Models;
|
||||
|
||||
public class SvgNodeModel : NodeModel
|
||||
{
|
||||
public SvgNodeModel(Point? position = null) : base(position)
|
||||
{
|
||||
}
|
||||
|
||||
public SvgNodeModel(string id, Point? position = null) : base(id, position)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
|
||||
<div class="blazor-diagram-node @(Node.Group != null ? "grouped" : "") @(Node.Selected ? "selected" : "")">
|
||||
@(Node.Title ?? "Title")
|
||||
@foreach (var port in Node.Ports)
|
||||
{
|
||||
<PortRenderer @key="port" Port="port" Class="blazor-diagram"></PortRenderer>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
[Parameter]
|
||||
public NodeModel Node { get; set; } = null!;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Options;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Options;
|
||||
|
||||
public class BlazorDiagramConstraintsOptions : DiagramConstraintsOptions
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Options;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Options;
|
||||
|
||||
public class BlazorDiagramGroupOptions : DiagramGroupOptions
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Options;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Options;
|
||||
|
||||
public class BlazorDiagramLinkOptions : DiagramLinkOptions
|
||||
{
|
||||
public string DefaultColor { get; set; } = "black";
|
||||
public string DefaultSelectedColor { get; set; } = "rgb(110, 159, 212)";
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Options;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Options;
|
||||
|
||||
public class BlazorDiagramOptions : DiagramOptions
|
||||
{
|
||||
public int LinksLayerOrder { get; set; } = 0;
|
||||
public int NodesLayerOrder { get; set; } = 0;
|
||||
|
||||
public override BlazorDiagramZoomOptions Zoom { get; } = new();
|
||||
public override BlazorDiagramLinkOptions Links { get; } = new();
|
||||
public override BlazorDiagramGroupOptions Groups { get; } = new();
|
||||
public override BlazorDiagramConstraintsOptions Constraints { get; } = new();
|
||||
public override BlazorDiagramVirtualizationOptions Virtualization { get; } = new();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Options;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Options;
|
||||
|
||||
public class BlazorDiagramVirtualizationOptions : DiagramVirtualizationOptions
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Options;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Options;
|
||||
|
||||
public class BlazorDiagramZoomOptions : DiagramZoomOptions
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
|
||||
using System.Text;
|
||||
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
using ThingsGateway.Blazor.Diagrams.Extensions;
|
||||
using ThingsGateway.Blazor.Diagrams.Models;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Components.Renderers;
|
||||
|
||||
public class GroupRenderer : ComponentBase, IDisposable
|
||||
{
|
||||
private bool _isSvg;
|
||||
private Size? _lastSize;
|
||||
private bool _shouldRender = true;
|
||||
|
||||
[CascadingParameter] public BlazorDiagram BlazorDiagram { get; set; } = null!;
|
||||
|
||||
[Parameter] public GroupModel Group { get; set; } = null!;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Group.Changed -= OnGroupChanged;
|
||||
Group.VisibilityChanged -= OnGroupChanged;
|
||||
}
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Group.Changed += OnGroupChanged;
|
||||
Group.VisibilityChanged += OnGroupChanged;
|
||||
}
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
base.OnParametersSet();
|
||||
|
||||
_isSvg = Group is SvgGroupModel;
|
||||
}
|
||||
|
||||
protected override bool ShouldRender()
|
||||
{
|
||||
if (_shouldRender)
|
||||
{
|
||||
_shouldRender = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void OnAfterRender(bool firstRender)
|
||||
{
|
||||
if (Size.Zero.Equals(Group.Size))
|
||||
return;
|
||||
|
||||
// Update the port positions (and links) when the size of the group changes
|
||||
// This will save us some JS trips as well as useless rerenders
|
||||
|
||||
if (_lastSize == null || !_lastSize.Equals(Group.Size))
|
||||
{
|
||||
Group.ReinitializePorts();
|
||||
Group.RefreshLinks();
|
||||
_lastSize = Group.Size;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGroupChanged(Model _)
|
||||
{
|
||||
_shouldRender = true;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private static string GenerateStyle(double top, double left, double width, double height)
|
||||
{
|
||||
return FormattableString.Invariant($"top: {top}px; left: {left}px; width: {width}px; height: {height}px");
|
||||
}
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
if (!Group.Visible)
|
||||
return;
|
||||
|
||||
var componentType = BlazorDiagram.GetComponent(Group) ?? typeof(DefaultGroupWidget);
|
||||
var classes = new StringBuilder("diagram-group")
|
||||
.AppendIf(" locked", Group.Locked)
|
||||
.AppendIf(" selected", Group.Selected)
|
||||
.AppendIf(" default", componentType == typeof(DefaultGroupWidget));
|
||||
|
||||
builder.OpenElement(0, _isSvg ? "g" : "div");
|
||||
builder.AddAttribute(1, "class", classes.ToString());
|
||||
builder.AddAttribute(2, "data-group-id", Group.Id);
|
||||
|
||||
if (_isSvg)
|
||||
builder.AddAttribute(3, "transform",
|
||||
FormattableString.Invariant($"translate({Group.Position.X} {Group.Position.Y})"));
|
||||
else
|
||||
builder.AddAttribute(3, "style",
|
||||
GenerateStyle(Group.Position.Y, Group.Position.X, Group.Size!.Width, Group.Size.Height));
|
||||
|
||||
builder.AddAttribute(4, "onpointerdown", EventCallback.Factory.Create<PointerEventArgs>(this, OnPointerDown));
|
||||
builder.AddEventStopPropagationAttribute(5, "onpointerdown", true);
|
||||
builder.AddAttribute(6, "onpointerup", EventCallback.Factory.Create<PointerEventArgs>(this, OnPointerUp));
|
||||
builder.AddEventStopPropagationAttribute(7, "onpointerup", true);
|
||||
builder.AddAttribute(8, "onmouseenter", EventCallback.Factory.Create<MouseEventArgs>(this, OnMouseEnter));
|
||||
builder.AddAttribute(9, "onmouseleave", EventCallback.Factory.Create<MouseEventArgs>(this, OnMouseLeave));
|
||||
|
||||
if (_isSvg)
|
||||
{
|
||||
builder.OpenElement(10, "rect");
|
||||
builder.AddAttribute(11, "width", Group.Size!.Width);
|
||||
builder.AddAttribute(12, "height", Group.Size.Height);
|
||||
builder.AddAttribute(13, "fill", "none");
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
builder.OpenComponent(14, componentType);
|
||||
builder.AddAttribute(15, "Group", Group);
|
||||
builder.CloseComponent();
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
private void OnPointerDown(PointerEventArgs e)
|
||||
{
|
||||
BlazorDiagram.TriggerPointerDown(Group, e.ToCore());
|
||||
}
|
||||
|
||||
private void OnPointerUp(PointerEventArgs e)
|
||||
{
|
||||
BlazorDiagram.TriggerPointerUp(Group, e.ToCore());
|
||||
}
|
||||
|
||||
private void OnMouseEnter(MouseEventArgs e)
|
||||
{
|
||||
BlazorDiagram.TriggerPointerEnter(Group, e.ToCore());
|
||||
}
|
||||
|
||||
private void OnMouseLeave(MouseEventArgs e)
|
||||
{
|
||||
BlazorDiagram.TriggerPointerLeave(Group, e.ToCore());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
|
||||
using SvgPathProperties;
|
||||
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Extensions;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Components.Renderers;
|
||||
|
||||
public class LinkLabelRenderer : ComponentBase, IDisposable
|
||||
{
|
||||
[CascadingParameter] public BlazorDiagram BlazorDiagram { get; set; } = null!;
|
||||
[Parameter] public LinkLabelModel Label { get; set; } = null!;
|
||||
[Parameter] public SvgPath Path { get; set; } = null!;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Label.Changed -= OnLabelChanged;
|
||||
Label.VisibilityChanged -= OnLabelChanged;
|
||||
}
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Label.Changed += OnLabelChanged;
|
||||
Label.VisibilityChanged += OnLabelChanged;
|
||||
}
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
if (!Label.Visible)
|
||||
return;
|
||||
|
||||
var position = FindPosition();
|
||||
if (position == null)
|
||||
return;
|
||||
|
||||
var componentType = BlazorDiagram.GetComponent(Label) ?? typeof(DefaultLinkLabelWidget);
|
||||
|
||||
builder.OpenElement(0, "foreignObject");
|
||||
builder.AddAttribute(1, "class", "diagram-link-label");
|
||||
builder.AddAttribute(2, "x", (position.X + (Label.Offset?.X ?? 0)).ToInvariantString());
|
||||
builder.AddAttribute(3, "y", (position.Y + (Label.Offset?.Y ?? 0)).ToInvariantString());
|
||||
|
||||
builder.OpenComponent(4, componentType);
|
||||
builder.AddAttribute(5, "Label", Label);
|
||||
builder.CloseComponent();
|
||||
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
private void OnLabelChanged(Model _)
|
||||
{
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private Point? FindPosition()
|
||||
{
|
||||
var totalLength = Path.Length;
|
||||
var length = Label.Distance switch
|
||||
{
|
||||
<= 1 and >= 0 => Label.Distance.Value * totalLength,
|
||||
> 1 => Label.Distance.Value,
|
||||
< 0 => totalLength + Label.Distance.Value,
|
||||
_ => totalLength * (Label.Parent.Labels.IndexOf(Label) + 1) / (Label.Parent.Labels.Count + 1)
|
||||
};
|
||||
|
||||
var pt = Path.GetPointAtLength(length);
|
||||
return new Point(pt.X, pt.Y);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
|
||||
using System.Text;
|
||||
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
using ThingsGateway.Blazor.Diagrams.Extensions;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Components.Renderers;
|
||||
|
||||
public class LinkRenderer : ComponentBase, IDisposable
|
||||
{
|
||||
private bool _shouldRender = true;
|
||||
|
||||
[CascadingParameter] public BlazorDiagram BlazorDiagram { get; set; } = null!;
|
||||
|
||||
[Parameter] public BaseLinkModel Link { get; set; } = null!;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Link.Changed -= OnLinkChanged;
|
||||
Link.VisibilityChanged -= OnLinkChanged;
|
||||
}
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
base.OnInitialized();
|
||||
|
||||
Link.Changed += OnLinkChanged;
|
||||
Link.VisibilityChanged += OnLinkChanged;
|
||||
}
|
||||
|
||||
protected override bool ShouldRender()
|
||||
{
|
||||
if (!_shouldRender)
|
||||
return false;
|
||||
|
||||
_shouldRender = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
if (!Link.Visible)
|
||||
return;
|
||||
|
||||
var componentType = BlazorDiagram.GetComponent(Link) ?? typeof(LinkWidget);
|
||||
var classes = new StringBuilder()
|
||||
.Append("diagram-link")
|
||||
.AppendIf(" attached", Link.IsAttached)
|
||||
.ToString();
|
||||
|
||||
builder.OpenElement(0, "g");
|
||||
builder.AddAttribute(1, "class", classes);
|
||||
builder.AddAttribute(2, "data-link-id", Link.Id);
|
||||
builder.AddAttribute(3, "onpointerdown", EventCallback.Factory.Create<PointerEventArgs>(this, OnPointerDown));
|
||||
builder.AddEventStopPropagationAttribute(4, "onpointerdown", true);
|
||||
builder.AddAttribute(5, "onpointerup", EventCallback.Factory.Create<PointerEventArgs>(this, OnPointerUp));
|
||||
builder.AddEventStopPropagationAttribute(6, "onpointerup", true);
|
||||
builder.AddAttribute(7, "onmouseenter", EventCallback.Factory.Create<MouseEventArgs>(this, OnMouseEnter));
|
||||
builder.AddAttribute(8, "onmouseleave", EventCallback.Factory.Create<MouseEventArgs>(this, OnMouseLeave));
|
||||
builder.OpenComponent(9, componentType);
|
||||
builder.AddAttribute(10, "Link", Link);
|
||||
builder.CloseComponent();
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
private void OnLinkChanged(Model _)
|
||||
{
|
||||
_shouldRender = true;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private void OnPointerDown(PointerEventArgs e)
|
||||
{
|
||||
BlazorDiagram.TriggerPointerDown(Link, e.ToCore());
|
||||
}
|
||||
|
||||
private void OnPointerUp(PointerEventArgs e)
|
||||
{
|
||||
BlazorDiagram.TriggerPointerUp(Link, e.ToCore());
|
||||
}
|
||||
|
||||
private void OnMouseEnter(MouseEventArgs e)
|
||||
{
|
||||
BlazorDiagram.TriggerPointerEnter(Link, e.ToCore());
|
||||
}
|
||||
|
||||
private void OnMouseLeave(MouseEventArgs e)
|
||||
{
|
||||
BlazorDiagram.TriggerPointerLeave(Link, e.ToCore());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Extensions;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
using ThingsGateway.Blazor.Diagrams.Extensions;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Components.Renderers;
|
||||
|
||||
public class LinkVertexRenderer : ComponentBase, IDisposable
|
||||
{
|
||||
private bool _shouldRender = true;
|
||||
|
||||
[CascadingParameter] public BlazorDiagram BlazorDiagram { get; set; } = null!;
|
||||
[Parameter] public LinkVertexModel Vertex { get; set; } = null!;
|
||||
[Parameter] public string? Color { get; set; }
|
||||
[Parameter] public string? SelectedColor { get; set; }
|
||||
|
||||
private string? ColorToUse => Vertex.Selected ? SelectedColor : Color;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Vertex.Changed -= OnVertexChanged;
|
||||
}
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Vertex.Changed += OnVertexChanged;
|
||||
}
|
||||
|
||||
protected override bool ShouldRender()
|
||||
{
|
||||
if (!_shouldRender) return false;
|
||||
|
||||
_shouldRender = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
var componentType = BlazorDiagram.GetComponent(Vertex);
|
||||
|
||||
builder.OpenElement(0, "g");
|
||||
builder.AddAttribute(1, "class", "diagram-link-vertex");
|
||||
builder.AddAttribute(4, "cursor", "move");
|
||||
builder.AddAttribute(5, "ondblclick", value: EventCallback.Factory.Create<MouseEventArgs>(this, OnDoubleClick));
|
||||
builder.AddAttribute(6, "onpointerdown", EventCallback.Factory.Create<PointerEventArgs>(this, OnPointerDown));
|
||||
builder.AddAttribute(7, "onpointerup", EventCallback.Factory.Create<PointerEventArgs>(this, OnPointerUp));
|
||||
builder.AddEventStopPropagationAttribute(8, "onpointerdown", true);
|
||||
builder.AddEventStopPropagationAttribute(9, "onpointerup", true);
|
||||
|
||||
if (componentType == null)
|
||||
{
|
||||
builder.OpenElement(10, "circle");
|
||||
builder.AddAttribute(11, "cx", Vertex.Position.X.ToInvariantString());
|
||||
builder.AddAttribute(12, "cy", Vertex.Position.Y.ToInvariantString());
|
||||
builder.AddAttribute(13, "r", "5");
|
||||
builder.AddAttribute(14, "fill", ColorToUse);
|
||||
builder.CloseElement();
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.OpenComponent(15, componentType);
|
||||
builder.AddAttribute(16, "Vertex", Vertex);
|
||||
builder.AddAttribute(17, "Color", ColorToUse);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
private void OnVertexChanged(Model _)
|
||||
{
|
||||
_shouldRender = true;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private void OnPointerDown(PointerEventArgs e)
|
||||
{
|
||||
BlazorDiagram.TriggerPointerDown(Vertex, e.ToCore());
|
||||
}
|
||||
|
||||
private void OnPointerUp(PointerEventArgs e)
|
||||
{
|
||||
BlazorDiagram.TriggerPointerUp(Vertex, e.ToCore());
|
||||
}
|
||||
|
||||
private void OnDoubleClick(MouseEventArgs e)
|
||||
{
|
||||
Vertex.Parent.Vertices.Remove(Vertex);
|
||||
Vertex.Parent.Refresh();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
using System.Text;
|
||||
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Extensions;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
using ThingsGateway.Blazor.Diagrams.Extensions;
|
||||
using ThingsGateway.Blazor.Diagrams.Models;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Components.Renderers;
|
||||
|
||||
public class NodeRenderer : ComponentBase, IDisposable
|
||||
{
|
||||
private bool _becameVisible;
|
||||
private ElementReference _element;
|
||||
private bool _isSvg;
|
||||
private DotNetObjectReference<NodeRenderer>? _reference;
|
||||
private bool _shouldRender;
|
||||
|
||||
[CascadingParameter] public BlazorDiagram BlazorDiagram { get; set; } = null!;
|
||||
|
||||
[Parameter] public NodeModel Node { get; set; } = null!;
|
||||
|
||||
[Inject] private IJSRuntime JsRuntime { get; set; } = null!;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Node.Changed -= OnNodeChanged;
|
||||
Node.VisibilityChanged -= OnVisibilityChanged;
|
||||
|
||||
if (_element.Id != null && !Node.ControlledSize)
|
||||
{
|
||||
_ = JsRuntime.UnobserveResizes(_element);
|
||||
}
|
||||
|
||||
_reference?.Dispose();
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public void OnResize(Size size)
|
||||
{
|
||||
// When the node becomes invisible (a.k.a unrendered), the size is zero
|
||||
if (Size.Zero.Equals(size))
|
||||
return;
|
||||
|
||||
size = new Size(size.Width / BlazorDiagram.Zoom, size.Height / BlazorDiagram.Zoom);
|
||||
if (Node.Size != null && Node.Size.Width.AlmostEqualTo(size.Width) &&
|
||||
Node.Size.Height.AlmostEqualTo(size.Height))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Node.Size = size;
|
||||
Node.Refresh();
|
||||
Node.RefreshLinks();
|
||||
Node.ReinitializePorts();
|
||||
}
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
base.OnInitialized();
|
||||
|
||||
_reference = DotNetObjectReference.Create(this);
|
||||
Node.Changed += OnNodeChanged;
|
||||
Node.VisibilityChanged += OnVisibilityChanged;
|
||||
}
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
base.OnParametersSet();
|
||||
|
||||
_isSvg = Node is SvgNodeModel;
|
||||
}
|
||||
|
||||
protected override bool ShouldRender()
|
||||
{
|
||||
if (!_shouldRender)
|
||||
return false;
|
||||
|
||||
_shouldRender = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
if (!Node.Visible)
|
||||
return;
|
||||
|
||||
var componentType = BlazorDiagram.GetComponent(Node) ??
|
||||
(_isSvg ? typeof(SvgNodeWidget) : typeof(NodeWidget));
|
||||
var classes = new StringBuilder("diagram-node")
|
||||
.AppendIf(" locked", Node.Locked)
|
||||
.AppendIf(" selected", Node.Selected)
|
||||
.AppendIf(" grouped", Node.Group != null);
|
||||
|
||||
builder.OpenElement(0, _isSvg ? "g" : "div");
|
||||
builder.AddAttribute(1, "class", classes.ToString());
|
||||
builder.AddAttribute(2, "data-node-id", Node.Id);
|
||||
|
||||
if (_isSvg)
|
||||
{
|
||||
builder.AddAttribute(3, "transform",
|
||||
$"translate({Node.Position.X.ToInvariantString()} {Node.Position.Y.ToInvariantString()})");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.AddAttribute(3, "style",
|
||||
$"top: {Node.Position.Y.ToInvariantString()}px; left: {Node.Position.X.ToInvariantString()}px");
|
||||
}
|
||||
|
||||
builder.AddAttribute(4, "onpointerdown", EventCallback.Factory.Create<PointerEventArgs>(this, OnPointerDown));
|
||||
builder.AddEventStopPropagationAttribute(5, "onpointerdown", true);
|
||||
builder.AddAttribute(6, "onpointerup", EventCallback.Factory.Create<PointerEventArgs>(this, OnPointerUp));
|
||||
builder.AddEventStopPropagationAttribute(7, "onpointerup", true);
|
||||
builder.AddAttribute(8, "onmouseenter", EventCallback.Factory.Create<MouseEventArgs>(this, OnMouseEnter));
|
||||
builder.AddAttribute(9, "onmouseleave", EventCallback.Factory.Create<MouseEventArgs>(this, OnMouseLeave));
|
||||
builder.AddElementReferenceCapture(10, value => _element = value);
|
||||
builder.OpenComponent(11, componentType);
|
||||
builder.AddAttribute(12, "Node", Node);
|
||||
builder.CloseComponent();
|
||||
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender && !Node.Visible)
|
||||
return;
|
||||
|
||||
|
||||
if (firstRender || _becameVisible)
|
||||
{
|
||||
_becameVisible = false;
|
||||
|
||||
if (!Node.ControlledSize)
|
||||
{
|
||||
await JsRuntime.ObserveResizes(_element, _reference!).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnNodeChanged(Model _)
|
||||
{
|
||||
ReRender();
|
||||
}
|
||||
|
||||
private void OnVisibilityChanged(Model _)
|
||||
{
|
||||
_becameVisible = Node.Visible;
|
||||
ReRender();
|
||||
}
|
||||
|
||||
private void ReRender()
|
||||
{
|
||||
_shouldRender = true;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private void OnPointerDown(PointerEventArgs e)
|
||||
{
|
||||
BlazorDiagram.TriggerPointerDown(Node, e.ToCore());
|
||||
}
|
||||
|
||||
private void OnPointerUp(PointerEventArgs e)
|
||||
{
|
||||
BlazorDiagram.TriggerPointerUp(Node, e.ToCore());
|
||||
}
|
||||
|
||||
private void OnMouseEnter(MouseEventArgs e)
|
||||
{
|
||||
BlazorDiagram.TriggerPointerEnter(Node, e.ToCore());
|
||||
}
|
||||
|
||||
private void OnMouseLeave(MouseEventArgs e)
|
||||
{
|
||||
BlazorDiagram.TriggerPointerLeave(Node, e.ToCore());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
using ThingsGateway.Blazor.Diagrams.Extensions;
|
||||
using ThingsGateway.Blazor.Diagrams.Models;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Components.Renderers;
|
||||
|
||||
public class PortRenderer : ComponentBase, IDisposable
|
||||
{
|
||||
private ElementReference _element;
|
||||
private bool _isParentSvg;
|
||||
private bool _shouldRefreshPort;
|
||||
private bool _shouldRender = true;
|
||||
private bool _updatingDimensions;
|
||||
|
||||
[CascadingParameter] public BlazorDiagram BlazorDiagram { get; set; } = null!;
|
||||
[Inject] private IJSRuntime JSRuntime { get; set; } = null!;
|
||||
[Parameter] public PortModel Port { get; set; } = null!;
|
||||
[Parameter] public string? Class { get; set; }
|
||||
[Parameter] public string? Style { get; set; }
|
||||
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Port.Changed -= OnPortChanged;
|
||||
Port.VisibilityChanged -= OnPortChanged;
|
||||
}
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
base.OnInitialized();
|
||||
|
||||
Port.Changed += OnPortChanged;
|
||||
Port.VisibilityChanged += OnPortChanged;
|
||||
}
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
base.OnParametersSet();
|
||||
|
||||
_isParentSvg = Port.Parent is SvgNodeModel;
|
||||
}
|
||||
|
||||
protected override bool ShouldRender()
|
||||
{
|
||||
if (!_shouldRender)
|
||||
return false;
|
||||
|
||||
_shouldRender = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
if (!Port.Visible)
|
||||
return;
|
||||
|
||||
builder.OpenElement(0, _isParentSvg ? "g" : "div");
|
||||
builder.AddAttribute(1, "style", Style);
|
||||
builder.AddAttribute(2, "class",
|
||||
"diagram-port" + " " + Port.Alignment.ToString().ToLower() + " " + (Port.Links.Count > 0 ? "has-links" : "") + " " +
|
||||
Class);
|
||||
builder.AddAttribute(3, "data-port-id", Port.Id);
|
||||
builder.AddAttribute(4, "onpointerdown", EventCallback.Factory.Create<PointerEventArgs>(this, OnPointerDown));
|
||||
builder.AddEventStopPropagationAttribute(5, "onpointerdown", true);
|
||||
builder.AddAttribute(6, "onpointerup", EventCallback.Factory.Create<PointerEventArgs>(this, OnPointerUp));
|
||||
builder.AddEventStopPropagationAttribute(7, "onpointerup", true);
|
||||
builder.AddElementReferenceCapture(8, __value => { _element = __value; });
|
||||
builder.AddContent(9, ChildContent);
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!Port.Initialized)
|
||||
{
|
||||
await UpdateDimensions().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPointerDown(PointerEventArgs e)
|
||||
{
|
||||
BlazorDiagram.TriggerPointerDown(Port, e.ToCore());
|
||||
}
|
||||
|
||||
private void OnPointerUp(PointerEventArgs e)
|
||||
{
|
||||
var model = e.PointerType == "mouse" ? Port : FindPortOn(e.ClientX, e.ClientY);
|
||||
BlazorDiagram.TriggerPointerUp(model, e.ToCore());
|
||||
}
|
||||
|
||||
private PortModel? FindPortOn(double clientX, double clientY)
|
||||
{
|
||||
var allPorts = BlazorDiagram.Nodes.SelectMany(n => n.Ports)
|
||||
.Union(BlazorDiagram.Groups.SelectMany(g => g.Ports));
|
||||
|
||||
foreach (var port in allPorts)
|
||||
{
|
||||
if (!port.Initialized)
|
||||
continue;
|
||||
|
||||
var relativePt = BlazorDiagram.GetRelativeMousePoint(clientX, clientY);
|
||||
if (port.GetBounds().ContainsPoint(relativePt))
|
||||
return port;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task UpdateDimensions()
|
||||
{
|
||||
if (BlazorDiagram.Container == null)
|
||||
return;
|
||||
|
||||
_updatingDimensions = true;
|
||||
var zoom = BlazorDiagram.Zoom;
|
||||
var pan = BlazorDiagram.Pan;
|
||||
var rect = await JSRuntime.GetBoundingClientRect(_element).ConfigureAwait(false);
|
||||
|
||||
Port.Size = new Size(rect.Width / zoom, rect.Height / zoom);
|
||||
Port.Position = new Point((rect.Left - BlazorDiagram.Container.Left - pan.X) / zoom,
|
||||
(rect.Top - BlazorDiagram.Container.Top - pan.Y) / zoom);
|
||||
|
||||
Port.Initialized = true;
|
||||
_updatingDimensions = false;
|
||||
|
||||
if (_shouldRefreshPort)
|
||||
{
|
||||
_shouldRefreshPort = false;
|
||||
Port.RefreshAll();
|
||||
}
|
||||
else
|
||||
{
|
||||
Port.RefreshLinks();
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnPortChanged(Model _)
|
||||
{
|
||||
// If an update is ongoing and the port is refreshed again,
|
||||
// it's highly likely the port needs to be refreshed (e.g. link added)
|
||||
if (_updatingDimensions) _shouldRefreshPort = true;
|
||||
|
||||
if (Port.Initialized)
|
||||
{
|
||||
_shouldRender = true;
|
||||
await InvokeAsync(StateHasChanged).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await UpdateDimensions().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<rect width="50" height="50" style="fill:rgb(0,0,255);stroke-width:3;stroke:rgb(0,0,0)"/>
|
||||
|
||||
@code {
|
||||
|
||||
[Parameter]
|
||||
public NodeModel Node { get; set; } = null!;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
@if (_visible)
|
||||
{
|
||||
<div class="grid" style="width: 100%; height: 100%; background-color: @BackgroundColor; @GenerateStyle()"></div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="grid"></div>
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using System.Text;
|
||||
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Extensions;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Components.Widgets;
|
||||
|
||||
public partial class GridWidget : IDisposable
|
||||
{
|
||||
private bool _visible;
|
||||
private double _scaledSize;
|
||||
private double _posX;
|
||||
private double _posY;
|
||||
|
||||
[CascadingParameter] public BlazorDiagram BlazorDiagram { get; set; } = null!;
|
||||
[Parameter] public double Size { get; set; } = 20;
|
||||
[Parameter] public double ZoomThreshold { get; set; } = 0;
|
||||
[Parameter] public GridMode Mode { get; set; } = GridMode.Line;
|
||||
[Parameter] public string BackgroundColor { get; set; } = "rgb(241 241 241)";
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
BlazorDiagram.PanChanged -= RefreshPosition;
|
||||
BlazorDiagram.ZoomChanged -= RefreshPosition;
|
||||
}
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
BlazorDiagram.PanChanged += RefreshPosition;
|
||||
BlazorDiagram.ZoomChanged += RefreshPosition;
|
||||
}
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
_posX = BlazorDiagram.Pan.X;
|
||||
_posY = BlazorDiagram.Pan.Y;
|
||||
_scaledSize = Size * BlazorDiagram.Zoom;
|
||||
_visible = BlazorDiagram.Zoom > ZoomThreshold;
|
||||
}
|
||||
|
||||
private void RefreshPosition()
|
||||
{
|
||||
_posX = BlazorDiagram.Pan.X;
|
||||
_posY = BlazorDiagram.Pan.Y;
|
||||
_scaledSize = Size * BlazorDiagram.Zoom;
|
||||
_visible = BlazorDiagram.Zoom > ZoomThreshold;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private string GenerateStyle()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.Append($"background-color: {BackgroundColor};");
|
||||
sb.Append($"background-size: {_scaledSize.ToInvariantString()}px {_scaledSize.ToInvariantString()}px;");
|
||||
sb.Append($"background-position-x: {_posX.ToInvariantString()}px;");
|
||||
sb.Append($"background-position-y: {_posY.ToInvariantString()}px;");
|
||||
|
||||
switch (Mode)
|
||||
{
|
||||
case GridMode.Line:
|
||||
sb.Append("background-image: linear-gradient(rgb(211, 211, 211) 1px, transparent 1px), linear-gradient(90deg, rgb(211, 211, 211) 1px, transparent 1px);");
|
||||
break;
|
||||
case GridMode.Point:
|
||||
sb.Append("background-image: radial-gradient(circle at 0 0, rgb(129, 129, 129) 1px, transparent 1px);");
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public enum GridMode
|
||||
{
|
||||
Line,
|
||||
Point
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<svg class="navigator @Class"
|
||||
style="@Style"
|
||||
width="@Width.ToInvariantString()"
|
||||
height="@Height.ToInvariantString()"
|
||||
viewBox="@(FormattableString.Invariant($"{_x} {_y} {_width} {_height}"))">
|
||||
|
||||
@foreach (var group in BlazorDiagram.Groups)
|
||||
{
|
||||
if (group.Size == null)
|
||||
continue;
|
||||
|
||||
<rect @key="group"
|
||||
class="navigator-group"
|
||||
x="@group.Position.X.ToInvariantString()"
|
||||
y="@group.Position.Y.ToInvariantString()"
|
||||
width="@group.Size.Width.ToInvariantString()"
|
||||
height="@group.Size.Height.ToInvariantString()"
|
||||
fill="@GroupColor">
|
||||
</rect>
|
||||
}
|
||||
|
||||
@foreach (var node in BlazorDiagram.Nodes)
|
||||
{
|
||||
if (node.Size == null)
|
||||
continue;
|
||||
|
||||
@GetNodeRenderFragment(node)
|
||||
}
|
||||
|
||||
<rect class="navigator-current-view"
|
||||
x="@(_vX.ToInvariantString())"
|
||||
y="@(_vY.ToInvariantString())"
|
||||
width="@_vWidth.ToInvariantString()"
|
||||
height="@_vHeight.ToInvariantString()"
|
||||
fill="none"
|
||||
stroke="@ViewStrokeColor"
|
||||
stroke-width="@ViewStrokeWidth">
|
||||
</rect>
|
||||
|
||||
</svg>
|
||||
@@ -0,0 +1,155 @@
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Extensions;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Components.Widgets;
|
||||
|
||||
public partial class NavigatorWidget : IDisposable
|
||||
{
|
||||
private double _x;
|
||||
private double _y;
|
||||
private double _width;
|
||||
private double _height;
|
||||
private double _scaledMargin;
|
||||
private double _vX;
|
||||
private double _vY;
|
||||
private double _vWidth;
|
||||
private double _vHeight;
|
||||
|
||||
[CascadingParameter] public BlazorDiagram BlazorDiagram { get; set; } = null!;
|
||||
[Parameter] public bool UseNodeShape { get; set; } = true;
|
||||
[Parameter] public double Width { get; set; }
|
||||
[Parameter] public double Height { get; set; }
|
||||
[Parameter] public double Margin { get; set; } = 5;
|
||||
[Parameter] public string NodeColor { get; set; } = "#40babd";
|
||||
[Parameter] public string GroupColor { get; set; } = "#9fd0d1";
|
||||
[Parameter] public string ViewStrokeColor { get; set; } = "#40babd";
|
||||
[Parameter] public int ViewStrokeWidth { get; set; } = 4;
|
||||
[Parameter] public string? Class { get; set; }
|
||||
[Parameter] public string? Style { get; set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
BlazorDiagram.Changed -= Refresh;
|
||||
BlazorDiagram.Nodes.Added -= OnNodeAdded;
|
||||
BlazorDiagram.Nodes.Removed -= OnNodeRemoved;
|
||||
BlazorDiagram.Groups.Added -= OnNodeAdded;
|
||||
BlazorDiagram.Groups.Removed -= OnNodeRemoved;
|
||||
|
||||
foreach (var node in BlazorDiagram.Nodes)
|
||||
node.Changed -= OnNodeChanged;
|
||||
|
||||
foreach (var group in BlazorDiagram.Groups)
|
||||
group.Changed -= OnNodeChanged;
|
||||
}
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
BlazorDiagram.Changed += Refresh;
|
||||
BlazorDiagram.Nodes.Added += OnNodeAdded;
|
||||
BlazorDiagram.Nodes.Removed += OnNodeRemoved;
|
||||
BlazorDiagram.Groups.Added += OnNodeAdded;
|
||||
BlazorDiagram.Groups.Removed += OnNodeRemoved;
|
||||
|
||||
foreach (var node in BlazorDiagram.Nodes)
|
||||
node.Changed += OnNodeChanged;
|
||||
|
||||
foreach (var group in BlazorDiagram.Groups)
|
||||
group.Changed += OnNodeChanged;
|
||||
}
|
||||
|
||||
private void OnNodeAdded(NodeModel node) => node.Changed += OnNodeChanged;
|
||||
|
||||
private void OnNodeRemoved(NodeModel node) => node.Changed -= OnNodeChanged;
|
||||
|
||||
private void OnNodeChanged(Model _) => Refresh();
|
||||
|
||||
private void Refresh()
|
||||
{
|
||||
if (BlazorDiagram.Container == null)
|
||||
return;
|
||||
|
||||
_vX = -BlazorDiagram.Pan.X / BlazorDiagram.Zoom;
|
||||
_vY = -BlazorDiagram.Pan.Y / BlazorDiagram.Zoom;
|
||||
_vWidth = BlazorDiagram.Container.Width / BlazorDiagram.Zoom;
|
||||
_vHeight = BlazorDiagram.Container.Height / BlazorDiagram.Zoom;
|
||||
|
||||
var minX = _vX;
|
||||
var minY = _vY;
|
||||
var maxX = _vX + _vWidth;
|
||||
var maxY = _vY + _vHeight;
|
||||
|
||||
foreach (var node in BlazorDiagram.Nodes.Union(BlazorDiagram.Groups))
|
||||
{
|
||||
if (node.Size == null)
|
||||
continue;
|
||||
|
||||
minX = Math.Min(minX, node.Position.X);
|
||||
minY = Math.Min(minY, node.Position.Y);
|
||||
maxX = Math.Max(maxX, node.Position.X + node.Size.Width);
|
||||
maxY = Math.Max(maxY, node.Position.Y + node.Size.Height);
|
||||
}
|
||||
|
||||
var width = maxX - minX;
|
||||
var height = maxY - minY;
|
||||
var scaledWidth = width / Width;
|
||||
var scaledHeight = height / Height;
|
||||
var scale = Math.Max(scaledWidth, scaledHeight);
|
||||
var viewWidth = scale * Width;
|
||||
var viewHeight = scale * Height;
|
||||
|
||||
_scaledMargin = Margin * scale;
|
||||
_x = minX - (viewWidth - width) / 2 - _scaledMargin;
|
||||
_y = minY - (viewHeight - height) / 2 - _scaledMargin;
|
||||
_width = viewWidth + _scaledMargin * 2;
|
||||
_height = viewHeight + _scaledMargin * 2;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private RenderFragment GetNodeRenderFragment(NodeModel node)
|
||||
{
|
||||
return builder =>
|
||||
{
|
||||
if (UseNodeShape)
|
||||
{
|
||||
var shape = node.GetShape();
|
||||
if (shape is Ellipse ellipse)
|
||||
{
|
||||
RenderEllipse(node, builder, ellipse);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
RenderRect(node, builder);
|
||||
};
|
||||
}
|
||||
|
||||
private void RenderRect(NodeModel node, RenderTreeBuilder builder)
|
||||
{
|
||||
builder.OpenElement(0, "rect");
|
||||
builder.SetKey(node);
|
||||
builder.AddAttribute(1, "class", "navigator-node");
|
||||
builder.AddAttribute(2, "fill", NodeColor);
|
||||
builder.AddAttribute(2, "x", node.Position.X.ToInvariantString());
|
||||
builder.AddAttribute(2, "y", node.Position.Y.ToInvariantString());
|
||||
builder.AddAttribute(2, "width", node.Size!.Width.ToInvariantString());
|
||||
builder.AddAttribute(2, "height", node.Size.Height.ToInvariantString());
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
private void RenderEllipse(NodeModel node, RenderTreeBuilder builder, Ellipse ellipse)
|
||||
{
|
||||
builder.OpenElement(0, "ellipse");
|
||||
builder.SetKey(node);
|
||||
builder.AddAttribute(1, "class", "navigator-node");
|
||||
builder.AddAttribute(2, "fill", NodeColor);
|
||||
builder.AddAttribute(2, "cx", ellipse.Cx.ToInvariantString());
|
||||
builder.AddAttribute(2, "cy", ellipse.Cy.ToInvariantString());
|
||||
builder.AddAttribute(2, "rx", ellipse.Rx.ToInvariantString());
|
||||
builder.AddAttribute(2, "ry", ellipse.Ry.ToInvariantString());
|
||||
builder.CloseElement();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@if (_selectionBoxTopLeft != null && _selectionBoxSize != null)
|
||||
{
|
||||
<div style="@GenerateStyle()"></div>
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Events;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Components.Widgets;
|
||||
|
||||
public partial class SelectionBoxWidget : IDisposable
|
||||
{
|
||||
private Point? _initialClientPoint;
|
||||
private Size? _selectionBoxSize; // Todo: remove unneeded instantiations
|
||||
private Point? _selectionBoxTopLeft; // Todo: remove unneeded instantiations
|
||||
|
||||
[CascadingParameter] public BlazorDiagram BlazorDiagram { get; set; } = null!;
|
||||
|
||||
[Parameter] public string Background { get; set; } = "rgb(110 159 212 / 25%)";
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
BlazorDiagram.PointerDown -= OnPointerDown;
|
||||
BlazorDiagram.PointerMove -= OnPointerMove;
|
||||
BlazorDiagram.PointerUp -= OnPointerUp;
|
||||
}
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
BlazorDiagram.PointerDown += OnPointerDown;
|
||||
BlazorDiagram.PointerMove += OnPointerMove;
|
||||
BlazorDiagram.PointerUp += OnPointerUp;
|
||||
}
|
||||
|
||||
private string GenerateStyle()
|
||||
{
|
||||
return FormattableString.Invariant(
|
||||
$"position: absolute; background: {Background}; top: {_selectionBoxTopLeft!.Y}px; left: {_selectionBoxTopLeft.X}px; width: {_selectionBoxSize!.Width}px; height: {_selectionBoxSize.Height}px;");
|
||||
}
|
||||
|
||||
private void OnPointerDown(Model? model, MouseEventArgs e)
|
||||
{
|
||||
if (model != null || !e.ShiftKey)
|
||||
return;
|
||||
|
||||
_initialClientPoint = new Point(e.ClientX, e.ClientY);
|
||||
}
|
||||
|
||||
private void OnPointerMove(Model? model, MouseEventArgs e)
|
||||
{
|
||||
if (_initialClientPoint == null)
|
||||
return;
|
||||
|
||||
SetSelectionBoxInformation(e);
|
||||
|
||||
var start = BlazorDiagram.GetRelativeMousePoint(_initialClientPoint.X, _initialClientPoint.Y);
|
||||
var end = BlazorDiagram.GetRelativeMousePoint(e.ClientX, e.ClientY);
|
||||
var (sX, sY) = (Math.Min(start.X, end.X), Math.Min(start.Y, end.Y));
|
||||
var (eX, eY) = (Math.Max(start.X, end.X), Math.Max(start.Y, end.Y));
|
||||
var bounds = new Rectangle(sX, sY, eX, eY);
|
||||
|
||||
foreach (var node in BlazorDiagram.Nodes)
|
||||
{
|
||||
var nodeBounds = node.GetBounds();
|
||||
if (nodeBounds == null)
|
||||
continue;
|
||||
|
||||
if (bounds.Overlap(nodeBounds))
|
||||
BlazorDiagram.SelectModel(node, false);
|
||||
else if (node.Selected) BlazorDiagram.UnselectModel(node);
|
||||
}
|
||||
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private void SetSelectionBoxInformation(MouseEventArgs e)
|
||||
{
|
||||
var start = BlazorDiagram.GetRelativePoint(_initialClientPoint!.X, _initialClientPoint.Y);
|
||||
var end = BlazorDiagram.GetRelativePoint(e.ClientX, e.ClientY);
|
||||
var (sX, sY) = (Math.Min(start.X, end.X), Math.Min(start.Y, end.Y));
|
||||
var (eX, eY) = (Math.Max(start.X, end.X), Math.Max(start.Y, end.Y));
|
||||
_selectionBoxTopLeft = new Point(sX, sY);
|
||||
_selectionBoxSize = new Size(eX - sX, eY - sY);
|
||||
}
|
||||
|
||||
private void OnPointerUp(Model? model, MouseEventArgs e)
|
||||
{
|
||||
_initialClientPoint = null;
|
||||
_selectionBoxTopLeft = null;
|
||||
_selectionBoxSize = null;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Anchors;
|
||||
|
||||
public abstract class Anchor
|
||||
{
|
||||
protected Anchor(ILinkable? model = null)
|
||||
{
|
||||
Model = model;
|
||||
}
|
||||
|
||||
public ILinkable? Model { get; }
|
||||
|
||||
public abstract Point? GetPosition(BaseLinkModel link, Point[] route);
|
||||
|
||||
public abstract Point? GetPlainPosition();
|
||||
|
||||
public Point? GetPosition(BaseLinkModel link) => GetPosition(link, Array.Empty<Point>());
|
||||
|
||||
protected static Point? GetOtherPosition(BaseLinkModel link, bool isTarget)
|
||||
{
|
||||
var anchor = isTarget ? link.Source : link.Target!;
|
||||
return anchor.GetPlainPosition();
|
||||
}
|
||||
|
||||
protected static Point? GetClosestPointTo(IEnumerable<Point?> points, Point point)
|
||||
{
|
||||
var minDist = double.MaxValue;
|
||||
Point? minPoint = null;
|
||||
|
||||
foreach (var pt in points)
|
||||
{
|
||||
if (pt == null)
|
||||
continue;
|
||||
|
||||
var dist = pt.DistanceTo(point);
|
||||
if (dist < minDist)
|
||||
{
|
||||
minDist = dist;
|
||||
minPoint = pt;
|
||||
}
|
||||
}
|
||||
|
||||
return minPoint;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Positions;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Anchors;
|
||||
|
||||
public sealed class DynamicAnchor : Anchor
|
||||
{
|
||||
public DynamicAnchor(NodeModel model, IPositionProvider[] providers) : base(model)
|
||||
{
|
||||
if (providers.Length == 0)
|
||||
throw new InvalidOperationException("No providers provided");
|
||||
|
||||
Node = model;
|
||||
Providers = providers;
|
||||
}
|
||||
|
||||
public NodeModel Node { get; }
|
||||
public IPositionProvider[] Providers { get; }
|
||||
|
||||
public override Point? GetPosition(BaseLinkModel link, Point[] route)
|
||||
{
|
||||
if (Node.Size == null)
|
||||
return null;
|
||||
|
||||
var isTarget = link.Target == this;
|
||||
var pt = route.Length > 0 ? route[isTarget ? ^1 : 0] : GetOtherPosition(link, isTarget);
|
||||
var positions = Providers.Select(e => e.GetPosition(Node));
|
||||
return pt is null ? null : GetClosestPointTo(positions, pt);
|
||||
}
|
||||
|
||||
public override Point? GetPlainPosition() => Node.GetBounds()?.Center ?? null;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Positions;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Anchors;
|
||||
|
||||
public class LinkAnchor : Anchor
|
||||
{
|
||||
private readonly LinkPathPositionProvider _positionProvider;
|
||||
|
||||
public LinkAnchor(BaseLinkModel link, double distance, double offsetX = 0, double offsetY = 0) : base(link)
|
||||
{
|
||||
_positionProvider = new LinkPathPositionProvider(distance, offsetX, offsetY);
|
||||
Link = link;
|
||||
}
|
||||
|
||||
public BaseLinkModel Link { get; }
|
||||
|
||||
public override Point? GetPosition(BaseLinkModel link, Point[] route) => _positionProvider.GetPosition(Link);
|
||||
|
||||
public override Point? GetPlainPosition() => _positionProvider.GetPosition(Link);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Anchors;
|
||||
|
||||
public sealed class PositionAnchor : Anchor
|
||||
{
|
||||
private Point _position;
|
||||
|
||||
public PositionAnchor(Point position) : base(null)
|
||||
{
|
||||
_position = position;
|
||||
}
|
||||
|
||||
public void SetPosition(Point position) => _position = position;
|
||||
|
||||
public override Point? GetPlainPosition() => _position;
|
||||
|
||||
public override Point? GetPosition(BaseLinkModel link, Point[] route) => _position;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Anchors;
|
||||
|
||||
public sealed class ShapeIntersectionAnchor : Anchor
|
||||
{
|
||||
public ShapeIntersectionAnchor(NodeModel model) : base(model)
|
||||
{
|
||||
Node = model;
|
||||
}
|
||||
|
||||
public NodeModel Node { get; }
|
||||
|
||||
public override Point? GetPosition(BaseLinkModel link, Point[] route)
|
||||
{
|
||||
if (Node.Size == null)
|
||||
return null;
|
||||
|
||||
var isTarget = link.Target == this;
|
||||
var nodeCenter = Node.GetBounds()!.Center;
|
||||
Point? pt;
|
||||
if (route.Length > 0)
|
||||
{
|
||||
pt = route[isTarget ? ^1 : 0];
|
||||
}
|
||||
else
|
||||
{
|
||||
pt = GetOtherPosition(link, isTarget);
|
||||
}
|
||||
|
||||
if (pt is null) return null;
|
||||
|
||||
var line = new Line(pt, nodeCenter);
|
||||
var intersections = Node.GetShape().GetIntersectionsWithLine(line);
|
||||
return GetClosestPointTo(intersections, pt);
|
||||
}
|
||||
|
||||
public override Point? GetPlainPosition() => Node.GetBounds()?.Center ?? null;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Anchors;
|
||||
|
||||
public sealed class SinglePortAnchor : Anchor
|
||||
{
|
||||
public SinglePortAnchor(PortModel port) : base(port)
|
||||
{
|
||||
Port = port;
|
||||
}
|
||||
|
||||
public PortModel Port { get; }
|
||||
public bool MiddleIfNoMarker { get; set; } = false;
|
||||
public bool UseShapeAndAlignment { get; set; } = true;
|
||||
|
||||
public override Point? GetPosition(BaseLinkModel link, Point[] route)
|
||||
{
|
||||
if (!Port.Initialized)
|
||||
return null;
|
||||
|
||||
if (MiddleIfNoMarker && ((link.Source == this && link.SourceMarker is null) || (link.Target == this && link.TargetMarker is null)))
|
||||
return Port.MiddlePosition;
|
||||
|
||||
var pt = Port.Position;
|
||||
if (UseShapeAndAlignment)
|
||||
{
|
||||
return Port.Alignment switch
|
||||
{
|
||||
PortAlignment.Top => Port.GetShape().GetPointAtAngle(270),
|
||||
PortAlignment.TopRight => Port.GetShape().GetPointAtAngle(315),
|
||||
PortAlignment.Right => Port.GetShape().GetPointAtAngle(0),
|
||||
PortAlignment.BottomRight => Port.GetShape().GetPointAtAngle(45),
|
||||
PortAlignment.Bottom => Port.GetShape().GetPointAtAngle(90),
|
||||
PortAlignment.BottomLeft => Port.GetShape().GetPointAtAngle(135),
|
||||
PortAlignment.Left => Port.GetShape().GetPointAtAngle(180),
|
||||
PortAlignment.TopLeft => Port.GetShape().GetPointAtAngle(225),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
return Port.Alignment switch
|
||||
{
|
||||
PortAlignment.Top => new Point(pt.X + Port.Size.Width / 2, pt.Y),
|
||||
PortAlignment.TopRight => new Point(pt.X + Port.Size.Width, pt.Y),
|
||||
PortAlignment.Right => new Point(pt.X + Port.Size.Width, pt.Y + Port.Size.Height / 2),
|
||||
PortAlignment.BottomRight => new Point(pt.X + Port.Size.Width, pt.Y + Port.Size.Height),
|
||||
PortAlignment.Bottom => new Point(pt.X + Port.Size.Width / 2, pt.Y + Port.Size.Height),
|
||||
PortAlignment.BottomLeft => new Point(pt.X, pt.Y + Port.Size.Height),
|
||||
PortAlignment.Left => new Point(pt.X, pt.Y + Port.Size.Height / 2),
|
||||
_ => pt,
|
||||
};
|
||||
}
|
||||
|
||||
public override Point? GetPlainPosition() => Port.MiddlePosition;
|
||||
}
|
||||
13
src/Gateway/ThingsGateway.Blazor.Diagrams/Core/Behavior.cs
Normal file
13
src/Gateway/ThingsGateway.Blazor.Diagrams/Core/Behavior.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core;
|
||||
|
||||
public abstract class Behavior : IDisposable
|
||||
{
|
||||
public Behavior(Diagram diagram)
|
||||
{
|
||||
Diagram = diagram;
|
||||
}
|
||||
|
||||
protected Diagram Diagram { get; }
|
||||
|
||||
public abstract void Dispose();
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Behaviors;
|
||||
|
||||
public class DebugEventsBehavior : Behavior
|
||||
{
|
||||
public DebugEventsBehavior(Diagram diagram) : base(diagram)
|
||||
{
|
||||
Diagram.Changed += Diagram_Changed;
|
||||
Diagram.ContainerChanged += Diagram_ContainerChanged;
|
||||
Diagram.PanChanged += Diagram_PanChanged;
|
||||
Diagram.Nodes.Added += Nodes_Added;
|
||||
Diagram.Nodes.Removed += Nodes_Removed;
|
||||
Diagram.Links.Added += Links_Added;
|
||||
Diagram.Links.Removed += Links_Removed;
|
||||
Diagram.Groups.Added += Diagram_GroupAdded;
|
||||
Diagram.Groups.Removed += Diagram_GroupRemoved;
|
||||
Diagram.SelectionChanged += Diagram_SelectionChanged;
|
||||
Diagram.ZoomChanged += Diagram_ZoomChanged;
|
||||
}
|
||||
|
||||
private void Diagram_ZoomChanged()
|
||||
{
|
||||
Console.WriteLine($"ZoomChanged, Zoom={Diagram.Zoom}");
|
||||
}
|
||||
|
||||
private void Diagram_SelectionChanged(SelectableModel obj)
|
||||
{
|
||||
Console.WriteLine($"SelectionChanged, Model={obj.GetType().Name}, Selected={obj.Selected}");
|
||||
}
|
||||
|
||||
private void Links_Removed(BaseLinkModel obj)
|
||||
{
|
||||
Console.WriteLine($"Links.Removed, Links=[{obj}]");
|
||||
}
|
||||
|
||||
private void Links_Added(BaseLinkModel obj)
|
||||
{
|
||||
Console.WriteLine($"Links.Added, Links=[{obj}]");
|
||||
}
|
||||
|
||||
private void Nodes_Removed(NodeModel obj)
|
||||
{
|
||||
Console.WriteLine($"Nodes.Removed, Nodes=[{obj}]");
|
||||
}
|
||||
|
||||
private void Nodes_Added(NodeModel obj)
|
||||
{
|
||||
Console.WriteLine($"Nodes.Added, Nodes=[{obj}]");
|
||||
}
|
||||
|
||||
private void Diagram_PanChanged()
|
||||
{
|
||||
Console.WriteLine($"PanChanged, Pan={Diagram.Pan}");
|
||||
}
|
||||
|
||||
private void Diagram_GroupRemoved(GroupModel obj)
|
||||
{
|
||||
Console.WriteLine($"GroupRemoved, Id={obj.Id}");
|
||||
}
|
||||
|
||||
private void Diagram_GroupAdded(GroupModel obj)
|
||||
{
|
||||
Console.WriteLine($"GroupAdded, Id={obj.Id}");
|
||||
}
|
||||
|
||||
private void Diagram_ContainerChanged()
|
||||
{
|
||||
Console.WriteLine($"ContainerChanged, Container={Diagram.Container}");
|
||||
}
|
||||
|
||||
private void Diagram_Changed()
|
||||
{
|
||||
Console.WriteLine("Changed");
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
Diagram.Changed -= Diagram_Changed;
|
||||
Diagram.ContainerChanged -= Diagram_ContainerChanged;
|
||||
Diagram.PanChanged -= Diagram_PanChanged;
|
||||
Diagram.Nodes.Added -= Nodes_Added;
|
||||
Diagram.Nodes.Removed -= Nodes_Removed;
|
||||
Diagram.Links.Added -= Links_Added;
|
||||
Diagram.Links.Removed -= Links_Removed;
|
||||
Diagram.Groups.Added -= Diagram_GroupAdded;
|
||||
Diagram.Groups.Removed -= Diagram_GroupRemoved;
|
||||
Diagram.SelectionChanged -= Diagram_SelectionChanged;
|
||||
Diagram.ZoomChanged -= Diagram_ZoomChanged;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Events;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Behaviors;
|
||||
|
||||
public class DragMovablesBehavior : Behavior
|
||||
{
|
||||
private readonly Dictionary<MovableModel, Point> _initialPositions;
|
||||
private double? _lastClientX;
|
||||
private double? _lastClientY;
|
||||
private bool _moved;
|
||||
|
||||
public DragMovablesBehavior(Diagram diagram) : base(diagram)
|
||||
{
|
||||
_initialPositions = new Dictionary<MovableModel, Point>();
|
||||
Diagram.PointerDown += OnPointerDown;
|
||||
Diagram.PointerMove += OnPointerMove;
|
||||
Diagram.PointerUp += OnPointerUp;
|
||||
}
|
||||
|
||||
private void OnPointerDown(Model? model, PointerEventArgs e)
|
||||
{
|
||||
if (model is not MovableModel)
|
||||
return;
|
||||
|
||||
_initialPositions.Clear();
|
||||
foreach (var sm in Diagram.GetSelectedModels())
|
||||
{
|
||||
if (sm is not MovableModel movable || movable.Locked)
|
||||
continue;
|
||||
|
||||
// Special case: groups without auto size on
|
||||
if (sm is NodeModel node && node.Group != null && !node.Group.AutoSize)
|
||||
continue;
|
||||
|
||||
var position = movable.Position;
|
||||
if (Diagram.Options.GridSnapToCenter && movable is NodeModel n)
|
||||
{
|
||||
position = new Point(movable.Position.X + (n.Size?.Width ?? 0) / 2,
|
||||
movable.Position.Y + (n.Size?.Height ?? 0) / 2);
|
||||
}
|
||||
|
||||
_initialPositions.Add(movable, position);
|
||||
}
|
||||
|
||||
_lastClientX = e.ClientX;
|
||||
_lastClientY = e.ClientY;
|
||||
_moved = false;
|
||||
}
|
||||
|
||||
private void OnPointerMove(Model? model, PointerEventArgs e)
|
||||
{
|
||||
if (_initialPositions.Count == 0 || _lastClientX == null || _lastClientY == null)
|
||||
return;
|
||||
|
||||
_moved = true;
|
||||
var deltaX = (e.ClientX - _lastClientX.Value) / Diagram.Zoom;
|
||||
var deltaY = (e.ClientY - _lastClientY.Value) / Diagram.Zoom;
|
||||
|
||||
foreach (var (movable, initialPosition) in _initialPositions)
|
||||
{
|
||||
var ndx = ApplyGridSize(deltaX + initialPosition.X);
|
||||
var ndy = ApplyGridSize(deltaY + initialPosition.Y);
|
||||
if (Diagram.Options.GridSnapToCenter && movable is NodeModel node)
|
||||
{
|
||||
node.SetPosition(ndx - (node.Size?.Width ?? 0) / 2, ndy - (node.Size?.Height ?? 0) / 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
movable.SetPosition(ndx, ndy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPointerUp(Model? model, PointerEventArgs e)
|
||||
{
|
||||
if (_initialPositions.Count == 0)
|
||||
return;
|
||||
|
||||
if (_moved)
|
||||
{
|
||||
foreach (var (movable, _) in _initialPositions)
|
||||
{
|
||||
movable.TriggerMoved();
|
||||
}
|
||||
}
|
||||
|
||||
_initialPositions.Clear();
|
||||
_lastClientX = null;
|
||||
_lastClientY = null;
|
||||
}
|
||||
|
||||
private double ApplyGridSize(double n)
|
||||
{
|
||||
if (Diagram.Options.GridSize == null)
|
||||
return n;
|
||||
|
||||
var gridSize = Diagram.Options.GridSize.Value;
|
||||
return gridSize * Math.Floor((n + gridSize / 2.0) / gridSize);
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
_initialPositions.Clear();
|
||||
|
||||
Diagram.PointerDown -= OnPointerDown;
|
||||
Diagram.PointerMove -= OnPointerMove;
|
||||
Diagram.PointerUp -= OnPointerUp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Anchors;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Events;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Behaviors;
|
||||
|
||||
public class DragNewLinkBehavior : Behavior
|
||||
{
|
||||
private PositionAnchor? _targetPositionAnchor;
|
||||
|
||||
public BaseLinkModel? OngoingLink { get; private set; }
|
||||
|
||||
public DragNewLinkBehavior(Diagram diagram) : base(diagram)
|
||||
{
|
||||
Diagram.PointerDown += OnPointerDown;
|
||||
Diagram.PointerMove += OnPointerMove;
|
||||
Diagram.PointerUp += OnPointerUp;
|
||||
}
|
||||
|
||||
public void StartFrom(ILinkable source, double clientX, double clientY)
|
||||
{
|
||||
if (OngoingLink != null)
|
||||
return;
|
||||
|
||||
_targetPositionAnchor = new PositionAnchor(CalculateTargetPosition(clientX, clientY));
|
||||
OngoingLink = Diagram.Options.Links.Factory(Diagram, source, _targetPositionAnchor);
|
||||
if (OngoingLink == null)
|
||||
return;
|
||||
|
||||
Diagram.Links.Add(OngoingLink);
|
||||
}
|
||||
|
||||
public void StartFrom(BaseLinkModel link, double clientX, double clientY)
|
||||
{
|
||||
if (OngoingLink != null)
|
||||
return;
|
||||
|
||||
_targetPositionAnchor = new PositionAnchor(CalculateTargetPosition(clientX, clientY));
|
||||
OngoingLink = link;
|
||||
OngoingLink.SetTarget(_targetPositionAnchor);
|
||||
OngoingLink.Refresh();
|
||||
OngoingLink.RefreshLinks();
|
||||
}
|
||||
|
||||
private void OnPointerDown(Model? model, MouseEventArgs e)
|
||||
{
|
||||
if (e.Button != (int)MouseEventButton.Left)
|
||||
return;
|
||||
|
||||
OngoingLink = null;
|
||||
_targetPositionAnchor = null;
|
||||
|
||||
if (model is PortModel port)
|
||||
{
|
||||
if (port.Locked)
|
||||
return;
|
||||
|
||||
_targetPositionAnchor = new PositionAnchor(CalculateTargetPosition(e.ClientX, e.ClientY));
|
||||
OngoingLink = Diagram.Options.Links.Factory(Diagram, port, _targetPositionAnchor);
|
||||
if (OngoingLink == null)
|
||||
return;
|
||||
|
||||
OngoingLink.SetTarget(_targetPositionAnchor);
|
||||
Diagram.Links.Add(OngoingLink);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPointerMove(Model? model, MouseEventArgs e)
|
||||
{
|
||||
if (OngoingLink == null || model != null)
|
||||
return;
|
||||
|
||||
_targetPositionAnchor!.SetPosition(CalculateTargetPosition(e.ClientX, e.ClientY));
|
||||
|
||||
if (Diagram.Options.Links.EnableSnapping)
|
||||
{
|
||||
var nearPort = FindNearPortToAttachTo();
|
||||
if ((nearPort != null && nearPort?.Alignment == PortAlignment.Top) || OngoingLink.Target is not PositionAnchor)
|
||||
{
|
||||
OngoingLink.SetTarget(nearPort is null ? _targetPositionAnchor : new SinglePortAnchor(nearPort));
|
||||
}
|
||||
}
|
||||
|
||||
OngoingLink.Refresh();
|
||||
OngoingLink.RefreshLinks();
|
||||
}
|
||||
|
||||
private void OnPointerUp(Model? model, MouseEventArgs e)
|
||||
{
|
||||
if (OngoingLink == null)
|
||||
return;
|
||||
|
||||
if (OngoingLink.IsAttached) // Snapped already
|
||||
{
|
||||
OngoingLink.TriggerTargetAttached();
|
||||
OngoingLink = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (model is ILinkable linkable && (OngoingLink.Source.Model == null || OngoingLink.Source.Model.CanAttachTo(linkable)))
|
||||
{
|
||||
var targetAnchor = Diagram.Options.Links.TargetAnchorFactory(Diagram, OngoingLink, linkable);
|
||||
if (targetAnchor is SinglePortAnchor singlePortAnchor && singlePortAnchor.Model is PortModel portModel && portModel.Alignment == PortAlignment.Top)
|
||||
{
|
||||
OngoingLink.SetTarget(targetAnchor);
|
||||
OngoingLink.TriggerTargetAttached();
|
||||
OngoingLink.Refresh();
|
||||
OngoingLink.RefreshLinks();
|
||||
}
|
||||
else
|
||||
{
|
||||
Diagram.Links.Remove(OngoingLink);
|
||||
}
|
||||
}
|
||||
else if (Diagram.Options.Links.RequireTarget)
|
||||
{
|
||||
Diagram.Links.Remove(OngoingLink);
|
||||
}
|
||||
else if (!Diagram.Options.Links.RequireTarget)
|
||||
{
|
||||
OngoingLink.Refresh();
|
||||
}
|
||||
|
||||
OngoingLink = null;
|
||||
}
|
||||
|
||||
private Point CalculateTargetPosition(double clientX, double clientY)
|
||||
{
|
||||
var target = Diagram.GetRelativeMousePoint(clientX, clientY);
|
||||
|
||||
if (OngoingLink == null)
|
||||
{
|
||||
return target;
|
||||
}
|
||||
|
||||
var source = OngoingLink.Source.GetPlainPosition()!;
|
||||
var dirVector = target.Subtract(source).Normalize();
|
||||
var change = dirVector.Multiply(5);
|
||||
return target.Subtract(change);
|
||||
}
|
||||
|
||||
private PortModel? FindNearPortToAttachTo()
|
||||
{
|
||||
if (OngoingLink is null || _targetPositionAnchor is null)
|
||||
return null;
|
||||
|
||||
PortModel? nearestSnapPort = null;
|
||||
var nearestSnapPortDistance = double.PositiveInfinity;
|
||||
|
||||
var position = _targetPositionAnchor!.GetPosition(OngoingLink)!;
|
||||
|
||||
foreach (var port in Diagram.Nodes.SelectMany((NodeModel n) => n.Ports))
|
||||
{
|
||||
var distance = position.DistanceTo(port.Position);
|
||||
|
||||
if (distance <= Diagram.Options.Links.SnappingRadius && (OngoingLink.Source.Model?.CanAttachTo(port) != false))
|
||||
{
|
||||
if (distance < nearestSnapPortDistance)
|
||||
{
|
||||
nearestSnapPortDistance = distance;
|
||||
nearestSnapPort = port;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nearestSnapPort;
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
Diagram.PointerDown -= OnPointerDown;
|
||||
Diagram.PointerMove -= OnPointerMove;
|
||||
Diagram.PointerUp -= OnPointerUp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Events;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Behaviors;
|
||||
|
||||
public class EventsBehavior : Behavior
|
||||
{
|
||||
private readonly Stopwatch _mouseClickSw;
|
||||
private Model? _model;
|
||||
private bool _captureMouseMove;
|
||||
private int _mouseMovedCount;
|
||||
|
||||
public EventsBehavior(Diagram diagram) : base(diagram)
|
||||
{
|
||||
_mouseClickSw = new Stopwatch();
|
||||
|
||||
Diagram.PointerDown += OnPointerDown;
|
||||
Diagram.PointerMove += OnPointerMove;
|
||||
Diagram.PointerUp += OnPointerUp;
|
||||
Diagram.PointerClick += OnPointerClick;
|
||||
}
|
||||
|
||||
private void OnPointerClick(Model? model, PointerEventArgs e)
|
||||
{
|
||||
if (_mouseClickSw.IsRunning && _mouseClickSw.ElapsedMilliseconds <= 500)
|
||||
{
|
||||
Diagram.TriggerPointerDoubleClick(model, e);
|
||||
}
|
||||
|
||||
_mouseClickSw.Restart();
|
||||
}
|
||||
|
||||
private void OnPointerDown(Model? model, PointerEventArgs e)
|
||||
{
|
||||
_captureMouseMove = true;
|
||||
_mouseMovedCount = 0;
|
||||
_model = model;
|
||||
}
|
||||
|
||||
private void OnPointerMove(Model? model, PointerEventArgs e)
|
||||
{
|
||||
if (!_captureMouseMove)
|
||||
return;
|
||||
|
||||
_mouseMovedCount++;
|
||||
}
|
||||
|
||||
private void OnPointerUp(Model? model, PointerEventArgs e)
|
||||
{
|
||||
if (!_captureMouseMove) return; // Only set by OnMouseDown
|
||||
_captureMouseMove = false;
|
||||
if (_mouseMovedCount > 0) return;
|
||||
|
||||
if (_model == model)
|
||||
{
|
||||
Diagram.TriggerPointerClick(model, e);
|
||||
_model = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
Diagram.PointerDown -= OnPointerDown;
|
||||
Diagram.PointerMove -= OnPointerMove;
|
||||
Diagram.PointerUp -= OnPointerUp;
|
||||
Diagram.PointerClick -= OnPointerClick;
|
||||
_model = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Events;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Utils;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Behaviors;
|
||||
|
||||
public class CaseInsensitiveComparer : IEqualityComparer<string>
|
||||
{
|
||||
public bool Equals(string x, string y)
|
||||
{
|
||||
// 比较两个字符串,忽略大小写
|
||||
return string.Equals(x, y, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public int GetHashCode(string obj)
|
||||
{
|
||||
// 生成不区分大小写的哈希码
|
||||
return obj == null ? 0 : StringComparer.OrdinalIgnoreCase.GetHashCode(obj);
|
||||
}
|
||||
}
|
||||
|
||||
public class KeyboardShortcutsBehavior : Behavior
|
||||
{
|
||||
private readonly Dictionary<string, Func<Diagram, ValueTask>> _shortcuts;
|
||||
|
||||
public KeyboardShortcutsBehavior(Diagram diagram) : base(diagram)
|
||||
{
|
||||
_shortcuts = new Dictionary<string, Func<Diagram, ValueTask>>(10000, new CaseInsensitiveComparer());
|
||||
SetShortcut("Delete", false, false, false, KeyboardShortcutsDefaults.DeleteSelection);
|
||||
SetShortcut("g", true, false, true, KeyboardShortcutsDefaults.Grouping);
|
||||
|
||||
Diagram.KeyDown += OnDiagramKeyDown;
|
||||
}
|
||||
|
||||
public void SetShortcut(string key, bool ctrl, bool shift, bool alt, Func<Diagram, ValueTask> action)
|
||||
{
|
||||
var k = KeysUtils.GetStringRepresentation(ctrl, shift, alt, key);
|
||||
_shortcuts[k] = action;
|
||||
}
|
||||
|
||||
public bool RemoveShortcut(string key, bool ctrl, bool shift, bool alt)
|
||||
{
|
||||
var k = KeysUtils.GetStringRepresentation(ctrl, shift, alt, key);
|
||||
return _shortcuts.Remove(k);
|
||||
}
|
||||
|
||||
private async void OnDiagramKeyDown(KeyboardEventArgs e)
|
||||
{
|
||||
var k = KeysUtils.GetStringRepresentation(e.CtrlKey, e.ShiftKey, e.AltKey, e.Key);
|
||||
if (_shortcuts.TryGetValue(k, out var action))
|
||||
{
|
||||
await action(Diagram).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
Diagram.KeyDown -= OnDiagramKeyDown;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Behaviors;
|
||||
|
||||
public static class KeyboardShortcutsDefaults
|
||||
{
|
||||
public static async ValueTask DeleteSelection(Diagram diagram)
|
||||
{
|
||||
var wasSuspended = diagram.SuspendRefresh;
|
||||
if (!wasSuspended) diagram.SuspendRefresh = true;
|
||||
|
||||
foreach (var sm in diagram.GetSelectedModels().ToArray())
|
||||
{
|
||||
if (sm.Locked)
|
||||
continue;
|
||||
|
||||
if (sm is GroupModel group && (await diagram.Options.Constraints.ShouldDeleteGroup(group).ConfigureAwait(false)))
|
||||
{
|
||||
diagram.Groups.Delete(group);
|
||||
}
|
||||
else if (sm is NodeModel node && (await diagram.Options.Constraints.ShouldDeleteNode(node).ConfigureAwait(false)))
|
||||
{
|
||||
diagram.Nodes.Remove(node);
|
||||
}
|
||||
else if (sm is BaseLinkModel link && (await diagram.Options.Constraints.ShouldDeleteLink(link).ConfigureAwait(false)))
|
||||
{
|
||||
diagram.Links.Remove(link);
|
||||
}
|
||||
}
|
||||
|
||||
if (!wasSuspended)
|
||||
{
|
||||
diagram.SuspendRefresh = false;
|
||||
diagram.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public static ValueTask Grouping(Diagram diagram)
|
||||
{
|
||||
if (!diagram.Options.Groups.Enabled)
|
||||
return ValueTask.CompletedTask;
|
||||
|
||||
if (!diagram.GetSelectedModels().Any())
|
||||
return ValueTask.CompletedTask;
|
||||
|
||||
var selectedNodes = diagram.Nodes.Where(n => n.Selected).ToArray();
|
||||
var nodesWithGroup = selectedNodes.Where(n => n.Group != null).ToArray();
|
||||
if (nodesWithGroup.Length > 0)
|
||||
{
|
||||
// Ungroup
|
||||
foreach (var group in nodesWithGroup.GroupBy(n => n.Group!).Select(g => g.Key))
|
||||
{
|
||||
diagram.Groups.Remove(group);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Group
|
||||
if (selectedNodes.Length < 2)
|
||||
return ValueTask.CompletedTask;
|
||||
|
||||
if (selectedNodes.Any(n => n.Group != null))
|
||||
return ValueTask.CompletedTask;
|
||||
|
||||
diagram.Groups.Group(selectedNodes);
|
||||
}
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Events;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Behaviors;
|
||||
|
||||
public class PanBehavior : Behavior
|
||||
{
|
||||
private Point? _initialPan;
|
||||
private double _lastClientX;
|
||||
private double _lastClientY;
|
||||
|
||||
public PanBehavior(Diagram diagram) : base(diagram)
|
||||
{
|
||||
Diagram.PointerDown += OnPointerDown;
|
||||
Diagram.PointerMove += OnPointerMove;
|
||||
Diagram.PointerUp += OnPointerUp;
|
||||
}
|
||||
|
||||
private void OnPointerDown(Model? model, PointerEventArgs e)
|
||||
{
|
||||
if (e.Button != (int)MouseEventButton.Left)
|
||||
return;
|
||||
|
||||
Start(model, e.ClientX, e.ClientY, e.ShiftKey);
|
||||
}
|
||||
|
||||
private void OnPointerMove(Model? model, PointerEventArgs e) => Move(e.ClientX, e.ClientY);
|
||||
|
||||
private void OnPointerUp(Model? model, PointerEventArgs e) => End();
|
||||
|
||||
private void Start(Model? model, double clientX, double clientY, bool shiftKey)
|
||||
{
|
||||
if (!Diagram.Options.AllowPanning || model != null || shiftKey)
|
||||
return;
|
||||
|
||||
_initialPan = Diagram.Pan;
|
||||
_lastClientX = clientX;
|
||||
_lastClientY = clientY;
|
||||
}
|
||||
|
||||
private void Move(double clientX, double clientY)
|
||||
{
|
||||
if (!Diagram.Options.AllowPanning || _initialPan == null)
|
||||
return;
|
||||
|
||||
var deltaX = clientX - _lastClientX - (Diagram.Pan.X - _initialPan.X);
|
||||
var deltaY = clientY - _lastClientY - (Diagram.Pan.Y - _initialPan.Y);
|
||||
Diagram.UpdatePan(deltaX, deltaY);
|
||||
}
|
||||
|
||||
private void End()
|
||||
{
|
||||
if (!Diagram.Options.AllowPanning)
|
||||
return;
|
||||
|
||||
_initialPan = null;
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
Diagram.PointerDown -= OnPointerDown;
|
||||
Diagram.PointerMove -= OnPointerMove;
|
||||
Diagram.PointerUp -= OnPointerUp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Events;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Behaviors;
|
||||
|
||||
public class SelectionBehavior : Behavior
|
||||
{
|
||||
public SelectionBehavior(Diagram diagram) : base(diagram)
|
||||
{
|
||||
Diagram.PointerDown += OnPointerDown;
|
||||
}
|
||||
|
||||
private void OnPointerDown(Model? model, PointerEventArgs e)
|
||||
{
|
||||
var ctrlKey = e.CtrlKey;
|
||||
switch (model)
|
||||
{
|
||||
case null:
|
||||
Diagram.UnselectAll();
|
||||
break;
|
||||
case SelectableModel sm when ctrlKey && sm.Selected:
|
||||
Diagram.UnselectModel(sm);
|
||||
break;
|
||||
case SelectableModel sm:
|
||||
{
|
||||
if (!sm.Selected)
|
||||
{
|
||||
Diagram.SelectModel(sm, !ctrlKey || !Diagram.Options.AllowMultiSelection);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
Diagram.PointerDown -= OnPointerDown;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Behaviors;
|
||||
|
||||
public class VirtualizationBehavior : Behavior
|
||||
{
|
||||
public VirtualizationBehavior(Diagram diagram) : base(diagram)
|
||||
{
|
||||
Diagram.ZoomChanged += CheckVisibility;
|
||||
Diagram.PanChanged += CheckVisibility;
|
||||
Diagram.ContainerChanged += CheckVisibility;
|
||||
}
|
||||
|
||||
private void CheckVisibility()
|
||||
{
|
||||
if (!Diagram.Options.Virtualization.Enabled)
|
||||
return;
|
||||
|
||||
if (Diagram.Container == null)
|
||||
return;
|
||||
|
||||
if (Diagram.Options.Virtualization.OnNodes)
|
||||
{
|
||||
foreach (var node in Diagram.Nodes)
|
||||
{
|
||||
CheckVisibility(node);
|
||||
}
|
||||
}
|
||||
|
||||
if (Diagram.Options.Virtualization.OnGroups)
|
||||
{
|
||||
foreach (var group in Diagram.Groups)
|
||||
{
|
||||
CheckVisibility(group);
|
||||
}
|
||||
}
|
||||
|
||||
if (Diagram.Options.Virtualization.OnLinks)
|
||||
{
|
||||
foreach (var link in Diagram.Links)
|
||||
{
|
||||
CheckVisibility(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckVisibility(Model model)
|
||||
{
|
||||
if (model is not IHasBounds ihb)
|
||||
return;
|
||||
|
||||
var bounds = ihb.GetBounds();
|
||||
if (bounds == null)
|
||||
return;
|
||||
|
||||
var left = bounds.Left * Diagram.Zoom + Diagram.Pan.X;
|
||||
var top = bounds.Top * Diagram.Zoom + Diagram.Pan.Y;
|
||||
var right = left + bounds.Width * Diagram.Zoom;
|
||||
var bottom = top + bounds.Height * Diagram.Zoom;
|
||||
model.Visible = right > 0 && left < Diagram.Container!.Width && bottom > 0 && top < Diagram.Container.Height;
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
Diagram.ZoomChanged -= CheckVisibility;
|
||||
Diagram.PanChanged -= CheckVisibility;
|
||||
Diagram.ContainerChanged -= CheckVisibility;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Events;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Behaviors;
|
||||
|
||||
public class ZoomBehavior : Behavior
|
||||
{
|
||||
public ZoomBehavior(Diagram diagram) : base(diagram)
|
||||
{
|
||||
Diagram.Wheel += Diagram_Wheel;
|
||||
}
|
||||
|
||||
private void Diagram_Wheel(WheelEventArgs e)
|
||||
{
|
||||
if (Diagram.Container == null || e.DeltaY == 0)
|
||||
return;
|
||||
|
||||
if (!Diagram.Options.Zoom.Enabled)
|
||||
return;
|
||||
|
||||
var scale = Diagram.Options.Zoom.ScaleFactor;
|
||||
var oldZoom = Diagram.Zoom;
|
||||
var deltaY = Diagram.Options.Zoom.Inverse ? e.DeltaY * -1 : e.DeltaY;
|
||||
var newZoom = deltaY > 0 ? oldZoom * scale : oldZoom / scale;
|
||||
newZoom = Math.Clamp(newZoom, Diagram.Options.Zoom.Minimum, Diagram.Options.Zoom.Maximum);
|
||||
|
||||
if (newZoom < 0 || newZoom == Diagram.Zoom)
|
||||
return;
|
||||
|
||||
// Other algorithms (based only on the changes in the zoom) don't work for our case
|
||||
// This solution is taken as is from react-diagrams (ZoomCanvasAction)
|
||||
var clientWidth = Diagram.Container.Width;
|
||||
var clientHeight = Diagram.Container.Height;
|
||||
var widthDiff = clientWidth * newZoom - clientWidth * oldZoom;
|
||||
var heightDiff = clientHeight * newZoom - clientHeight * oldZoom;
|
||||
var clientX = e.ClientX - Diagram.Container.Left;
|
||||
var clientY = e.ClientY - Diagram.Container.Top;
|
||||
var xFactor = (clientX - Diagram.Pan.X) / oldZoom / clientWidth;
|
||||
var yFactor = (clientY - Diagram.Pan.Y) / oldZoom / clientHeight;
|
||||
var newPanX = Diagram.Pan.X - widthDiff * xFactor;
|
||||
var newPanY = Diagram.Pan.Y - heightDiff * yFactor;
|
||||
|
||||
Diagram.Batch(() =>
|
||||
{
|
||||
Diagram.SetPan(newPanX, newPanY);
|
||||
Diagram.SetZoom(newZoom);
|
||||
});
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
Diagram.Wheel -= Diagram_Wheel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Controls;
|
||||
|
||||
public abstract class Control
|
||||
{
|
||||
public abstract Point? GetPosition(Model model);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Events;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Controls;
|
||||
|
||||
public class ControlsBehavior : Behavior
|
||||
{
|
||||
public ControlsBehavior(Diagram diagram) : base(diagram)
|
||||
{
|
||||
Diagram.PointerEnter += OnPointerEnter;
|
||||
Diagram.PointerLeave += OnPointerLeave;
|
||||
Diagram.SelectionChanged += OnSelectionChanged;
|
||||
}
|
||||
|
||||
private void OnSelectionChanged(SelectableModel model)
|
||||
{
|
||||
var controls = Diagram.Controls.GetFor(model);
|
||||
if (controls is not { Type: ControlsType.OnSelection })
|
||||
return;
|
||||
|
||||
if (model.Selected)
|
||||
{
|
||||
controls.Show();
|
||||
}
|
||||
else
|
||||
{
|
||||
controls.Hide();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPointerEnter(Model? model, PointerEventArgs e)
|
||||
{
|
||||
if (model == null)
|
||||
return;
|
||||
|
||||
var controls = Diagram.Controls.GetFor(model);
|
||||
if (controls is not { Type: ControlsType.OnHover })
|
||||
return;
|
||||
|
||||
controls.Show();
|
||||
}
|
||||
|
||||
private void OnPointerLeave(Model? model, PointerEventArgs e)
|
||||
{
|
||||
if (model == null)
|
||||
return;
|
||||
|
||||
var controls = Diagram.Controls.GetFor(model);
|
||||
if (controls is not { Type: ControlsType.OnHover })
|
||||
return;
|
||||
|
||||
controls.Hide();
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
Diagram.PointerEnter -= OnPointerEnter;
|
||||
Diagram.PointerLeave -= OnPointerLeave;
|
||||
Diagram.SelectionChanged -= OnSelectionChanged;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System.Collections;
|
||||
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Controls;
|
||||
|
||||
public class ControlsContainer : IReadOnlyList<Control>
|
||||
{
|
||||
private readonly List<Control> _controls = new(4);
|
||||
|
||||
public event Action<Model>? VisibilityChanged;
|
||||
public event Action<Model>? ControlsChanged;
|
||||
|
||||
public ControlsContainer(Model model, ControlsType type = ControlsType.OnSelection)
|
||||
{
|
||||
Model = model;
|
||||
Type = type;
|
||||
}
|
||||
|
||||
public Model Model { get; }
|
||||
public ControlsType Type { get; set; }
|
||||
public bool Visible { get; private set; }
|
||||
|
||||
public void Show()
|
||||
{
|
||||
if (Visible)
|
||||
return;
|
||||
|
||||
Visible = true;
|
||||
VisibilityChanged?.Invoke(Model);
|
||||
}
|
||||
|
||||
public void Hide()
|
||||
{
|
||||
if (!Visible)
|
||||
return;
|
||||
|
||||
Visible = false;
|
||||
VisibilityChanged?.Invoke(Model);
|
||||
}
|
||||
|
||||
public ControlsContainer Add(Control control)
|
||||
{
|
||||
_controls.Add(control);
|
||||
ControlsChanged?.Invoke(Model);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ControlsContainer Remove(Control control)
|
||||
{
|
||||
if (_controls.Remove(control))
|
||||
{
|
||||
ControlsChanged?.Invoke(Model);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public int Count => _controls.Count;
|
||||
public Control this[int index] => _controls[index];
|
||||
public IEnumerator<Control> GetEnumerator() => _controls.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => _controls.GetEnumerator();
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Controls;
|
||||
|
||||
public class ControlsLayer
|
||||
{
|
||||
private readonly Dictionary<Model, ControlsContainer> _containers;
|
||||
|
||||
public event Action<Model>? ChangeCaused;
|
||||
|
||||
public ControlsLayer()
|
||||
{
|
||||
_containers = new Dictionary<Model, ControlsContainer>();
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<Model> Models => _containers.Keys;
|
||||
|
||||
public ControlsContainer AddFor(Model model, ControlsType type = ControlsType.OnSelection)
|
||||
{
|
||||
if (_containers.TryGetValue(model, out ControlsContainer? value))
|
||||
return value;
|
||||
|
||||
var container = new ControlsContainer(model, type);
|
||||
container.VisibilityChanged += OnVisibilityChanged;
|
||||
container.ControlsChanged += RefreshIfVisible;
|
||||
model.Changed += RefreshIfVisible;
|
||||
_containers.Add(model, container);
|
||||
return container;
|
||||
}
|
||||
|
||||
public ControlsContainer? GetFor(Model model)
|
||||
{
|
||||
return _containers.TryGetValue(model, out var container) ? container : null;
|
||||
}
|
||||
|
||||
public bool RemoveFor(Model model)
|
||||
{
|
||||
if (!_containers.TryGetValue(model, out var container))
|
||||
return false;
|
||||
|
||||
container.VisibilityChanged -= OnVisibilityChanged;
|
||||
container.ControlsChanged -= RefreshIfVisible;
|
||||
model.Changed -= RefreshIfVisible;
|
||||
_containers.Remove(model);
|
||||
ChangeCaused?.Invoke(model);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool AreVisibleFor(Model model) => GetFor(model)?.Visible ?? false;
|
||||
|
||||
private void RefreshIfVisible(Model cause)
|
||||
{
|
||||
if (!AreVisibleFor(cause))
|
||||
return;
|
||||
|
||||
ChangeCaused?.Invoke(cause);
|
||||
}
|
||||
|
||||
private void OnVisibilityChanged(Model cause) => ChangeCaused?.Invoke(cause);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Controls;
|
||||
|
||||
public enum ControlsType
|
||||
{
|
||||
OnHover,
|
||||
OnSelection
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Behaviors;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Events;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Positions;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Controls.Default;
|
||||
|
||||
public class ArrowHeadControl : ExecutableControl
|
||||
{
|
||||
public ArrowHeadControl(bool source, LinkMarker? marker = null)
|
||||
{
|
||||
Source = source;
|
||||
Marker = marker ?? LinkMarker.NewArrow(20, 20);
|
||||
}
|
||||
|
||||
public bool Source { get; }
|
||||
public LinkMarker Marker { get; }
|
||||
public double Angle { get; private set; }
|
||||
|
||||
public override Point? GetPosition(Model model)
|
||||
{
|
||||
if (model is not BaseLinkModel link)
|
||||
throw new DiagramsException("ArrowHeadControl only works for models of type BaseLinkModel");
|
||||
|
||||
var dist = Source ? Marker.Width - (link.SourceMarker?.Width ?? 0) : (link.TargetMarker?.Width ?? 0) - Marker.Width;
|
||||
var pp = new LinkPathPositionProvider(dist);
|
||||
var p1 = pp.GetPosition(link);
|
||||
if (p1 is not null)
|
||||
{
|
||||
var p2 = Source ? link.Source.GetPosition(link) : link.Target.GetPosition(link);
|
||||
if (p2 is not null)
|
||||
{
|
||||
Angle = Math.Atan2(p2.Y - p1.Y, p2.X - p1.X) * 180 / Math.PI;
|
||||
}
|
||||
}
|
||||
|
||||
return p1;
|
||||
}
|
||||
|
||||
public override ValueTask OnPointerDown(Diagram diagram, Model model, PointerEventArgs e)
|
||||
{
|
||||
if (model is not BaseLinkModel link)
|
||||
throw new DiagramsException("ArrowHeadControl only works for models of type BaseLinkModel");
|
||||
|
||||
var dnlb = diagram.GetBehavior<DragNewLinkBehavior>()!;
|
||||
if (Source)
|
||||
{
|
||||
link.SetSource(link.Target);
|
||||
}
|
||||
|
||||
dnlb.StartFrom(link, e.ClientX, e.ClientY);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Controls.Default;
|
||||
|
||||
public class BoundaryControl : Control
|
||||
{
|
||||
public Rectangle Bounds { get; private set; } = Rectangle.Zero;
|
||||
|
||||
public override Point? GetPosition(Model model)
|
||||
{
|
||||
if (model is not IHasBounds hb)
|
||||
return null;
|
||||
|
||||
var bounds = hb.GetBounds();
|
||||
if (bounds == null)
|
||||
return null;
|
||||
|
||||
Bounds = bounds.Inflate(10, 10);
|
||||
return Bounds.NorthWest;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Behaviors;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Events;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Positions;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Controls.Default;
|
||||
|
||||
public class DragNewLinkControl : ExecutableControl
|
||||
{
|
||||
private readonly IPositionProvider _positionProvider;
|
||||
|
||||
public DragNewLinkControl(double x, double y, double offsetX = 0, double offsetY = 0)
|
||||
: this(new BoundsBasedPositionProvider(x, y, offsetX, offsetY))
|
||||
{
|
||||
}
|
||||
|
||||
public DragNewLinkControl(IPositionProvider positionProvider)
|
||||
{
|
||||
_positionProvider = positionProvider;
|
||||
}
|
||||
|
||||
public override Point? GetPosition(Model model) => _positionProvider.GetPosition(model);
|
||||
|
||||
public override ValueTask OnPointerDown(Diagram diagram, Model model, PointerEventArgs e)
|
||||
{
|
||||
if (model is not NodeModel node || node.Locked)
|
||||
return ValueTask.CompletedTask;
|
||||
|
||||
var behavior = diagram.GetBehavior<DragNewLinkBehavior>();
|
||||
if (behavior == null)
|
||||
throw new DiagramsException($"DragNewLinkBehavior was not found");
|
||||
|
||||
behavior.StartFrom(node, e.ClientX, e.ClientY);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Events;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Positions;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Controls.Default;
|
||||
|
||||
public class RemoveControl : ExecutableControl
|
||||
{
|
||||
private readonly IPositionProvider _positionProvider;
|
||||
|
||||
public RemoveControl(double x, double y, double offsetX = 0, double offsetY = 0)
|
||||
: this(new BoundsBasedPositionProvider(x, y, offsetX, offsetY))
|
||||
{
|
||||
}
|
||||
|
||||
public RemoveControl(IPositionProvider positionProvider)
|
||||
{
|
||||
_positionProvider = positionProvider;
|
||||
}
|
||||
|
||||
public override Point? GetPosition(Model model) => _positionProvider.GetPosition(model);
|
||||
|
||||
public override async ValueTask OnPointerDown(Diagram diagram, Model model, PointerEventArgs _)
|
||||
{
|
||||
if (await ShouldDeleteModel(diagram, model).ConfigureAwait(false))
|
||||
{
|
||||
DeleteModel(diagram, model);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeleteModel(Diagram diagram, Model model)
|
||||
{
|
||||
switch (model)
|
||||
{
|
||||
case GroupModel group:
|
||||
diagram.Groups.Delete(group);
|
||||
return;
|
||||
case NodeModel node:
|
||||
diagram.Nodes.Remove(node);
|
||||
return;
|
||||
|
||||
case BaseLinkModel link:
|
||||
diagram.Links.Remove(link);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static async ValueTask<bool> ShouldDeleteModel(Diagram diagram, Model model)
|
||||
{
|
||||
if (model.Locked)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return model switch
|
||||
{
|
||||
GroupModel group => await diagram.Options.Constraints.ShouldDeleteGroup.Invoke(group).ConfigureAwait(false),
|
||||
NodeModel node => await diagram.Options.Constraints.ShouldDeleteNode.Invoke(node).ConfigureAwait(false),
|
||||
BaseLinkModel link => await diagram.Options.Constraints.ShouldDeleteLink.Invoke(link).ConfigureAwait(false),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Events;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Controls;
|
||||
|
||||
public abstract class ExecutableControl : Control
|
||||
{
|
||||
public abstract ValueTask OnPointerDown(Diagram diagram, Model model, PointerEventArgs e);
|
||||
}
|
||||
11
src/Gateway/ThingsGateway.Blazor.Diagrams/Core/Delegates.cs
Normal file
11
src/Gateway/ThingsGateway.Blazor.Diagrams/Core/Delegates.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Anchors;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core;
|
||||
|
||||
public delegate BaseLinkModel? LinkFactory(Diagram diagram, ILinkable source, Anchor targetAnchor);
|
||||
|
||||
public delegate Anchor AnchorFactory(Diagram diagram, BaseLinkModel link, ILinkable model);
|
||||
|
||||
public delegate GroupModel GroupFactory(Diagram diagram, NodeModel[] children);
|
||||
407
src/Gateway/ThingsGateway.Blazor.Diagrams/Core/Diagram.cs
Normal file
407
src/Gateway/ThingsGateway.Blazor.Diagrams/Core/Diagram.cs
Normal file
@@ -0,0 +1,407 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Behaviors;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Controls;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Events;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Extensions;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Layers;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Options;
|
||||
|
||||
[assembly: InternalsVisibleTo("ThingsGateway.Blazor.Diagrams")]
|
||||
[assembly: InternalsVisibleTo("ThingsGateway.Blazor.Diagrams.Tests")]
|
||||
[assembly: InternalsVisibleTo("ThingsGateway.Blazor.Diagrams.Core.Tests")]
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core;
|
||||
|
||||
public abstract class Diagram
|
||||
{
|
||||
private readonly Dictionary<Type, Behavior> _behaviors;
|
||||
private readonly List<SelectableModel> _orderedSelectables;
|
||||
|
||||
public event Action<Model?, PointerEventArgs>? PointerDown;
|
||||
public event Action<Model?, PointerEventArgs>? PointerMove;
|
||||
public event Action<Model?, PointerEventArgs>? PointerUp;
|
||||
public event Action<Model?, PointerEventArgs>? PointerEnter;
|
||||
public event Action<Model?, PointerEventArgs>? PointerLeave;
|
||||
public event Action<KeyboardEventArgs>? KeyDown;
|
||||
public event Action<WheelEventArgs>? Wheel;
|
||||
public event Action<Model?, PointerEventArgs>? PointerClick;
|
||||
public event Action<Model?, PointerEventArgs>? PointerDoubleClick;
|
||||
|
||||
public event Action<SelectableModel>? SelectionChanged;
|
||||
public event Action? PanChanged;
|
||||
public event Action? ZoomChanged;
|
||||
public event Action? ContainerChanged;
|
||||
public event Action? Changed;
|
||||
|
||||
protected Diagram(bool registerDefaultBehaviors = true)
|
||||
{
|
||||
_behaviors = new Dictionary<Type, Behavior>();
|
||||
_orderedSelectables = new List<SelectableModel>();
|
||||
|
||||
Nodes = new NodeLayer(this);
|
||||
Links = new LinkLayer(this);
|
||||
Groups = new GroupLayer(this);
|
||||
Controls = new ControlsLayer();
|
||||
|
||||
Nodes.Added += OnSelectableAdded;
|
||||
Links.Added += OnSelectableAdded;
|
||||
Groups.Added += OnSelectableAdded;
|
||||
|
||||
Nodes.Removed += OnSelectableRemoved;
|
||||
Links.Removed += OnSelectableRemoved;
|
||||
Groups.Removed += OnSelectableRemoved;
|
||||
|
||||
if (!registerDefaultBehaviors)
|
||||
return;
|
||||
|
||||
RegisterBehavior(new SelectionBehavior(this));
|
||||
RegisterBehavior(new DragMovablesBehavior(this));
|
||||
RegisterBehavior(new DragNewLinkBehavior(this));
|
||||
RegisterBehavior(new PanBehavior(this));
|
||||
RegisterBehavior(new ZoomBehavior(this));
|
||||
RegisterBehavior(new EventsBehavior(this));
|
||||
RegisterBehavior(new KeyboardShortcutsBehavior(this));
|
||||
RegisterBehavior(new ControlsBehavior(this));
|
||||
RegisterBehavior(new VirtualizationBehavior(this));
|
||||
}
|
||||
|
||||
public abstract DiagramOptions Options { get; }
|
||||
public NodeLayer Nodes { get; }
|
||||
public LinkLayer Links { get; }
|
||||
public GroupLayer Groups { get; }
|
||||
public ControlsLayer Controls { get; }
|
||||
public Rectangle? Container { get; private set; }
|
||||
public Point Pan { get; private set; } = Point.Zero;
|
||||
public double Zoom { get; private set; } = 1;
|
||||
public bool SuspendRefresh { get; set; }
|
||||
public bool SuspendSorting { get; set; }
|
||||
public IReadOnlyList<SelectableModel> OrderedSelectables => _orderedSelectables;
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
if (SuspendRefresh)
|
||||
return;
|
||||
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
public void Batch(Action action)
|
||||
{
|
||||
if (SuspendRefresh)
|
||||
{
|
||||
// If it's already suspended, just execute the action and leave it suspended
|
||||
// It's probably handled by an outer batch
|
||||
action();
|
||||
return;
|
||||
}
|
||||
|
||||
SuspendRefresh = true;
|
||||
action();
|
||||
SuspendRefresh = false;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
#region Selection
|
||||
|
||||
public IEnumerable<SelectableModel> GetSelectedModels()
|
||||
{
|
||||
foreach (var node in Nodes)
|
||||
{
|
||||
if (node.Selected)
|
||||
yield return node;
|
||||
}
|
||||
|
||||
foreach (var link in Links)
|
||||
{
|
||||
if (link.Selected)
|
||||
yield return link;
|
||||
|
||||
foreach (var vertex in link.Vertices)
|
||||
{
|
||||
if (vertex.Selected)
|
||||
yield return vertex;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var group in Groups)
|
||||
{
|
||||
if (group.Selected)
|
||||
yield return group;
|
||||
}
|
||||
}
|
||||
|
||||
public void SelectModel(SelectableModel model, bool unselectOthers)
|
||||
{
|
||||
if (model.Selected)
|
||||
return;
|
||||
|
||||
if (unselectOthers)
|
||||
UnselectAll();
|
||||
|
||||
model.Selected = true;
|
||||
model.Refresh();
|
||||
SelectionChanged?.Invoke(model);
|
||||
}
|
||||
|
||||
public void UnselectModel(SelectableModel model)
|
||||
{
|
||||
if (!model.Selected)
|
||||
return;
|
||||
|
||||
model.Selected = false;
|
||||
model.Refresh();
|
||||
SelectionChanged?.Invoke(model);
|
||||
}
|
||||
|
||||
public void UnselectAll()
|
||||
{
|
||||
foreach (var model in GetSelectedModels())
|
||||
{
|
||||
model.Selected = false;
|
||||
model.Refresh();
|
||||
// Todo: will result in many events, maybe one event for all of them?
|
||||
SelectionChanged?.Invoke(model);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Behaviors
|
||||
|
||||
public void RegisterBehavior(Behavior behavior)
|
||||
{
|
||||
var type = behavior.GetType();
|
||||
if (_behaviors.ContainsKey(type))
|
||||
throw new Exception($"Behavior '{type.Name}' already registered");
|
||||
|
||||
_behaviors.Add(type, behavior);
|
||||
}
|
||||
|
||||
public T? GetBehavior<T>() where T : Behavior
|
||||
{
|
||||
var type = typeof(T);
|
||||
return (T?)(_behaviors.TryGetValue(type, out Behavior? value) ? value : null);
|
||||
}
|
||||
|
||||
public void UnregisterBehavior<T>() where T : Behavior
|
||||
{
|
||||
var type = typeof(T);
|
||||
if (!_behaviors.TryGetValue(type, out Behavior? value))
|
||||
return;
|
||||
|
||||
value.Dispose();
|
||||
_behaviors.Remove(type);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public void ZoomToFit(double margin = 10)
|
||||
{
|
||||
if (Container == null || Nodes.Count == 0)
|
||||
return;
|
||||
|
||||
var selectedNodes = Nodes.Where(s => s.Selected);
|
||||
var nodesToUse = selectedNodes.Any() ? selectedNodes : Nodes;
|
||||
var bounds = nodesToUse.GetBounds();
|
||||
var width = bounds.Width + 2 * margin;
|
||||
var height = bounds.Height + 2 * margin;
|
||||
var minX = bounds.Left - margin;
|
||||
var minY = bounds.Top - margin;
|
||||
|
||||
SuspendRefresh = true;
|
||||
|
||||
var xf = Container.Width / width;
|
||||
var yf = Container.Height / height;
|
||||
SetZoom(Math.Min(xf, yf));
|
||||
|
||||
var nx = Container.Left + Pan.X + minX * Zoom;
|
||||
var ny = Container.Top + Pan.Y + minY * Zoom;
|
||||
UpdatePan(Container.Left - nx, Container.Top - ny);
|
||||
|
||||
SuspendRefresh = false;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
public void SetPan(double x, double y)
|
||||
{
|
||||
Pan = new Point(x, y);
|
||||
PanChanged?.Invoke();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
public void UpdatePan(double deltaX, double deltaY)
|
||||
{
|
||||
Pan = Pan.Add(deltaX, deltaY);
|
||||
PanChanged?.Invoke();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
public void SetZoom(double newZoom)
|
||||
{
|
||||
if (newZoom <= 0)
|
||||
throw new ArgumentException($"{nameof(newZoom)} cannot be equal or lower than 0");
|
||||
|
||||
if (newZoom < Options.Zoom.Minimum)
|
||||
newZoom = Options.Zoom.Minimum;
|
||||
|
||||
Zoom = newZoom;
|
||||
ZoomChanged?.Invoke();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
public void SetContainer(Rectangle newRect)
|
||||
{
|
||||
if (newRect.Equals(Container))
|
||||
return;
|
||||
|
||||
Container = newRect;
|
||||
ContainerChanged?.Invoke();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
public Point GetRelativeMousePoint(double clientX, double clientY)
|
||||
{
|
||||
if (Container == null)
|
||||
throw new Exception(
|
||||
"Container not available. Make sure you're not using this method before the diagram is fully loaded");
|
||||
|
||||
return new Point((clientX - Container.Left - Pan.X) / Zoom, (clientY - Container.Top - Pan.Y) / Zoom);
|
||||
}
|
||||
|
||||
public Point GetRelativePoint(double clientX, double clientY)
|
||||
{
|
||||
if (Container == null)
|
||||
throw new Exception(
|
||||
"Container not available. Make sure you're not using this method before the diagram is fully loaded");
|
||||
|
||||
return new Point(clientX - Container.Left, clientY - Container.Top);
|
||||
}
|
||||
|
||||
public Point GetScreenPoint(double clientX, double clientY)
|
||||
{
|
||||
if (Container == null)
|
||||
throw new Exception(
|
||||
"Container not available. Make sure you're not using this method before the diagram is fully loaded");
|
||||
|
||||
return new Point(Zoom * clientX + Container.Left + Pan.X, Zoom * clientY + Container.Top + Pan.Y);
|
||||
}
|
||||
|
||||
#region Ordering
|
||||
|
||||
public void SendToBack(SelectableModel model)
|
||||
{
|
||||
var minOrder = GetMinOrder();
|
||||
if (model.Order == minOrder)
|
||||
return;
|
||||
|
||||
if (!_orderedSelectables.Remove(model))
|
||||
return;
|
||||
|
||||
_orderedSelectables.Insert(0, model);
|
||||
|
||||
// Todo: can optimize this by only updating the order of items before model
|
||||
Batch(() =>
|
||||
{
|
||||
SuspendSorting = true;
|
||||
for (var i = 0; i < _orderedSelectables.Count; i++)
|
||||
{
|
||||
_orderedSelectables[i].Order = i + 1;
|
||||
}
|
||||
SuspendSorting = false;
|
||||
});
|
||||
}
|
||||
|
||||
public void SendToFront(SelectableModel model)
|
||||
{
|
||||
var maxOrder = GetMaxOrder();
|
||||
if (model.Order == maxOrder)
|
||||
return;
|
||||
|
||||
if (!_orderedSelectables.Remove(model))
|
||||
return;
|
||||
|
||||
_orderedSelectables.Add(model);
|
||||
|
||||
SuspendSorting = true;
|
||||
model.Order = maxOrder + 1;
|
||||
SuspendSorting = false;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
public int GetMinOrder()
|
||||
{
|
||||
return _orderedSelectables.Count > 0 ? _orderedSelectables[0].Order : 0;
|
||||
}
|
||||
|
||||
public int GetMaxOrder()
|
||||
{
|
||||
return _orderedSelectables.Count > 0 ? _orderedSelectables[^1].Order : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sorts the list of selectables based on their order
|
||||
/// </summary>
|
||||
public void RefreshOrders(bool refresh = true)
|
||||
{
|
||||
_orderedSelectables.Sort((a, b) => a.Order.CompareTo(b.Order));
|
||||
|
||||
if (refresh)
|
||||
{
|
||||
Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSelectableAdded(SelectableModel model)
|
||||
{
|
||||
var maxOrder = GetMaxOrder();
|
||||
_orderedSelectables.Add(model);
|
||||
|
||||
if (model.Order == 0)
|
||||
{
|
||||
model.Order = maxOrder + 1;
|
||||
}
|
||||
|
||||
model.OrderChanged += OnModelOrderChanged;
|
||||
}
|
||||
|
||||
private void OnSelectableRemoved(SelectableModel model)
|
||||
{
|
||||
model.OrderChanged -= OnModelOrderChanged;
|
||||
_orderedSelectables.Remove(model);
|
||||
}
|
||||
|
||||
private void OnModelOrderChanged(Model model)
|
||||
{
|
||||
if (SuspendSorting)
|
||||
return;
|
||||
|
||||
RefreshOrders();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
public void TriggerPointerDown(Model? model, PointerEventArgs e) => PointerDown?.Invoke(model, e);
|
||||
|
||||
public void TriggerPointerMove(Model? model, PointerEventArgs e) => PointerMove?.Invoke(model, e);
|
||||
|
||||
public void TriggerPointerUp(Model? model, PointerEventArgs e) => PointerUp?.Invoke(model, e);
|
||||
|
||||
public void TriggerPointerEnter(Model? model, PointerEventArgs e) => PointerEnter?.Invoke(model, e);
|
||||
|
||||
public void TriggerPointerLeave(Model? model, PointerEventArgs e) => PointerLeave?.Invoke(model, e);
|
||||
|
||||
public void TriggerKeyDown(KeyboardEventArgs e) => KeyDown?.Invoke(e);
|
||||
|
||||
public void TriggerWheel(WheelEventArgs e) => Wheel?.Invoke(e);
|
||||
|
||||
public void TriggerPointerClick(Model? model, PointerEventArgs e) => PointerClick?.Invoke(model, e);
|
||||
|
||||
public void TriggerPointerDoubleClick(Model? model, PointerEventArgs e) => PointerDoubleClick?.Invoke(model, e);
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core;
|
||||
|
||||
public class DiagramsException : Exception
|
||||
{
|
||||
public DiagramsException(string? message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public DiagramsException() : base()
|
||||
{
|
||||
}
|
||||
|
||||
public DiagramsException(string? message, Exception? innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Events;
|
||||
|
||||
public record KeyboardEventArgs(string Key, string Code, float Location, bool CtrlKey, bool ShiftKey, bool AltKey);
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Events;
|
||||
|
||||
public record MouseEventArgs(double ClientX, double ClientY, long Button, long Buttons, bool CtrlKey, bool ShiftKey, bool AltKey);
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Events;
|
||||
|
||||
public record PointerEventArgs(double ClientX, double ClientY, long Button, long Buttons, bool CtrlKey, bool ShiftKey,
|
||||
bool AltKey, long PointerId, float Width, float Height, float Pressure, float TiltX, float TiltY,
|
||||
string PointerType, bool IsPrimary) : MouseEventArgs(ClientX, ClientY, Button, Buttons, CtrlKey, ShiftKey, AltKey);
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Events;
|
||||
|
||||
public record TouchEventArgs(TouchPoint[] ChangedTouches, bool CtrlKey, bool ShiftKey, bool AltKey);
|
||||
public record TouchPoint(long Identifier, double ClientX, double ClientY);
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Events;
|
||||
|
||||
public record WheelEventArgs(
|
||||
double ClientX,
|
||||
double ClientY,
|
||||
long Button,
|
||||
long Buttons,
|
||||
bool CtrlKey,
|
||||
bool ShiftKey,
|
||||
bool AltKey,
|
||||
double DeltaX,
|
||||
double DeltaY,
|
||||
double DeltaZ,
|
||||
long DeltaMode) : MouseEventArgs(ClientX, ClientY, Button, Buttons, CtrlKey, ShiftKey, AltKey);
|
||||
@@ -0,0 +1,46 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Extensions;
|
||||
|
||||
public static class DiagramExtensions
|
||||
{
|
||||
public static Rectangle GetBounds(this IEnumerable<NodeModel> nodes)
|
||||
{
|
||||
if (!nodes.Any())
|
||||
return Rectangle.Zero;
|
||||
|
||||
var minX = double.MaxValue;
|
||||
var maxX = double.MinValue;
|
||||
var minY = double.MaxValue;
|
||||
var maxY = double.MinValue;
|
||||
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
if (node.Size == null) // Ignore nodes that didn't get a size yet
|
||||
continue;
|
||||
|
||||
var trX = node.Position.X + node.Size!.Width;
|
||||
var bY = node.Position.Y + node.Size.Height;
|
||||
|
||||
if (node.Position.X < minX)
|
||||
{
|
||||
minX = node.Position.X;
|
||||
}
|
||||
if (trX > maxX)
|
||||
{
|
||||
maxX = trX;
|
||||
}
|
||||
if (node.Position.Y < minY)
|
||||
{
|
||||
minY = node.Position.Y;
|
||||
}
|
||||
if (bY > maxY)
|
||||
{
|
||||
maxY = bY;
|
||||
}
|
||||
}
|
||||
|
||||
return new Rectangle(minX, minY, maxX, maxY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Extensions;
|
||||
|
||||
public static class DoubleExtensions
|
||||
{
|
||||
public static bool AlmostEqualTo(this double double1, double double2, double tolerance = 0.0001)
|
||||
=> Math.Abs(double1 - double2) < tolerance;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Extensions;
|
||||
|
||||
public static class NumberExtensions
|
||||
{
|
||||
public static string ToInvariantString(this double n) => n.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
|
||||
public static class BezierSpline
|
||||
{
|
||||
/// <summary>
|
||||
/// Get open-ended Bezier Spline Control Points.
|
||||
/// </summary>
|
||||
/// <param name="knots">Input Knot Bezier spline points.</param>
|
||||
/// <param name="firstControlPoints">Output First Control points array of knots.Length - 1 length.</param>
|
||||
/// <param name="secondControlPoints">Output Second Control points array of knots.Length - 1 length.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="knots"/> parameter must be not null.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="knots"/> array must containg at least two points.</exception>
|
||||
public static void GetCurveControlPoints(Point[] knots, out Point[] firstControlPoints, out Point[] secondControlPoints)
|
||||
{
|
||||
if (knots == null)
|
||||
throw new ArgumentNullException(nameof(knots));
|
||||
int n = knots.Length - 1;
|
||||
if (n < 1)
|
||||
throw new ArgumentException("At least two knot points required", nameof(knots));
|
||||
if (n == 1)
|
||||
{ // Special case: Bezier curve should be a straight line.
|
||||
firstControlPoints = new Point[1];
|
||||
// 3P1 = 2P0 + P3
|
||||
firstControlPoints[0] = new Point((2 * knots[0].X + knots[1].X) / 3, (2 * knots[0].Y + knots[1].Y) / 3);
|
||||
|
||||
secondControlPoints = new Point[1];
|
||||
// P2 = 2P1 – P0
|
||||
secondControlPoints[0] = new Point(2 * firstControlPoints[0].X - knots[0].X, 2 * firstControlPoints[0].Y - knots[0].Y);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate first Bezier control points
|
||||
// Right hand side vector
|
||||
double[] rhs = new double[n];
|
||||
|
||||
// Set right hand side X values
|
||||
for (int i = 1; i < n - 1; ++i)
|
||||
rhs[i] = 4 * knots[i].X + 2 * knots[i + 1].X;
|
||||
rhs[0] = knots[0].X + 2 * knots[1].X;
|
||||
rhs[n - 1] = (8 * knots[n - 1].X + knots[n].X) / 2.0;
|
||||
// Get first control points X-values
|
||||
double[] x = GetFirstControlPoints(rhs);
|
||||
|
||||
// Set right hand side Y values
|
||||
for (int i = 1; i < n - 1; ++i)
|
||||
rhs[i] = 4 * knots[i].Y + 2 * knots[i + 1].Y;
|
||||
rhs[0] = knots[0].Y + 2 * knots[1].Y;
|
||||
rhs[n - 1] = (8 * knots[n - 1].Y + knots[n].Y) / 2.0;
|
||||
// Get first control points Y-values
|
||||
double[] y = GetFirstControlPoints(rhs);
|
||||
|
||||
// Fill output arrays.
|
||||
firstControlPoints = new Point[n];
|
||||
secondControlPoints = new Point[n];
|
||||
for (int i = 0; i < n; ++i)
|
||||
{
|
||||
// First control point
|
||||
firstControlPoints[i] = new Point(x[i], y[i]);
|
||||
// Second control point
|
||||
if (i < n - 1)
|
||||
secondControlPoints[i] = new Point(2 * knots[i + 1].X - x[i + 1], 2 * knots[i + 1].Y - y[i + 1]);
|
||||
else
|
||||
secondControlPoints[i] = new Point((knots[n].X + x[n - 1]) / 2, (knots[n].Y + y[n - 1]) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Solves a tridiagonal system for one of coordinates (x or y) of first Bezier control points.
|
||||
/// </summary>
|
||||
/// <param name="rhs">Right hand side vector.</param>
|
||||
/// <returns>Solution vector.</returns>
|
||||
private static double[] GetFirstControlPoints(double[] rhs)
|
||||
{
|
||||
int n = rhs.Length;
|
||||
double[] x = new double[n]; // Solution vector.
|
||||
double[] tmp = new double[n]; // Temp workspace.
|
||||
|
||||
double b = 2.0;
|
||||
x[0] = rhs[0] / b;
|
||||
for (int i = 1; i < n; i++) // Decomposition and forward substitution.
|
||||
{
|
||||
tmp[i] = 1 / b;
|
||||
b = (i < n - 1 ? 4.0 : 3.5) - tmp[i];
|
||||
x[i] = (rhs[i] - x[i - 1]) / b;
|
||||
}
|
||||
for (int i = 1; i < n; i++)
|
||||
x[n - i - 1] -= tmp[n - i] * x[n - i]; // Backsubstitution.
|
||||
|
||||
return x;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
|
||||
public class Ellipse : IShape
|
||||
{
|
||||
public Ellipse(double cx, double cy, double rx, double ry)
|
||||
{
|
||||
Cx = cx;
|
||||
Cy = cy;
|
||||
Rx = rx;
|
||||
Ry = ry;
|
||||
}
|
||||
|
||||
public double Cx { get; }
|
||||
public double Cy { get; }
|
||||
public double Rx { get; }
|
||||
public double Ry { get; }
|
||||
|
||||
public IEnumerable<Point> GetIntersectionsWithLine(Line line)
|
||||
{
|
||||
var a1 = line.Start;
|
||||
var a2 = line.End;
|
||||
var dir = new Point(line.End.X - line.Start.X, line.End.Y - line.Start.Y);
|
||||
var diff = a1.Subtract(Cx, Cy);
|
||||
var mDir = new Point(dir.X / (Rx * Rx), dir.Y / (Ry * Ry));
|
||||
var mDiff = new Point(diff.X / (Rx * Rx), diff.Y / (Ry * Ry));
|
||||
|
||||
var a = dir.Dot(mDir);
|
||||
var b = dir.Dot(mDiff);
|
||||
var c = diff.Dot(mDiff) - 1.0;
|
||||
var d = b * b - a * c;
|
||||
|
||||
if (d > 0)
|
||||
{
|
||||
var root = Math.Sqrt(d);
|
||||
var ta = (-b - root) / a;
|
||||
var tb = (-b + root) / a;
|
||||
|
||||
if (ta >= 0 && 1 >= ta || tb >= 0 && 1 >= tb)
|
||||
{
|
||||
if (0 <= ta && ta <= 1)
|
||||
yield return a1.Lerp(a2, ta);
|
||||
|
||||
if (0 <= tb && tb <= 1)
|
||||
yield return a1.Lerp(a2, tb);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var t = -b / a;
|
||||
if (0 <= t && t <= 1)
|
||||
{
|
||||
yield return a1.Lerp(a2, t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Point? GetPointAtAngle(double a)
|
||||
{
|
||||
var t = Math.Tan(a / 360 * Math.PI);
|
||||
var px = Rx * (1 - Math.Pow(t, 2)) / (1 + Math.Pow(t, 2));
|
||||
var py = Ry * 2 * t / (1 + Math.Pow(t, 2));
|
||||
return new Point(Cx + px, Cy + py);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
|
||||
public interface IShape
|
||||
{
|
||||
public IEnumerable<Point> GetIntersectionsWithLine(Line line);
|
||||
public Point? GetPointAtAngle(double a);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
|
||||
public class Line
|
||||
{
|
||||
public Line(Point start, Point end)
|
||||
{
|
||||
Start = start;
|
||||
End = end;
|
||||
}
|
||||
|
||||
public Point Start { get; }
|
||||
public Point End { get; }
|
||||
|
||||
public Point? GetIntersection(Line line)
|
||||
{
|
||||
var pt1Dir = new Point(End.X - Start.X, End.Y - Start.Y);
|
||||
var pt2Dir = new Point(line.End.X - line.Start.X, line.End.Y - line.Start.Y);
|
||||
var det = (pt1Dir.X * pt2Dir.Y) - (pt1Dir.Y * pt2Dir.X);
|
||||
var deltaPt = new Point(line.Start.X - Start.X, line.Start.Y - Start.Y);
|
||||
var alpha = (deltaPt.X * pt2Dir.Y) - (deltaPt.Y * pt2Dir.X);
|
||||
var beta = (deltaPt.X * pt1Dir.Y) - (deltaPt.Y * pt1Dir.X);
|
||||
|
||||
if (det == 0 || alpha * det < 0 || beta * det < 0)
|
||||
return null;
|
||||
|
||||
if (det > 0)
|
||||
{
|
||||
if (alpha > det || beta > det)
|
||||
return null;
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
if (alpha < det || beta < det)
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Point(Start.X + (alpha * pt1Dir.X / det), Start.Y + (alpha * pt1Dir.Y / det));
|
||||
}
|
||||
|
||||
public override string ToString() => $"Line from {Start} to {End}";
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
|
||||
public record Point
|
||||
{
|
||||
public static Point Zero { get; } = new(0, 0);
|
||||
|
||||
public Point(double x, double y)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
}
|
||||
|
||||
public double X { get; init; }
|
||||
public double Y { get; init; }
|
||||
|
||||
public double Length => Math.Sqrt(Dot(this));
|
||||
|
||||
public double Dot(Point other) => X * other.X + Y * other.Y;
|
||||
public Point Lerp(Point other, double t)
|
||||
=> new(X * (1.0 - t) + other.X * t, Y * (1.0 - t) + other.Y * t);
|
||||
|
||||
public Point Add(double value) => new(X + value, Y + value);
|
||||
public Point Add(double x, double y) => new(X + x, Y + y);
|
||||
|
||||
public Point Subtract(double value) => new(X - value, Y - value);
|
||||
public Point Subtract(double x, double y) => new(X - x, Y - y);
|
||||
public Point Subtract(Point other) => new(X - other.X, Y - other.Y);
|
||||
|
||||
public Point Divide(Point other) => new(X / other.X, Y / other.Y);
|
||||
|
||||
public Point Multiply(double value) => new(X * value, Y * value);
|
||||
|
||||
public Point Normalize()
|
||||
{
|
||||
var length = Length;
|
||||
return new Point(X / length, Y / length);
|
||||
}
|
||||
|
||||
public double DistanceTo(Point other) => Math.Sqrt(Math.Pow(X - other.X, 2) + Math.Pow(Y - other.Y, 2));
|
||||
public double DistanceTo(double x, double y) => Math.Sqrt(Math.Pow(X - x, 2) + Math.Pow(Y - y, 2));
|
||||
|
||||
public Point MoveAlongLine(Point from, double dist)
|
||||
{
|
||||
var x = X - from.X;
|
||||
var y = Y - from.Y;
|
||||
var angle = Math.Atan2(y, x);
|
||||
var xOffset = Math.Cos(angle) * dist;
|
||||
var yOffset = Math.Sin(angle) * dist;
|
||||
return new Point(X + xOffset, Y + yOffset);
|
||||
}
|
||||
|
||||
public static Point operator -(Point a, Point b) => new(a.X - b.X, a.Y - b.Y);
|
||||
public static Point operator +(Point a, Point b) => new(a.X + b.X, a.Y + b.Y);
|
||||
|
||||
public void Deconstruct(out double x, out double y)
|
||||
{
|
||||
x = X;
|
||||
y = Y;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
|
||||
public class Rectangle : IShape
|
||||
{
|
||||
public static Rectangle Zero { get; } = new(0, 0, 0, 0);
|
||||
|
||||
public double Width { get; }
|
||||
public double Height { get; }
|
||||
public double Top { get; }
|
||||
public double Right { get; }
|
||||
public double Bottom { get; }
|
||||
public double Left { get; }
|
||||
|
||||
[JsonConstructor]
|
||||
public Rectangle(double left, double top, double right, double bottom)
|
||||
{
|
||||
Left = left;
|
||||
Top = top;
|
||||
Right = right;
|
||||
Bottom = bottom;
|
||||
Width = Math.Abs(Left - Right);
|
||||
Height = Math.Abs(Top - Bottom);
|
||||
}
|
||||
|
||||
public Rectangle(Point position, Size size)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(position, nameof(position));
|
||||
ArgumentNullException.ThrowIfNull(size, nameof(size));
|
||||
|
||||
Left = position.X;
|
||||
Top = position.Y;
|
||||
Right = Left + size.Width;
|
||||
Bottom = Top + size.Height;
|
||||
Width = size.Width;
|
||||
Height = size.Height;
|
||||
}
|
||||
|
||||
public bool Overlap(Rectangle r)
|
||||
=> Left < r.Right && Right > r.Left && Top < r.Bottom && Bottom > r.Top;
|
||||
|
||||
public bool Intersects(Rectangle r)
|
||||
{
|
||||
var thisX = Left;
|
||||
var thisY = Top;
|
||||
var thisW = Width;
|
||||
var thisH = Height;
|
||||
var rectX = r.Left;
|
||||
var rectY = r.Top;
|
||||
var rectW = r.Width;
|
||||
var rectH = r.Height;
|
||||
return rectX < thisX + thisW && thisX < rectX + rectW && rectY < thisY + thisH && thisY < rectY + rectH;
|
||||
}
|
||||
|
||||
public Rectangle Inflate(double horizontal, double vertical)
|
||||
=> new(Left - horizontal, Top - vertical, Right + horizontal, Bottom + vertical);
|
||||
|
||||
public Rectangle Union(Rectangle r)
|
||||
{
|
||||
var x1 = Math.Min(Left, r.Left);
|
||||
var x2 = Math.Max(Left + Width, r.Left + r.Width);
|
||||
var y1 = Math.Min(Top, r.Top);
|
||||
var y2 = Math.Max(Top + Height, r.Top + r.Height);
|
||||
return new(x1, y1, x2, y2);
|
||||
}
|
||||
|
||||
public bool ContainsPoint(Point point) => ContainsPoint(point.X, point.Y);
|
||||
|
||||
public bool ContainsPoint(double x, double y)
|
||||
=> x >= Left && x <= Right && y >= Top && y <= Bottom;
|
||||
|
||||
public IEnumerable<Point> GetIntersectionsWithLine(Line line)
|
||||
{
|
||||
var borders = new[] {
|
||||
new Line(NorthWest, NorthEast),
|
||||
new Line(NorthEast, SouthEast),
|
||||
new Line(SouthWest, SouthEast),
|
||||
new Line(NorthWest, SouthWest)
|
||||
};
|
||||
|
||||
for (var i = 0; i < borders.Length; i++)
|
||||
{
|
||||
var intersectionPt = borders[i].GetIntersection(line);
|
||||
if (intersectionPt != null)
|
||||
yield return intersectionPt;
|
||||
}
|
||||
}
|
||||
|
||||
public Point? GetPointAtAngle(double a)
|
||||
{
|
||||
var vx = Math.Cos(a * Math.PI / 180);
|
||||
var vy = Math.Sin(a * Math.PI / 180);
|
||||
var px = Left + Width / 2;
|
||||
var py = Top + Height / 2;
|
||||
double? t1 = (Left - px) / vx; // left
|
||||
double? t2 = (Right - px) / vx; // right
|
||||
double? t3 = (Top - py) / vy; // top
|
||||
double? t4 = (Bottom - py) / vy; // bottom
|
||||
var t = (new[] { t1, t2, t3, t4 }).Where(n => n.HasValue && double.IsFinite(n.Value) && n.Value > 0).DefaultIfEmpty(null).Min();
|
||||
if (t == null) return null;
|
||||
|
||||
var x = px + t.Value * vx;
|
||||
var y = py + t.Value * vy;
|
||||
return new Point(x, y);
|
||||
}
|
||||
|
||||
public Point Center => new(Left + Width / 2, Top + Height / 2);
|
||||
public Point NorthEast => new(Right, Top);
|
||||
public Point SouthEast => new(Right, Bottom);
|
||||
public Point SouthWest => new(Left, Bottom);
|
||||
public Point NorthWest => new(Left, Top);
|
||||
public Point East => new(Right, Top + Height / 2);
|
||||
public Point North => new(Left + Width / 2, Top);
|
||||
public Point South => new(Left + Width / 2, Bottom);
|
||||
public Point West => new(Left, Top + Height / 2);
|
||||
|
||||
public bool Equals(Rectangle? other)
|
||||
{
|
||||
return other != null && Left == other.Left && Right == other.Right && Top == other.Top &&
|
||||
Bottom == other.Bottom && Width == other.Width && Height == other.Height;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
=> $"Rectangle(width={Width}, height={Height}, top={Top}, right={Right}, bottom={Bottom}, left={Left})";
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
|
||||
public static class Shapes
|
||||
{
|
||||
public static IShape Rectangle(NodeModel node) => Rectangle(node.Position, node.Size!);
|
||||
|
||||
public static IShape Circle(NodeModel node) => Circle(node.Position, node.Size!);
|
||||
|
||||
public static IShape Ellipse(NodeModel node) => Ellipse(node.Position, node.Size!);
|
||||
|
||||
public static IShape Rectangle(PortModel port) => Rectangle(port.Position, port.Size!);
|
||||
|
||||
public static IShape Circle(PortModel port) => Circle(port.Position, port.Size!);
|
||||
|
||||
public static IShape Ellipse(PortModel port) => Ellipse(port.Position, port.Size!);
|
||||
|
||||
private static Rectangle Rectangle(Point position, Size size) => new Rectangle(position, size);
|
||||
|
||||
private static Ellipse Circle(Point position, Size size)
|
||||
{
|
||||
var halfWidth = size.Width / 2;
|
||||
var centerX = position.X + halfWidth;
|
||||
var centerY = position.Y + size.Height / 2;
|
||||
return new Ellipse(centerX, centerY, halfWidth, halfWidth);
|
||||
}
|
||||
|
||||
private static Ellipse Ellipse(Point position, Size size)
|
||||
{
|
||||
var halfWidth = size.Width / 2;
|
||||
var halfHeight = size.Height / 2;
|
||||
var centerX = position.X + halfWidth;
|
||||
var centerY = position.Y + halfHeight;
|
||||
return new Ellipse(centerX, centerY, halfWidth, halfHeight);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
|
||||
public record Size
|
||||
{
|
||||
public static Size Zero { get; } = new(0, 0);
|
||||
|
||||
public Size(double width, double height)
|
||||
{
|
||||
Width = width;
|
||||
Height = height;
|
||||
}
|
||||
|
||||
public double Width { get; init; }
|
||||
public double Height { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
using System.Collections;
|
||||
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core;
|
||||
|
||||
public abstract class BaseLayer<T> : IReadOnlyList<T> where T : Model
|
||||
{
|
||||
private readonly List<T> _items = new List<T>();
|
||||
|
||||
public event Action<T>? Added;
|
||||
public event Action<T>? Removed;
|
||||
|
||||
public BaseLayer(Diagram diagram)
|
||||
{
|
||||
Diagram = diagram;
|
||||
}
|
||||
|
||||
public virtual TSpecific Add<TSpecific>(TSpecific item) where TSpecific : T
|
||||
{
|
||||
if (item is null)
|
||||
throw new ArgumentNullException(nameof(item));
|
||||
|
||||
Diagram.Batch(() =>
|
||||
{
|
||||
_items.Add(item);
|
||||
OnItemAdded(item);
|
||||
Added?.Invoke(item);
|
||||
});
|
||||
return item;
|
||||
}
|
||||
|
||||
public virtual void Add(IEnumerable<T> items)
|
||||
{
|
||||
if (items is null)
|
||||
throw new ArgumentNullException(nameof(items));
|
||||
|
||||
Diagram.Batch(() =>
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
_items.Add(item);
|
||||
OnItemAdded(item);
|
||||
Added?.Invoke(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public virtual void Remove(T item)
|
||||
{
|
||||
if (item is null)
|
||||
throw new ArgumentNullException(nameof(item));
|
||||
|
||||
if (_items.Remove(item))
|
||||
{
|
||||
Diagram.Batch(() =>
|
||||
{
|
||||
OnItemRemoved(item);
|
||||
Removed?.Invoke(item);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Remove(IEnumerable<T> items)
|
||||
{
|
||||
if (items is null)
|
||||
throw new ArgumentNullException(nameof(items));
|
||||
|
||||
Diagram.Batch(() =>
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (_items.Remove(item))
|
||||
{
|
||||
OnItemRemoved(item);
|
||||
Removed?.Invoke(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public bool Contains(T item) => _items.Contains(item);
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
if (Count == 0)
|
||||
return;
|
||||
|
||||
Diagram.Batch(() =>
|
||||
{
|
||||
for (var i = _items.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var item = _items[i];
|
||||
_items.RemoveAt(i);
|
||||
OnItemRemoved(item);
|
||||
Removed?.Invoke(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected virtual void OnItemAdded(T item) { }
|
||||
|
||||
protected virtual void OnItemRemoved(T item) { }
|
||||
|
||||
public Diagram Diagram { get; }
|
||||
|
||||
public int Count => _items.Count;
|
||||
public T this[int index] => _items[index];
|
||||
public IEnumerator<T> GetEnumerator() => _items.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => _items.GetEnumerator();
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Layers;
|
||||
|
||||
public class GroupLayer : BaseLayer<GroupModel>
|
||||
{
|
||||
public GroupLayer(Diagram diagram) : base(diagram)
|
||||
{
|
||||
}
|
||||
|
||||
public GroupModel Group(params NodeModel[] children)
|
||||
{
|
||||
return Add(Diagram.Options.Groups.Factory(Diagram, children));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the group AND its children
|
||||
/// </summary>
|
||||
public void Delete(GroupModel group)
|
||||
{
|
||||
Diagram.Batch(() =>
|
||||
{
|
||||
var children = group.Children.ToArray();
|
||||
|
||||
Remove(group);
|
||||
|
||||
foreach (var child in children)
|
||||
{
|
||||
if (child is GroupModel g)
|
||||
{
|
||||
Delete(g);
|
||||
}
|
||||
else
|
||||
{
|
||||
Diagram.Nodes.Remove(child);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void OnItemRemoved(GroupModel group)
|
||||
{
|
||||
Diagram.Links.Remove(group.PortLinks.ToArray());
|
||||
Diagram.Links.Remove(group.Links.ToArray());
|
||||
group.Ungroup();
|
||||
group.Group?.RemoveChild(group);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Anchors;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Layers;
|
||||
|
||||
public class LinkLayer : BaseLayer<BaseLinkModel>
|
||||
{
|
||||
public LinkLayer(Diagram diagram) : base(diagram) { }
|
||||
|
||||
protected override void OnItemAdded(BaseLinkModel link)
|
||||
{
|
||||
link.Diagram = Diagram;
|
||||
HandleAnchor(link, link.Source, true);
|
||||
HandleAnchor(link, link.Target, true);
|
||||
link.Refresh();
|
||||
|
||||
link.SourceChanged += OnLinkSourceChanged;
|
||||
link.TargetChanged += OnLinkTargetChanged;
|
||||
}
|
||||
|
||||
protected override void OnItemRemoved(BaseLinkModel link)
|
||||
{
|
||||
link.Diagram = null;
|
||||
HandleAnchor(link, link.Source, false);
|
||||
HandleAnchor(link, link.Target, false);
|
||||
link.Refresh();
|
||||
|
||||
link.SourceChanged -= OnLinkSourceChanged;
|
||||
link.TargetChanged -= OnLinkTargetChanged;
|
||||
|
||||
Diagram.Controls.RemoveFor(link);
|
||||
Remove(link.Links.ToList());
|
||||
}
|
||||
|
||||
private static void OnLinkSourceChanged(BaseLinkModel link, Anchor old, Anchor @new)
|
||||
{
|
||||
HandleAnchor(link, old, add: false);
|
||||
HandleAnchor(link, @new, add: true);
|
||||
}
|
||||
|
||||
private static void OnLinkTargetChanged(BaseLinkModel link, Anchor old, Anchor @new)
|
||||
{
|
||||
HandleAnchor(link, old, add: false);
|
||||
HandleAnchor(link, @new, add: true);
|
||||
}
|
||||
|
||||
private static void HandleAnchor(BaseLinkModel link, Anchor anchor, bool add)
|
||||
{
|
||||
if (add)
|
||||
{
|
||||
anchor.Model?.AddLink(link);
|
||||
}
|
||||
else
|
||||
{
|
||||
anchor.Model?.RemoveLink(link);
|
||||
}
|
||||
|
||||
if (anchor.Model is Model model)
|
||||
{
|
||||
model.Refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Layers;
|
||||
|
||||
public class NodeLayer : BaseLayer<NodeModel>
|
||||
{
|
||||
public NodeLayer(Diagram diagram) : base(diagram) { }
|
||||
|
||||
protected override void OnItemRemoved(NodeModel node)
|
||||
{
|
||||
Diagram.Links.Remove(node.PortLinks.ToList());
|
||||
Diagram.Links.Remove(node.Links.ToList());
|
||||
node.Group?.RemoveChild(node);
|
||||
Diagram.Controls.RemoveFor(node);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Anchors;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Algorithms;
|
||||
|
||||
public static class LinksReconnectionAlgorithms
|
||||
{
|
||||
public static void ReconnectLinksToClosestPorts(this Diagram diagram)
|
||||
{
|
||||
// Only refresh ports once
|
||||
var modelsToRefresh = new HashSet<Model>();
|
||||
|
||||
foreach (var link in diagram.Links.ToArray())
|
||||
{
|
||||
if (link.Source is not SinglePortAnchor spa1 || link.Target is not SinglePortAnchor spa2)
|
||||
continue;
|
||||
|
||||
var sourcePorts = spa1.Port.Parent.Ports;
|
||||
var targetPorts = spa2.Port.Parent.Ports;
|
||||
|
||||
// Find the ports with minimal distance
|
||||
var minDistance = double.MaxValue;
|
||||
var minSourcePort = spa1.Port;
|
||||
var minTargetPort = spa2.Port;
|
||||
foreach (var sourcePort in sourcePorts)
|
||||
{
|
||||
foreach (var targetPort in targetPorts)
|
||||
{
|
||||
var distance = sourcePort.Position.DistanceTo(targetPort.Position);
|
||||
if (distance < minDistance)
|
||||
{
|
||||
minDistance = distance;
|
||||
minSourcePort = sourcePort;
|
||||
minTargetPort = targetPort;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reconnect
|
||||
if (spa1.Port != minSourcePort)
|
||||
{
|
||||
modelsToRefresh.Add(spa1.Port);
|
||||
modelsToRefresh.Add(minSourcePort);
|
||||
link.SetSource(new SinglePortAnchor(minSourcePort));
|
||||
modelsToRefresh.Add(link);
|
||||
}
|
||||
|
||||
if (spa2.Port != minTargetPort)
|
||||
{
|
||||
modelsToRefresh.Add(spa2.Port);
|
||||
modelsToRefresh.Add(minTargetPort);
|
||||
link.SetTarget(new SinglePortAnchor(minTargetPort));
|
||||
modelsToRefresh.Add(link);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var model in modelsToRefresh)
|
||||
{
|
||||
model.Refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Anchors;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.PathGenerators;
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Routers;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
|
||||
public abstract class BaseLinkModel : SelectableModel, IHasBounds, ILinkable
|
||||
{
|
||||
private readonly List<BaseLinkModel> _links = new();
|
||||
|
||||
public event Action<BaseLinkModel, Anchor, Anchor>? SourceChanged;
|
||||
public event Action<BaseLinkModel, Anchor, Anchor>? TargetChanged;
|
||||
public event Action<BaseLinkModel>? TargetAttached;
|
||||
|
||||
protected BaseLinkModel(Anchor source, Anchor target)
|
||||
{
|
||||
Source = source;
|
||||
Target = target;
|
||||
}
|
||||
|
||||
protected BaseLinkModel(string id, Anchor source, Anchor target) : base(id)
|
||||
{
|
||||
Source = source;
|
||||
Target = target;
|
||||
}
|
||||
|
||||
public Anchor Source { get; private set; }
|
||||
public Anchor Target { get; private set; }
|
||||
public Diagram? Diagram { get; internal set; }
|
||||
public Point[]? Route { get; private set; }
|
||||
public PathGeneratorResult? PathGeneratorResult { get; private set; }
|
||||
public bool IsAttached => Source is not PositionAnchor && Target is not PositionAnchor;
|
||||
public Router? Router { get; set; }
|
||||
public PathGenerator? PathGenerator { get; set; }
|
||||
public LinkMarker? SourceMarker { get; set; }
|
||||
public LinkMarker? TargetMarker { get; set; }
|
||||
public bool Segmentable { get; set; } = false;
|
||||
public List<LinkVertexModel> Vertices { get; } = new();
|
||||
public List<LinkLabelModel> Labels { get; } = new();
|
||||
public IReadOnlyList<BaseLinkModel> Links => _links;
|
||||
|
||||
public override void Refresh()
|
||||
{
|
||||
GeneratePath();
|
||||
base.Refresh();
|
||||
}
|
||||
|
||||
public void RefreshLinks()
|
||||
{
|
||||
foreach (var link in Links)
|
||||
{
|
||||
link.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public LinkLabelModel AddLabel(string content, double? distance = null, Point? offset = null)
|
||||
{
|
||||
var label = new LinkLabelModel(this, content, distance, offset);
|
||||
Labels.Add(label);
|
||||
return label;
|
||||
}
|
||||
|
||||
public LinkVertexModel AddVertex(Point? position = null)
|
||||
{
|
||||
var vertex = new LinkVertexModel(this, position);
|
||||
Vertices.Add(vertex);
|
||||
return vertex;
|
||||
}
|
||||
|
||||
public void SetSource(Anchor anchor)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(anchor, nameof(anchor));
|
||||
|
||||
if (Source == anchor)
|
||||
return;
|
||||
|
||||
var old = Source;
|
||||
Source = anchor;
|
||||
SourceChanged?.Invoke(this, old, Source);
|
||||
}
|
||||
|
||||
public void SetTarget(Anchor anchor)
|
||||
{
|
||||
if (Target == anchor)
|
||||
return;
|
||||
|
||||
var old = Target;
|
||||
Target = anchor;
|
||||
TargetChanged?.Invoke(this, old, Target);
|
||||
}
|
||||
|
||||
public Rectangle? GetBounds()
|
||||
{
|
||||
if (PathGeneratorResult == null)
|
||||
return null;
|
||||
|
||||
var minX = double.PositiveInfinity;
|
||||
var minY = double.PositiveInfinity;
|
||||
var maxX = double.NegativeInfinity;
|
||||
var maxY = double.NegativeInfinity;
|
||||
|
||||
var path = PathGeneratorResult.FullPath;
|
||||
var bbox = path.GetBBox();
|
||||
minX = Math.Min(minX, bbox.Left);
|
||||
minY = Math.Min(minY, bbox.Top);
|
||||
maxX = Math.Max(maxX, bbox.Right);
|
||||
maxY = Math.Max(maxY, bbox.Bottom);
|
||||
|
||||
return new Rectangle(minX, minY, maxX, maxY);
|
||||
}
|
||||
|
||||
public bool CanAttachTo(ILinkable other) => true;
|
||||
|
||||
/// <summary>
|
||||
/// Triggers the TargetAttached event
|
||||
/// </summary>
|
||||
public void TriggerTargetAttached() => TargetAttached?.Invoke(this);
|
||||
|
||||
private void GeneratePath()
|
||||
{
|
||||
if (Diagram != null)
|
||||
{
|
||||
var router = Router ?? Diagram.Options.Links.DefaultRouter;
|
||||
var pathGenerator = PathGenerator ?? Diagram.Options.Links.DefaultPathGenerator;
|
||||
var route = router.GetRoute(Diagram, this);
|
||||
var source = Source.GetPosition(this, route);
|
||||
var target = Target.GetPosition(this, route);
|
||||
if (source != null && target != null)
|
||||
{
|
||||
Route = route;
|
||||
PathGeneratorResult = pathGenerator.GetResult(Diagram, this, route, source, target);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Route = null;
|
||||
PathGeneratorResult = null;
|
||||
}
|
||||
|
||||
void ILinkable.AddLink(BaseLinkModel link) => _links.Add(link);
|
||||
|
||||
void ILinkable.RemoveLink(BaseLinkModel link) => _links.Remove(link);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
|
||||
public interface IHasBounds
|
||||
{
|
||||
public Rectangle? GetBounds();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
|
||||
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
|
||||
public interface IHasShape
|
||||
{
|
||||
public IShape GetShape();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace ThingsGateway.Blazor.Diagrams.Core.Models.Base;
|
||||
|
||||
public interface ILinkable
|
||||
{
|
||||
public IReadOnlyList<BaseLinkModel> Links { get; }
|
||||
|
||||
public bool CanAttachTo(ILinkable other);
|
||||
|
||||
internal void AddLink(BaseLinkModel link);
|
||||
|
||||
internal void RemoveLink(BaseLinkModel link);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user