@@ -0,0 +1,6 @@
|
|||||||
|
using AipsCore.Application.Abstract.MessageBroking;
|
||||||
|
using AipsCore.Application.Models.Whiteboard.Command.UserCanceledRequestToJoin;
|
||||||
|
|
||||||
|
namespace AipsCore.Application.Common.Message.UserCanceledRequestToJoin;
|
||||||
|
|
||||||
|
public record UserCanceledRequestToJoinMessage(UserCanceledRequestToJoinCommand Command): IMessage;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using AipsCore.Application.Abstract;
|
||||||
|
using AipsCore.Application.Abstract.MessageBroking;
|
||||||
|
|
||||||
|
namespace AipsCore.Application.Common.Message.UserCanceledRequestToJoin;
|
||||||
|
|
||||||
|
public class UserCanceledRequestToJoinMessageHandler : IMessageHandler<UserCanceledRequestToJoinMessage>
|
||||||
|
{
|
||||||
|
private readonly IDispatcher _dispatcher;
|
||||||
|
|
||||||
|
public UserCanceledRequestToJoinMessageHandler(IDispatcher dispatcher)
|
||||||
|
{
|
||||||
|
_dispatcher = dispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Handle(UserCanceledRequestToJoinMessage message, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _dispatcher.Execute(message.Command, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
using AipsCore.Application.Abstract.Command;
|
||||||
|
|
||||||
|
namespace AipsCore.Application.Models.Whiteboard.Command.UserCanceledRequestToJoin;
|
||||||
|
|
||||||
|
public record UserCanceledRequestToJoinCommand(string WhiteboardId, string UserId): ICommand;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using AipsCore.Application.Abstract.Command;
|
||||||
|
using AipsCore.Domain.Abstract;
|
||||||
|
using AipsCore.Domain.Common.Validation;
|
||||||
|
using AipsCore.Domain.Models.User.ValueObjects;
|
||||||
|
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
|
||||||
|
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
|
||||||
|
using AipsCore.Domain.Models.WhiteboardMembership.External;
|
||||||
|
using AipsCore.Domain.Models.WhiteboardMembership.Validation;
|
||||||
|
|
||||||
|
namespace AipsCore.Application.Models.Whiteboard.Command.UserCanceledRequestToJoin;
|
||||||
|
|
||||||
|
public class CancelJoinRequestCommandHandler : ICommandHandler<UserCanceledRequestToJoinCommand>
|
||||||
|
{
|
||||||
|
private readonly IWhiteboardMembershipRepository _whiteboardMembershipRepository;
|
||||||
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
|
||||||
|
public CancelJoinRequestCommandHandler(IWhiteboardMembershipRepository whiteboardMembershipRepository, IUnitOfWork unitOfWork)
|
||||||
|
{
|
||||||
|
_whiteboardMembershipRepository = whiteboardMembershipRepository;
|
||||||
|
_unitOfWork = unitOfWork;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Handle(UserCanceledRequestToJoinCommand command, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var whiteboardId = new WhiteboardId(command.WhiteboardId);
|
||||||
|
var userId = new UserId(command.UserId);
|
||||||
|
|
||||||
|
var membership = await _whiteboardMembershipRepository.GetByWhiteboardAndUserAsync(whiteboardId, userId, cancellationToken);
|
||||||
|
|
||||||
|
if (membership is null)
|
||||||
|
{
|
||||||
|
throw new ValidationException(WhiteboardMembershipErrors.NotFound(whiteboardId, userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
membership.UpdateStatus(WhiteboardMembershipStatus.Cancelled);
|
||||||
|
|
||||||
|
await _whiteboardMembershipRepository.SaveAsync(membership, cancellationToken);
|
||||||
|
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ using AipsCore.Application.Abstract;
|
|||||||
using AipsCore.Application.Models.Shape.Command.MoveShape;
|
using AipsCore.Application.Models.Shape.Command.MoveShape;
|
||||||
using AipsCore.Application.Models.Whiteboard.Command.AcceptUserRequestToJoin;
|
using AipsCore.Application.Models.Whiteboard.Command.AcceptUserRequestToJoin;
|
||||||
using AipsCore.Application.Models.Whiteboard.Command.RejectUserRequestToJoin;
|
using AipsCore.Application.Models.Whiteboard.Command.RejectUserRequestToJoin;
|
||||||
|
using AipsCore.Application.Models.Whiteboard.Command.UserCanceledRequestToJoin;
|
||||||
using AipsCore.Application.Models.Whiteboard.Query.GetMembershipStatus;
|
using AipsCore.Application.Models.Whiteboard.Query.GetMembershipStatus;
|
||||||
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
|
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
|
||||||
using AipsRT.Model.Memberships;
|
using AipsRT.Model.Memberships;
|
||||||
@@ -21,14 +22,14 @@ public class WhiteboardHub : Hub
|
|||||||
private readonly WhiteboardManager _whiteboardManager;
|
private readonly WhiteboardManager _whiteboardManager;
|
||||||
private readonly IMessagingService _messagingService;
|
private readonly IMessagingService _messagingService;
|
||||||
private readonly MembershipService _membershipService;
|
private readonly MembershipService _membershipService;
|
||||||
private readonly GetUserService _getUserService;
|
private readonly UserService _userService;
|
||||||
|
|
||||||
public WhiteboardHub(WhiteboardManager whiteboardManager, IMessagingService messagingService, MembershipService membershipService, GetUserService getUserService)
|
public WhiteboardHub(WhiteboardManager whiteboardManager, IMessagingService messagingService, MembershipService membershipService, UserService userService)
|
||||||
{
|
{
|
||||||
_whiteboardManager = whiteboardManager;
|
_whiteboardManager = whiteboardManager;
|
||||||
_messagingService = messagingService;
|
_messagingService = messagingService;
|
||||||
_membershipService = membershipService;
|
_membershipService = membershipService;
|
||||||
_getUserService = getUserService;
|
_userService = userService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task OnDisconnectedAsync(Exception? exception)
|
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||||
@@ -107,7 +108,7 @@ public class WhiteboardHub : Hub
|
|||||||
|
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
user = await _getUserService.GetUser(userId);
|
user = await _userService.GetUser(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Clients.User(ownerId.ToString()).SendAsync("UserWaitingForApproval", user);
|
await Clients.User(ownerId.ToString()).SendAsync("UserWaitingForApproval", user);
|
||||||
@@ -147,6 +148,7 @@ public class WhiteboardHub : Hub
|
|||||||
|
|
||||||
if (whiteboard != null)
|
if (whiteboard != null)
|
||||||
{
|
{
|
||||||
|
await _messagingService.CancelJoinRequest(new UserCanceledRequestToJoinCommand(whiteboard.WhiteboardId.ToString(), userId.ToString()));
|
||||||
await Clients.User(whiteboard.OwnerId.ToString()).SendAsync("UserCanceledJoinRequest", userId.ToString());
|
await Clients.User(whiteboard.OwnerId.ToString()).SendAsync("UserCanceledJoinRequest", userId.ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
using AipsCore.Application.Abstract;
|
using AipsCore.Application.Abstract;
|
||||||
using AipsCore.Application.Models.User.Query.GetUser;
|
using AipsCore.Application.Models.User.Query.GetUser;
|
||||||
|
using AipsCore.Application.Models.Whiteboard.Command.UserCanceledRequestToJoin;
|
||||||
|
|
||||||
namespace AipsRT.Model.Users;
|
namespace AipsRT.Model.Users;
|
||||||
|
|
||||||
public class GetUserService
|
public class UserService
|
||||||
{
|
{
|
||||||
private readonly IDispatcher _dispatcher;
|
private readonly IDispatcher _dispatcher;
|
||||||
|
|
||||||
public GetUserService(IDispatcher dispatcher)
|
public UserService(IDispatcher dispatcher)
|
||||||
{
|
{
|
||||||
_dispatcher = dispatcher;
|
_dispatcher = dispatcher;
|
||||||
}
|
}
|
||||||
@@ -28,7 +28,8 @@ public class GetWhiteboardService
|
|||||||
{
|
{
|
||||||
WhiteboardId = entity.Id,
|
WhiteboardId = entity.Id,
|
||||||
OwnerId = entity.OwnerId,
|
OwnerId = entity.OwnerId,
|
||||||
Owner = new User(entity.Owner.Id, entity.Owner.UserName!, entity.Owner.Email!)
|
Owner = new User(entity.Owner.Id, entity.Owner.UserName!, entity.Owner.Email!),
|
||||||
|
Code = entity.Code,
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var membership in entity.Memberships)
|
foreach (var membership in entity.Memberships)
|
||||||
|
|||||||
@@ -9,13 +9,14 @@ public class Whiteboard
|
|||||||
|
|
||||||
public Guid OwnerId { get; set; }
|
public Guid OwnerId { get; set; }
|
||||||
public User Owner { get; set; } = null!;
|
public User Owner { get; set; } = null!;
|
||||||
|
|
||||||
|
public string Code {get; set;}
|
||||||
|
|
||||||
public List<User> Users { get; } = [];
|
public List<User> Users { get; } = [];
|
||||||
|
|
||||||
public List<User> ActiveUsers { get; } = [];
|
public List<User> ActiveUsers { get; } = [];
|
||||||
public void AddActiveUser(User user) => ActiveUsers.Add(user);
|
public void AddActiveUser(User user) => ActiveUsers.Add(user);
|
||||||
public void RemoveActiveUser(Guid userId)
|
public void RemoveActiveUser(Guid userId) => ActiveUsers.RemoveAll(u => u.UserId == userId);
|
||||||
=> ActiveUsers.RemoveAll(u => u.UserId == userId);
|
|
||||||
|
|
||||||
public List<Shape> Shapes { get; } = [];
|
public List<Shape> Shapes { get; } = [];
|
||||||
|
|
||||||
@@ -48,5 +49,11 @@ public class Whiteboard
|
|||||||
TextShapes.Add(shape);
|
TextShapes.Add(shape);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddUser(User user) => Users.Add(user);
|
public void AddUser(User user)
|
||||||
|
{
|
||||||
|
if (!Users.Contains(user))
|
||||||
|
{
|
||||||
|
Users.Add(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ builder.Services.AddSingleton<IErrorMessageHandleStrategy, RtErrorHandleStrategy
|
|||||||
builder.Services.AddHostedService<ErrorSubscriberBackgroundService>();
|
builder.Services.AddHostedService<ErrorSubscriberBackgroundService>();
|
||||||
|
|
||||||
builder.Services.AddTransient<MembershipService>();
|
builder.Services.AddTransient<MembershipService>();
|
||||||
builder.Services.AddTransient<GetUserService>();
|
builder.Services.AddTransient<UserService>();
|
||||||
|
|
||||||
builder.Services.AddScoped<GetWhiteboardService>();
|
builder.Services.AddScoped<GetWhiteboardService>();
|
||||||
builder.Services.AddSingleton<WhiteboardManager>();
|
builder.Services.AddSingleton<WhiteboardManager>();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using AipsCore.Application.Models.Shape.Command.CreateTextShape;
|
|||||||
using AipsCore.Application.Models.Shape.Command.MoveShape;
|
using AipsCore.Application.Models.Shape.Command.MoveShape;
|
||||||
using AipsCore.Application.Models.Whiteboard.Command.AcceptUserRequestToJoin;
|
using AipsCore.Application.Models.Whiteboard.Command.AcceptUserRequestToJoin;
|
||||||
using AipsCore.Application.Models.Whiteboard.Command.RejectUserRequestToJoin;
|
using AipsCore.Application.Models.Whiteboard.Command.RejectUserRequestToJoin;
|
||||||
|
using AipsCore.Application.Models.Whiteboard.Command.UserCanceledRequestToJoin;
|
||||||
using AipsRT.Model.Whiteboard.Shapes;
|
using AipsRT.Model.Whiteboard.Shapes;
|
||||||
|
|
||||||
namespace AipsRT.Services.Interfaces;
|
namespace AipsRT.Services.Interfaces;
|
||||||
@@ -17,4 +18,5 @@ public interface IMessagingService
|
|||||||
|
|
||||||
Task AcceptedUser(AcceptUserRequestToJoinCommand command);
|
Task AcceptedUser(AcceptUserRequestToJoinCommand command);
|
||||||
Task RejectedUser(RejectUserRequestToJoinCommand command);
|
Task RejectedUser(RejectUserRequestToJoinCommand command);
|
||||||
|
Task CancelJoinRequest(UserCanceledRequestToJoinCommand command);
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ using AipsCore.Application.Common.Message.AddRectangle;
|
|||||||
using AipsCore.Application.Common.Message.AddTextShape;
|
using AipsCore.Application.Common.Message.AddTextShape;
|
||||||
using AipsCore.Application.Common.Message.MoveShape;
|
using AipsCore.Application.Common.Message.MoveShape;
|
||||||
using AipsCore.Application.Common.Message.RejectUserRequestToJoin;
|
using AipsCore.Application.Common.Message.RejectUserRequestToJoin;
|
||||||
|
using AipsCore.Application.Common.Message.UserCanceledRequestToJoin;
|
||||||
using AipsCore.Application.Models.Shape.Command.CreateArrow;
|
using AipsCore.Application.Models.Shape.Command.CreateArrow;
|
||||||
using AipsCore.Application.Models.Shape.Command.CreateLine;
|
using AipsCore.Application.Models.Shape.Command.CreateLine;
|
||||||
using AipsCore.Application.Models.Shape.Command.CreateRectangle;
|
using AipsCore.Application.Models.Shape.Command.CreateRectangle;
|
||||||
@@ -13,6 +14,7 @@ using AipsCore.Application.Models.Shape.Command.CreateTextShape;
|
|||||||
using AipsCore.Application.Models.Shape.Command.MoveShape;
|
using AipsCore.Application.Models.Shape.Command.MoveShape;
|
||||||
using AipsCore.Application.Models.Whiteboard.Command.AcceptUserRequestToJoin;
|
using AipsCore.Application.Models.Whiteboard.Command.AcceptUserRequestToJoin;
|
||||||
using AipsCore.Application.Models.Whiteboard.Command.RejectUserRequestToJoin;
|
using AipsCore.Application.Models.Whiteboard.Command.RejectUserRequestToJoin;
|
||||||
|
using AipsCore.Application.Models.Whiteboard.Command.UserCanceledRequestToJoin;
|
||||||
using AipsRT.Model.Whiteboard.Shapes;
|
using AipsRT.Model.Whiteboard.Shapes;
|
||||||
using AipsRT.Services.Interfaces;
|
using AipsRT.Services.Interfaces;
|
||||||
|
|
||||||
@@ -116,4 +118,10 @@ public class MessagingService : IMessagingService
|
|||||||
var message = new RejectUserRequestToJoinMessage(command);
|
var message = new RejectUserRequestToJoinMessage(command);
|
||||||
await _messagePublisher.PublishAsync(message);
|
await _messagePublisher.PublishAsync(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task CancelJoinRequest(UserCanceledRequestToJoinCommand command)
|
||||||
|
{
|
||||||
|
var message = new UserCanceledRequestToJoinMessage(command);
|
||||||
|
await _messagePublisher.PublishAsync(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -18,9 +18,16 @@ public class RtErrorHandleStrategy : IErrorMessageHandleStrategy
|
|||||||
|
|
||||||
public async Task Handle(ErrorMessage message, CancellationToken cancellationToken)
|
public async Task Handle(ErrorMessage message, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
var activeUsers = _whiteboardManager.GetWhiteboard(message.WhiteboardId)!.ActiveUsers;
|
||||||
|
|
||||||
await _whiteboardManager.LoadWhiteboard(message.WhiteboardId);
|
await _whiteboardManager.LoadWhiteboard(message.WhiteboardId);
|
||||||
|
|
||||||
var whiteboard = _whiteboardManager.GetWhiteboard(message.WhiteboardId)!;
|
var whiteboard = _whiteboardManager.GetWhiteboard(message.WhiteboardId)!;
|
||||||
|
|
||||||
|
foreach (var user in activeUsers)
|
||||||
|
{
|
||||||
|
whiteboard.AddActiveUser(user);
|
||||||
|
}
|
||||||
|
|
||||||
await _hubContext.Clients
|
await _hubContext.Clients
|
||||||
.Group(whiteboard.WhiteboardId.ToString())
|
.Group(whiteboard.WhiteboardId.ToString())
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using AipsCore.Application.Common.Message.AddRectangle;
|
|||||||
using AipsCore.Application.Common.Message.AddTextShape;
|
using AipsCore.Application.Common.Message.AddTextShape;
|
||||||
using AipsCore.Application.Common.Message.MoveShape;
|
using AipsCore.Application.Common.Message.MoveShape;
|
||||||
using AipsCore.Application.Common.Message.RejectUserRequestToJoin;
|
using AipsCore.Application.Common.Message.RejectUserRequestToJoin;
|
||||||
|
using AipsCore.Application.Common.Message.UserCanceledRequestToJoin;
|
||||||
|
|
||||||
namespace AipsWorker.Messages;
|
namespace AipsWorker.Messages;
|
||||||
|
|
||||||
@@ -21,7 +22,8 @@ public class MessageTypesProvider : IMessageTypesProvider
|
|||||||
typeof(AddTextShapeMessage),
|
typeof(AddTextShapeMessage),
|
||||||
typeof(MoveShapeMessage),
|
typeof(MoveShapeMessage),
|
||||||
typeof(AcceptUserRequestToJoinMessage),
|
typeof(AcceptUserRequestToJoinMessage),
|
||||||
typeof(RejectUserRequestToJoinMessage)
|
typeof(RejectUserRequestToJoinMessage),
|
||||||
|
typeof(UserCanceledRequestToJoinMessage)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,6 +23,12 @@ const colors = ['#4f9dff', '#ff4f4f', '#4fff4f', '#ffff4f', '#ff4fff', '#ffffff'
|
|||||||
|
|
||||||
const isReadOnly = computed(() => sessionStore.selectedTool === 'hand' && !!sessionStore.selectedShape)
|
const isReadOnly = computed(() => sessionStore.selectedTool === 'hand' && !!sessionStore.selectedShape)
|
||||||
|
|
||||||
|
const isOwner = computed(() => {
|
||||||
|
const wb = sessionStore.whiteboard
|
||||||
|
if (!wb || !auth.user) return false
|
||||||
|
return wb.ownerId === auth.user.userId
|
||||||
|
})
|
||||||
|
|
||||||
const showProperties = computed(() => {
|
const showProperties = computed(() => {
|
||||||
if (['rectangle', 'arrow', 'line', 'text'].includes(sessionStore.selectedTool)) return true
|
if (['rectangle', 'arrow', 'line', 'text'].includes(sessionStore.selectedTool)) return true
|
||||||
if (sessionStore.selectedTool === 'hand' && sessionStore.selectedShape) return true
|
if (sessionStore.selectedTool === 'hand' && sessionStore.selectedShape) return true
|
||||||
@@ -61,7 +67,7 @@ const displayTextSize = computed(() => {
|
|||||||
|
|
||||||
const showCopiedTooltip = ref(false)
|
const showCopiedTooltip = ref(false)
|
||||||
|
|
||||||
const whiteboardCode = computed(() => infoStore.getCurrentWhiteboard()?.code || '')
|
const whiteboardCode = computed(() => sessionStore.whiteboard!.code)
|
||||||
|
|
||||||
const copyCodeToClipboard = async () => {
|
const copyCodeToClipboard = async () => {
|
||||||
console.info(whiteboardCode.value)
|
console.info(whiteboardCode.value)
|
||||||
@@ -166,7 +172,7 @@ const copyCodeToClipboard = async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="position-relative mb-2">
|
<div v-if="isOwner" class="position-relative mb-2">
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-outline-primary w-100"
|
class="btn btn-sm btn-outline-primary w-100"
|
||||||
@click="copyCodeToClipboard"
|
@click="copyCodeToClipboard"
|
||||||
|
|||||||
@@ -22,8 +22,23 @@ export class SignalRService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async start(): Promise<void> {
|
async start(): Promise<void> {
|
||||||
if (this.connection.state === HubConnectionState.Disconnected) {
|
if (this.connection.state !== HubConnectionState.Disconnected) return;
|
||||||
|
|
||||||
|
try {
|
||||||
await this.connection.start();
|
await this.connection.start();
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.statusCode === 401 || err.message?.includes("401")) {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
try {
|
||||||
|
await authStore.tryRefresh();
|
||||||
|
|
||||||
|
await this.connection.start();
|
||||||
|
} catch (refreshErr) {
|
||||||
|
throw refreshErr;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
|
tryRefresh,
|
||||||
initialize,
|
initialize,
|
||||||
login,
|
login,
|
||||||
signup,
|
signup,
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ export const useWhiteboardStore = defineStore('whiteboard', () => {
|
|||||||
whiteboardHubService.onAccepted(() => {
|
whiteboardHubService.onAccepted(() => {
|
||||||
const infoStore = useWhiteboardsStore()
|
const infoStore = useWhiteboardsStore()
|
||||||
infoStore.stopWaitingToJoin()
|
infoStore.stopWaitingToJoin()
|
||||||
|
isLoading.value = false
|
||||||
})
|
})
|
||||||
|
|
||||||
whiteboardHubService.onRejected(() => {
|
whiteboardHubService.onRejected(() => {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export interface TextShape extends Shape {
|
|||||||
export interface Whiteboard {
|
export interface Whiteboard {
|
||||||
whiteboardId: string
|
whiteboardId: string
|
||||||
ownerId: string
|
ownerId: string
|
||||||
|
code: string
|
||||||
rectangles: Rectangle[]
|
rectangles: Rectangle[]
|
||||||
arrows: Arrow[]
|
arrows: Arrow[]
|
||||||
lines: Line[]
|
lines: Line[]
|
||||||
|
|||||||
Reference in New Issue
Block a user