Implemented the Floor Plans multi-block room and transition phase.
This commit is contained in:
parent
e455018d73
commit
18858f3fba
@ -178,6 +178,43 @@ public sealed class FloorPlansController(IFloorPlanService floorPlans) : Control
|
|||||||
return RedirectToAction(nameof(Edit), new { id = floorPlanId });
|
return RedirectToAction(nameof(Edit), new { id = floorPlanId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> SaveTransition(int floorPlanId, FloorPlanTransitionEditViewModel model)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (ModelState.IsValid)
|
||||||
|
{
|
||||||
|
await floorPlans.SaveTransitionAsync(floorPlanId, model);
|
||||||
|
TempData["FloorPlanMessage"] = model.FloorPlanTransitionID == 0 ? "Transition created." : "Transition updated.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
TempData["FloorPlanError"] = ex.Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(Edit), new { id = floorPlanId });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> DeleteTransition(int floorPlanId, int floorPlanTransitionId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await floorPlans.DeleteTransitionAsync(floorPlanId, floorPlanTransitionId);
|
||||||
|
TempData["FloorPlanMessage"] = "Transition deleted.";
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
TempData["FloorPlanError"] = ex.Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(Edit), new { id = floorPlanId });
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> AddBlock(int floorPlanId, FloorPlanBlockEditViewModel model)
|
public async Task<IActionResult> AddBlock(int floorPlanId, FloorPlanBlockEditViewModel model)
|
||||||
|
|||||||
@ -218,7 +218,7 @@ public interface IFloorPlanRepository
|
|||||||
{
|
{
|
||||||
Task<IReadOnlyList<FloorPlan>> ListByProjectAsync(int projectId);
|
Task<IReadOnlyList<FloorPlan>> ListByProjectAsync(int projectId);
|
||||||
Task<FloorPlan?> GetAsync(int floorPlanId);
|
Task<FloorPlan?> GetAsync(int floorPlanId);
|
||||||
Task<(FloorPlan? FloorPlan, IReadOnlyList<FloorPlanFloor> Floors, IReadOnlyList<FloorPlanBlock> Blocks)> GetEditorAsync(int floorPlanId);
|
Task<(FloorPlan? FloorPlan, IReadOnlyList<FloorPlanFloor> Floors, IReadOnlyList<FloorPlanBlock> Blocks, IReadOnlyList<FloorPlanTransition> Transitions)> GetEditorAsync(int floorPlanId);
|
||||||
Task<FloorPlanFloor?> GetFloorAsync(int floorPlanFloorId);
|
Task<FloorPlanFloor?> GetFloorAsync(int floorPlanFloorId);
|
||||||
Task<int> SavePlanAsync(FloorPlan floorPlan);
|
Task<int> SavePlanAsync(FloorPlan floorPlan);
|
||||||
Task DeletePlanAsync(int floorPlanId);
|
Task DeletePlanAsync(int floorPlanId);
|
||||||
@ -227,6 +227,8 @@ public interface IFloorPlanRepository
|
|||||||
Task DeleteFloorAsync(int floorPlanFloorId);
|
Task DeleteFloorAsync(int floorPlanFloorId);
|
||||||
Task<int> SaveBlockAsync(FloorPlanBlock block);
|
Task<int> SaveBlockAsync(FloorPlanBlock block);
|
||||||
Task DeleteBlockAsync(int floorPlanBlockId);
|
Task DeleteBlockAsync(int floorPlanBlockId);
|
||||||
|
Task<int> SaveTransitionAsync(FloorPlanTransition transition);
|
||||||
|
Task DeleteTransitionAsync(int floorPlanTransitionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IWarningRepository
|
public interface IWarningRepository
|
||||||
@ -552,14 +554,15 @@ public sealed class FloorPlanRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
return await connection.QuerySingleOrDefaultAsync<FloorPlan>("dbo.FloorPlan_Get", new { FloorPlanID = floorPlanId }, commandType: CommandType.StoredProcedure);
|
return await connection.QuerySingleOrDefaultAsync<FloorPlan>("dbo.FloorPlan_Get", new { FloorPlanID = floorPlanId }, commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(FloorPlan? FloorPlan, IReadOnlyList<FloorPlanFloor> Floors, IReadOnlyList<FloorPlanBlock> Blocks)> GetEditorAsync(int floorPlanId)
|
public async Task<(FloorPlan? FloorPlan, IReadOnlyList<FloorPlanFloor> Floors, IReadOnlyList<FloorPlanBlock> Blocks, IReadOnlyList<FloorPlanTransition> Transitions)> GetEditorAsync(int floorPlanId)
|
||||||
{
|
{
|
||||||
using var connection = connectionFactory.CreateConnection();
|
using var connection = connectionFactory.CreateConnection();
|
||||||
using var result = await connection.QueryMultipleAsync("dbo.FloorPlan_GetEditor", new { FloorPlanID = floorPlanId }, commandType: CommandType.StoredProcedure);
|
using var result = await connection.QueryMultipleAsync("dbo.FloorPlan_GetEditor", new { FloorPlanID = floorPlanId }, commandType: CommandType.StoredProcedure);
|
||||||
var floorPlan = await result.ReadSingleOrDefaultAsync<FloorPlan>();
|
var floorPlan = await result.ReadSingleOrDefaultAsync<FloorPlan>();
|
||||||
var floors = (await result.ReadAsync<FloorPlanFloor>()).ToList();
|
var floors = (await result.ReadAsync<FloorPlanFloor>()).ToList();
|
||||||
var blocks = (await result.ReadAsync<FloorPlanBlock>()).ToList();
|
var blocks = (await result.ReadAsync<FloorPlanBlock>()).ToList();
|
||||||
return (floorPlan, floors, blocks);
|
var transitions = (await result.ReadAsync<FloorPlanTransition>()).ToList();
|
||||||
|
return (floorPlan, floors, blocks, transitions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<FloorPlanFloor?> GetFloorAsync(int floorPlanFloorId)
|
public async Task<FloorPlanFloor?> GetFloorAsync(int floorPlanFloorId)
|
||||||
@ -645,6 +648,31 @@ public sealed class FloorPlanRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
using var connection = connectionFactory.CreateConnection();
|
using var connection = connectionFactory.CreateConnection();
|
||||||
await connection.ExecuteAsync("dbo.FloorPlanBlock_Delete", new { FloorPlanBlockID = floorPlanBlockId }, commandType: CommandType.StoredProcedure);
|
await connection.ExecuteAsync("dbo.FloorPlanBlock_Delete", new { FloorPlanBlockID = floorPlanBlockId }, commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<int> SaveTransitionAsync(FloorPlanTransition transition)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
return await connection.QuerySingleAsync<int>(
|
||||||
|
"dbo.FloorPlanTransition_Save",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
transition.FloorPlanTransitionID,
|
||||||
|
transition.FloorPlanFloorID,
|
||||||
|
transition.FromLocationID,
|
||||||
|
transition.ToLocationID,
|
||||||
|
transition.TransitionType,
|
||||||
|
transition.Notes,
|
||||||
|
transition.IsLocked,
|
||||||
|
transition.DisplayLabel
|
||||||
|
},
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteTransitionAsync(int floorPlanTransitionId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
await connection.ExecuteAsync("dbo.FloorPlanTransition_Delete", new { FloorPlanTransitionID = floorPlanTransitionId }, commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class WarningRepository(ISqlConnectionFactory connectionFactory) : IWarningRepository
|
public sealed class WarningRepository(ISqlConnectionFactory connectionFactory) : IWarningRepository
|
||||||
|
|||||||
@ -1195,6 +1195,56 @@ public sealed class FloorPlanBlock
|
|||||||
public string DisplayLabel => string.IsNullOrWhiteSpace(LabelOverride) ? LocationName : LabelOverride;
|
public string DisplayLabel => string.IsNullOrWhiteSpace(LabelOverride) ? LocationName : LabelOverride;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class FloorPlanTransitionTypes
|
||||||
|
{
|
||||||
|
public const string Door = "Door";
|
||||||
|
public const string DoubleDoor = "Double Door";
|
||||||
|
public const string Archway = "Archway";
|
||||||
|
public const string OpenPassage = "Open Passage";
|
||||||
|
public const string FrenchDoors = "French Doors";
|
||||||
|
public const string SlidingDoors = "Sliding Doors";
|
||||||
|
public const string StairsUp = "Stairs Up";
|
||||||
|
public const string StairsDown = "Stairs Down";
|
||||||
|
public const string Ladder = "Ladder";
|
||||||
|
public const string SecretPassage = "Secret Passage";
|
||||||
|
public const string Window = "Window";
|
||||||
|
|
||||||
|
public static readonly IReadOnlyList<string> All =
|
||||||
|
[
|
||||||
|
Door,
|
||||||
|
DoubleDoor,
|
||||||
|
Archway,
|
||||||
|
OpenPassage,
|
||||||
|
FrenchDoors,
|
||||||
|
SlidingDoors,
|
||||||
|
StairsUp,
|
||||||
|
StairsDown,
|
||||||
|
Ladder,
|
||||||
|
SecretPassage,
|
||||||
|
Window
|
||||||
|
];
|
||||||
|
|
||||||
|
public static bool IsValid(string? transitionType) => All.Contains(transitionType ?? string.Empty, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FloorPlanTransition
|
||||||
|
{
|
||||||
|
public int FloorPlanTransitionID { get; set; }
|
||||||
|
public int FloorPlanFloorID { get; set; }
|
||||||
|
public int FromLocationID { get; set; }
|
||||||
|
public string FromLocationName { get; set; } = string.Empty;
|
||||||
|
public string FromLocationPath { get; set; } = string.Empty;
|
||||||
|
public int ToLocationID { get; set; }
|
||||||
|
public string ToLocationName { get; set; } = string.Empty;
|
||||||
|
public string ToLocationPath { get; set; } = string.Empty;
|
||||||
|
public string TransitionType { get; set; } = FloorPlanTransitionTypes.Door;
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
public bool IsLocked { get; set; }
|
||||||
|
public string? DisplayLabel { get; set; }
|
||||||
|
public DateTime CreatedDate { get; set; }
|
||||||
|
public DateTime? ModifiedDate { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class SceneAssetLocation
|
public sealed class SceneAssetLocation
|
||||||
{
|
{
|
||||||
public int SceneAssetLocationID { get; set; }
|
public int SceneAssetLocationID { get; set; }
|
||||||
|
|||||||
@ -191,6 +191,8 @@ public interface IFloorPlanService
|
|||||||
Task SaveBackgroundSettingsAsync(FloorPlanFloorBackgroundSettingsViewModel model);
|
Task SaveBackgroundSettingsAsync(FloorPlanFloorBackgroundSettingsViewModel model);
|
||||||
Task ResetBackgroundAsync(int floorPlanId, int floorPlanFloorId);
|
Task ResetBackgroundAsync(int floorPlanId, int floorPlanFloorId);
|
||||||
Task RemoveBackgroundAsync(int floorPlanId, int floorPlanFloorId);
|
Task RemoveBackgroundAsync(int floorPlanId, int floorPlanFloorId);
|
||||||
|
Task<int> SaveTransitionAsync(int floorPlanId, FloorPlanTransitionEditViewModel model);
|
||||||
|
Task DeleteTransitionAsync(int floorPlanId, int floorPlanTransitionId);
|
||||||
Task DeleteBlockAsync(int floorPlanBlockId);
|
Task DeleteBlockAsync(int floorPlanBlockId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -7576,6 +7578,7 @@ public sealed class FloorPlanService(
|
|||||||
},
|
},
|
||||||
Floors = data.Floors.Select(ToFloorEdit).ToList(),
|
Floors = data.Floors.Select(ToFloorEdit).ToList(),
|
||||||
Blocks = data.Blocks.Select(ToBlockEdit).ToList(),
|
Blocks = data.Blocks.Select(ToBlockEdit).ToList(),
|
||||||
|
Transitions = data.Transitions.Select(ToTransitionEdit).ToList(),
|
||||||
NewFloor = new FloorPlanFloorEditViewModel
|
NewFloor = new FloorPlanFloorEditViewModel
|
||||||
{
|
{
|
||||||
FloorPlanID = data.FloorPlan.FloorPlanID,
|
FloorPlanID = data.FloorPlan.FloorPlanID,
|
||||||
@ -7591,9 +7594,15 @@ public sealed class FloorPlanService(
|
|||||||
WidthCells = 2,
|
WidthCells = 2,
|
||||||
HeightCells = 1
|
HeightCells = 1
|
||||||
},
|
},
|
||||||
|
NewTransition = new FloorPlanTransitionEditViewModel
|
||||||
|
{
|
||||||
|
FloorPlanFloorID = data.Floors.FirstOrDefault()?.FloorPlanFloorID ?? 0,
|
||||||
|
TransitionType = FloorPlanTransitionTypes.Door
|
||||||
|
},
|
||||||
LocationOptions = ToOptionalLocationOptions(projectLocations, "Create/link automatically"),
|
LocationOptions = ToOptionalLocationOptions(projectLocations, "Create/link automatically"),
|
||||||
Locations = projectLocations,
|
Locations = projectLocations,
|
||||||
BlockTypeOptions = FloorPlanBlockTypes.All.Select(x => new SelectListItem(BlockTypeLabel(x), x)).ToList()
|
BlockTypeOptions = FloorPlanBlockTypes.All.Select(x => new SelectListItem(BlockTypeLabel(x), x)).ToList(),
|
||||||
|
TransitionTypeOptions = FloorPlanTransitionTypes.All.Select(x => new SelectListItem(x, x)).ToList()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -7711,6 +7720,37 @@ public sealed class FloorPlanService(
|
|||||||
|
|
||||||
public Task DeleteBlockAsync(int floorPlanBlockId) => floorPlans.DeleteBlockAsync(floorPlanBlockId);
|
public Task DeleteBlockAsync(int floorPlanBlockId) => floorPlans.DeleteBlockAsync(floorPlanBlockId);
|
||||||
|
|
||||||
|
public async Task<int> SaveTransitionAsync(int floorPlanId, FloorPlanTransitionEditViewModel model)
|
||||||
|
{
|
||||||
|
var data = await floorPlans.GetEditorAsync(floorPlanId);
|
||||||
|
if (data.FloorPlan is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Floor plan not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var floor = data.Floors.FirstOrDefault(x => x.FloorPlanFloorID == model.FloorPlanFloorID)
|
||||||
|
?? throw new InvalidOperationException("Floor not found.");
|
||||||
|
|
||||||
|
ValidateTransition(model, floor, data.Blocks, data.Transitions);
|
||||||
|
var transitionId = await floorPlans.SaveTransitionAsync(ToTransition(model));
|
||||||
|
await activity.RecordAsync(data.FloorPlan.ProjectID, model.FloorPlanTransitionID == 0 ? "Created" : "Updated", "Floor Plan Transition", transitionId, model.TransitionType);
|
||||||
|
return transitionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteTransitionAsync(int floorPlanId, int floorPlanTransitionId)
|
||||||
|
{
|
||||||
|
var data = await floorPlans.GetEditorAsync(floorPlanId);
|
||||||
|
if (data.FloorPlan is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Floor plan not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var transition = data.Transitions.FirstOrDefault(x => x.FloorPlanTransitionID == floorPlanTransitionId)
|
||||||
|
?? throw new InvalidOperationException("Transition not found.");
|
||||||
|
await floorPlans.DeleteTransitionAsync(floorPlanTransitionId);
|
||||||
|
await activity.RecordAsync(data.FloorPlan.ProjectID, "Deleted", "Floor Plan Transition", floorPlanTransitionId, transition.TransitionType);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<FloorPlanBackgroundImageUploadResult> UploadBackgroundAsync(int floorPlanId, int floorPlanFloorId, IFormFile upload)
|
public async Task<FloorPlanBackgroundImageUploadResult> UploadBackgroundAsync(int floorPlanId, int floorPlanFloorId, IFormFile upload)
|
||||||
{
|
{
|
||||||
var (plan, floor) = await GetPlanFloorAsync(floorPlanId, floorPlanFloorId);
|
var (plan, floor) = await GetPlanFloorAsync(floorPlanId, floorPlanFloorId);
|
||||||
@ -7819,6 +7859,49 @@ public sealed class FloorPlanService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ValidateTransition(
|
||||||
|
FloorPlanTransitionEditViewModel model,
|
||||||
|
FloorPlanFloor floor,
|
||||||
|
IReadOnlyList<FloorPlanBlock> blocks,
|
||||||
|
IReadOnlyList<FloorPlanTransition> transitions)
|
||||||
|
{
|
||||||
|
if (!FloorPlanTransitionTypes.IsValid(model.TransitionType))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Choose a valid transition type.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!model.FromLocationID.HasValue || !model.ToLocationID.HasValue)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Choose both transition locations.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.FromLocationID.Value == model.ToLocationID.Value)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("From and To locations must be different.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var floorLocationIds = blocks
|
||||||
|
.Where(x => x.FloorPlanFloorID == floor.FloorPlanFloorID)
|
||||||
|
.Select(x => x.LocationID)
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
if (!floorLocationIds.Contains(model.FromLocationID.Value) || !floorLocationIds.Contains(model.ToLocationID.Value))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Transition locations must both exist on the active floor.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var duplicate = transitions.Any(x => x.FloorPlanFloorID == floor.FloorPlanFloorID
|
||||||
|
&& x.FloorPlanTransitionID != model.FloorPlanTransitionID
|
||||||
|
&& x.FromLocationID == model.FromLocationID.Value
|
||||||
|
&& x.ToLocationID == model.ToLocationID.Value
|
||||||
|
&& string.Equals(x.TransitionType, model.TransitionType, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (duplicate)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("That transition already exists on this floor.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<(FloorPlan Plan, FloorPlanFloor Floor)> GetPlanFloorAsync(int floorPlanId, int floorPlanFloorId)
|
private async Task<(FloorPlan Plan, FloorPlanFloor Floor)> GetPlanFloorAsync(int floorPlanId, int floorPlanFloorId)
|
||||||
{
|
{
|
||||||
var plan = await floorPlans.GetAsync(floorPlanId)
|
var plan = await floorPlans.GetAsync(floorPlanId)
|
||||||
@ -8019,6 +8102,34 @@ public sealed class FloorPlanService(
|
|||||||
Notes = model.Notes
|
Notes = model.Notes
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static FloorPlanTransitionEditViewModel ToTransitionEdit(FloorPlanTransition transition) => new()
|
||||||
|
{
|
||||||
|
FloorPlanTransitionID = transition.FloorPlanTransitionID,
|
||||||
|
FloorPlanFloorID = transition.FloorPlanFloorID,
|
||||||
|
FromLocationID = transition.FromLocationID,
|
||||||
|
FromLocationName = transition.FromLocationName,
|
||||||
|
FromLocationPath = transition.FromLocationPath,
|
||||||
|
ToLocationID = transition.ToLocationID,
|
||||||
|
ToLocationName = transition.ToLocationName,
|
||||||
|
ToLocationPath = transition.ToLocationPath,
|
||||||
|
TransitionType = transition.TransitionType,
|
||||||
|
Notes = transition.Notes,
|
||||||
|
IsLocked = transition.IsLocked,
|
||||||
|
DisplayLabel = transition.DisplayLabel
|
||||||
|
};
|
||||||
|
|
||||||
|
private static FloorPlanTransition ToTransition(FloorPlanTransitionEditViewModel model) => new()
|
||||||
|
{
|
||||||
|
FloorPlanTransitionID = model.FloorPlanTransitionID,
|
||||||
|
FloorPlanFloorID = model.FloorPlanFloorID,
|
||||||
|
FromLocationID = model.FromLocationID!.Value,
|
||||||
|
ToLocationID = model.ToLocationID!.Value,
|
||||||
|
TransitionType = model.TransitionType,
|
||||||
|
Notes = model.Notes,
|
||||||
|
IsLocked = model.IsLocked,
|
||||||
|
DisplayLabel = model.DisplayLabel
|
||||||
|
};
|
||||||
|
|
||||||
private static string BlockTypeLabel(string blockType) => blockType switch
|
private static string BlockTypeLabel(string blockType) => blockType switch
|
||||||
{
|
{
|
||||||
FloorPlanBlockTypes.OpenSpace => "Open space",
|
FloorPlanBlockTypes.OpenSpace => "Open space",
|
||||||
|
|||||||
189
PlotLine/Sql/075_FloorPlanTransitions.sql
Normal file
189
PlotLine/Sql/075_FloorPlanTransitions.sql
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
SET ANSI_NULLS ON;
|
||||||
|
GO
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF OBJECT_ID(N'dbo.FloorPlanTransitions', N'U') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE dbo.FloorPlanTransitions
|
||||||
|
(
|
||||||
|
FloorPlanTransitionID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_FloorPlanTransitions PRIMARY KEY,
|
||||||
|
FloorPlanFloorID int NOT NULL,
|
||||||
|
FromLocationID int NOT NULL,
|
||||||
|
ToLocationID int NOT NULL,
|
||||||
|
TransitionType nvarchar(50) NOT NULL,
|
||||||
|
Notes nvarchar(max) NULL,
|
||||||
|
IsLocked bit NOT NULL CONSTRAINT DF_FloorPlanTransitions_IsLocked DEFAULT 0,
|
||||||
|
DisplayLabel nvarchar(255) NULL,
|
||||||
|
CreatedDate datetime2 NOT NULL CONSTRAINT DF_FloorPlanTransitions_CreatedDate DEFAULT SYSUTCDATETIME(),
|
||||||
|
ModifiedDate datetime2 NULL
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = N'FK_FloorPlanTransitions_FloorPlanFloors')
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE dbo.FloorPlanTransitions
|
||||||
|
ADD CONSTRAINT FK_FloorPlanTransitions_FloorPlanFloors FOREIGN KEY (FloorPlanFloorID) REFERENCES dbo.FloorPlanFloors(FloorPlanFloorID) ON DELETE CASCADE;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = N'FK_FloorPlanTransitions_FromLocations')
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE dbo.FloorPlanTransitions
|
||||||
|
ADD CONSTRAINT FK_FloorPlanTransitions_FromLocations FOREIGN KEY (FromLocationID) REFERENCES dbo.Locations(LocationID);
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = N'FK_FloorPlanTransitions_ToLocations')
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE dbo.FloorPlanTransitions
|
||||||
|
ADD CONSTRAINT FK_FloorPlanTransitions_ToLocations FOREIGN KEY (ToLocationID) REFERENCES dbo.Locations(LocationID);
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_FloorPlanTransitions_Floor' AND object_id = OBJECT_ID(N'dbo.FloorPlanTransitions'))
|
||||||
|
BEGIN
|
||||||
|
CREATE INDEX IX_FloorPlanTransitions_Floor ON dbo.FloorPlanTransitions(FloorPlanFloorID);
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_FloorPlanTransitions_Locations' AND object_id = OBJECT_ID(N'dbo.FloorPlanTransitions'))
|
||||||
|
BEGIN
|
||||||
|
CREATE INDEX IX_FloorPlanTransitions_Locations ON dbo.FloorPlanTransitions(FloorPlanFloorID, FromLocationID, ToLocationID, TransitionType);
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.FloorPlan_GetEditor
|
||||||
|
@FloorPlanID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
EXEC dbo.FloorPlan_Get @FloorPlanID;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
fpf.FloorPlanFloorID,
|
||||||
|
fpf.FloorPlanID,
|
||||||
|
fpf.LocationID,
|
||||||
|
lp.LocationName,
|
||||||
|
lp.LocationPath,
|
||||||
|
fpf.Name,
|
||||||
|
fpf.SortOrder,
|
||||||
|
fpf.GridWidth,
|
||||||
|
fpf.GridHeight,
|
||||||
|
fpf.BackgroundImagePath,
|
||||||
|
fpf.BackgroundImageOriginalFileName,
|
||||||
|
COALESCE(fpf.BackgroundOpacity, CONVERT(decimal(5,2), 0.35)) AS BackgroundOpacity,
|
||||||
|
COALESCE(fpf.BackgroundScale, CONVERT(decimal(8,3), 1.000)) AS BackgroundScale,
|
||||||
|
COALESCE(fpf.BackgroundOffsetX, CONVERT(decimal(10,2), 0.00)) AS BackgroundOffsetX,
|
||||||
|
COALESCE(fpf.BackgroundOffsetY, CONVERT(decimal(10,2), 0.00)) AS BackgroundOffsetY,
|
||||||
|
fpf.BackgroundLocked,
|
||||||
|
fpf.CreatedDate,
|
||||||
|
fpf.ModifiedDate
|
||||||
|
FROM dbo.FloorPlanFloors fpf
|
||||||
|
LEFT JOIN dbo.LocationPaths lp ON lp.LocationID = fpf.LocationID
|
||||||
|
WHERE fpf.FloorPlanID = @FloorPlanID
|
||||||
|
ORDER BY fpf.SortOrder, fpf.Name;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
fpb.FloorPlanBlockID,
|
||||||
|
fpb.FloorPlanFloorID,
|
||||||
|
fpb.LocationID,
|
||||||
|
lp.LocationName,
|
||||||
|
COALESCE(NULLIF(lp.LocationPath, N''), lp.LocationName) AS LocationPath,
|
||||||
|
fpb.X,
|
||||||
|
fpb.Y,
|
||||||
|
fpb.WidthCells,
|
||||||
|
fpb.HeightCells,
|
||||||
|
fpb.BlockType,
|
||||||
|
fpb.LabelOverride,
|
||||||
|
fpb.Notes,
|
||||||
|
fpb.CreatedDate,
|
||||||
|
fpb.ModifiedDate
|
||||||
|
FROM dbo.FloorPlanBlocks fpb
|
||||||
|
INNER JOIN dbo.FloorPlanFloors fpf ON fpf.FloorPlanFloorID = fpb.FloorPlanFloorID
|
||||||
|
INNER JOIN dbo.LocationPaths lp ON lp.LocationID = fpb.LocationID
|
||||||
|
WHERE fpf.FloorPlanID = @FloorPlanID
|
||||||
|
ORDER BY fpf.SortOrder, fpb.Y, fpb.X, lp.LocationName;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
fpt.FloorPlanTransitionID,
|
||||||
|
fpt.FloorPlanFloorID,
|
||||||
|
fpt.FromLocationID,
|
||||||
|
fromPath.LocationName AS FromLocationName,
|
||||||
|
COALESCE(NULLIF(fromPath.LocationPath, N''), fromPath.LocationName) AS FromLocationPath,
|
||||||
|
fpt.ToLocationID,
|
||||||
|
toPath.LocationName AS ToLocationName,
|
||||||
|
COALESCE(NULLIF(toPath.LocationPath, N''), toPath.LocationName) AS ToLocationPath,
|
||||||
|
fpt.TransitionType,
|
||||||
|
fpt.Notes,
|
||||||
|
fpt.IsLocked,
|
||||||
|
fpt.DisplayLabel,
|
||||||
|
fpt.CreatedDate,
|
||||||
|
fpt.ModifiedDate
|
||||||
|
FROM dbo.FloorPlanTransitions fpt
|
||||||
|
INNER JOIN dbo.FloorPlanFloors fpf ON fpf.FloorPlanFloorID = fpt.FloorPlanFloorID
|
||||||
|
INNER JOIN dbo.LocationPaths fromPath ON fromPath.LocationID = fpt.FromLocationID
|
||||||
|
INNER JOIN dbo.LocationPaths toPath ON toPath.LocationID = fpt.ToLocationID
|
||||||
|
WHERE fpf.FloorPlanID = @FloorPlanID
|
||||||
|
ORDER BY fpf.SortOrder, fromPath.LocationName, toPath.LocationName, fpt.TransitionType;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.FloorPlanTransition_Save
|
||||||
|
@FloorPlanTransitionID int,
|
||||||
|
@FloorPlanFloorID int,
|
||||||
|
@FromLocationID int,
|
||||||
|
@ToLocationID int,
|
||||||
|
@TransitionType nvarchar(50),
|
||||||
|
@Notes nvarchar(max) = NULL,
|
||||||
|
@IsLocked bit = 0,
|
||||||
|
@DisplayLabel nvarchar(255) = NULL
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SET @TransitionType = NULLIF(LTRIM(RTRIM(@TransitionType)), N'');
|
||||||
|
SET @DisplayLabel = NULLIF(LTRIM(RTRIM(@DisplayLabel)), N'');
|
||||||
|
|
||||||
|
IF @TransitionType IS NULL
|
||||||
|
THROW 51020, 'Transition type is required.', 1;
|
||||||
|
|
||||||
|
IF @FromLocationID = @ToLocationID
|
||||||
|
THROW 51021, 'From and To locations must be different.', 1;
|
||||||
|
|
||||||
|
IF @FloorPlanTransitionID = 0
|
||||||
|
BEGIN
|
||||||
|
INSERT dbo.FloorPlanTransitions (FloorPlanFloorID, FromLocationID, ToLocationID, TransitionType, Notes, IsLocked, DisplayLabel)
|
||||||
|
VALUES (@FloorPlanFloorID, @FromLocationID, @ToLocationID, @TransitionType, @Notes, @IsLocked, @DisplayLabel);
|
||||||
|
|
||||||
|
SELECT CONVERT(int, SCOPE_IDENTITY());
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
|
||||||
|
UPDATE dbo.FloorPlanTransitions
|
||||||
|
SET FromLocationID = @FromLocationID,
|
||||||
|
ToLocationID = @ToLocationID,
|
||||||
|
TransitionType = @TransitionType,
|
||||||
|
Notes = @Notes,
|
||||||
|
IsLocked = @IsLocked,
|
||||||
|
DisplayLabel = @DisplayLabel,
|
||||||
|
ModifiedDate = SYSUTCDATETIME()
|
||||||
|
WHERE FloorPlanTransitionID = @FloorPlanTransitionID
|
||||||
|
AND FloorPlanFloorID = @FloorPlanFloorID;
|
||||||
|
|
||||||
|
SELECT @FloorPlanTransitionID;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.FloorPlanTransition_Delete
|
||||||
|
@FloorPlanTransitionID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
DELETE dbo.FloorPlanTransitions
|
||||||
|
WHERE FloorPlanTransitionID = @FloorPlanTransitionID;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
@ -1844,11 +1844,14 @@ public sealed class FloorPlanEditorViewModel
|
|||||||
public FloorPlanEditViewModel FloorPlan { get; set; } = new();
|
public FloorPlanEditViewModel FloorPlan { get; set; } = new();
|
||||||
public List<FloorPlanFloorEditViewModel> Floors { get; set; } = [];
|
public List<FloorPlanFloorEditViewModel> Floors { get; set; } = [];
|
||||||
public List<FloorPlanBlockEditViewModel> Blocks { get; set; } = [];
|
public List<FloorPlanBlockEditViewModel> Blocks { get; set; } = [];
|
||||||
|
public List<FloorPlanTransitionEditViewModel> Transitions { get; set; } = [];
|
||||||
public FloorPlanFloorEditViewModel NewFloor { get; set; } = new();
|
public FloorPlanFloorEditViewModel NewFloor { get; set; } = new();
|
||||||
public FloorPlanBlockEditViewModel NewBlock { get; set; } = new();
|
public FloorPlanBlockEditViewModel NewBlock { get; set; } = new();
|
||||||
|
public FloorPlanTransitionEditViewModel NewTransition { get; set; } = new();
|
||||||
public IReadOnlyList<SelectListItem> LocationOptions { get; set; } = [];
|
public IReadOnlyList<SelectListItem> LocationOptions { get; set; } = [];
|
||||||
public IReadOnlyList<LocationItem> Locations { get; set; } = [];
|
public IReadOnlyList<LocationItem> Locations { get; set; } = [];
|
||||||
public IReadOnlyList<SelectListItem> BlockTypeOptions { get; set; } = [];
|
public IReadOnlyList<SelectListItem> BlockTypeOptions { get; set; } = [];
|
||||||
|
public IReadOnlyList<SelectListItem> TransitionTypeOptions { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class FloorPlanFloorEditViewModel
|
public sealed class FloorPlanFloorEditViewModel
|
||||||
@ -1942,6 +1945,35 @@ public sealed class FloorPlanBlockEditViewModel
|
|||||||
public string DisplayLabel => string.IsNullOrWhiteSpace(LabelOverride) ? LocationName : LabelOverride;
|
public string DisplayLabel => string.IsNullOrWhiteSpace(LabelOverride) ? LocationName : LabelOverride;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class FloorPlanTransitionEditViewModel
|
||||||
|
{
|
||||||
|
public int FloorPlanTransitionID { get; set; }
|
||||||
|
public int FloorPlanFloorID { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public int? FromLocationID { get; set; }
|
||||||
|
|
||||||
|
public string FromLocationName { get; set; } = string.Empty;
|
||||||
|
public string FromLocationPath { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public int? ToLocationID { get; set; }
|
||||||
|
|
||||||
|
public string ToLocationName { get; set; } = string.Empty;
|
||||||
|
public string ToLocationPath { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required, StringLength(50)]
|
||||||
|
public string TransitionType { get; set; } = FloorPlanTransitionTypes.Door;
|
||||||
|
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
public bool IsLocked { get; set; }
|
||||||
|
|
||||||
|
[StringLength(255)]
|
||||||
|
public string? DisplayLabel { get; set; }
|
||||||
|
|
||||||
|
public string Label => string.IsNullOrWhiteSpace(DisplayLabel) ? TransitionType : DisplayLabel;
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class WarningDashboardViewModel
|
public sealed class WarningDashboardViewModel
|
||||||
{
|
{
|
||||||
public Project Project { get; set; } = new();
|
public Project Project { get; set; } = new();
|
||||||
|
|||||||
@ -4,9 +4,50 @@
|
|||||||
ViewData["ProjectSection"] = "Floor Plans";
|
ViewData["ProjectSection"] = "Floor Plans";
|
||||||
var activeFloor = Model.Floors.OrderBy(x => x.SortOrder).ThenBy(x => x.Name).FirstOrDefault();
|
var activeFloor = Model.Floors.OrderBy(x => x.SortOrder).ThenBy(x => x.Name).FirstOrDefault();
|
||||||
var blocksByFloor = Model.Blocks.GroupBy(x => x.FloorPlanFloorID).ToDictionary(x => x.Key, x => x.ToList());
|
var blocksByFloor = Model.Blocks.GroupBy(x => x.FloorPlanFloorID).ToDictionary(x => x.Key, x => x.ToList());
|
||||||
|
var transitionsByFloor = Model.Transitions.GroupBy(x => x.FloorPlanFloorID).ToDictionary(x => x.Key, x => x.ToList());
|
||||||
var selectedBlockId = int.TryParse(Context.Request.Query["selectedBlockId"], out var parsedSelectedBlockId) ? parsedSelectedBlockId : Model.Blocks.FirstOrDefault()?.FloorPlanBlockID ?? 0;
|
var selectedBlockId = int.TryParse(Context.Request.Query["selectedBlockId"], out var parsedSelectedBlockId) ? parsedSelectedBlockId : Model.Blocks.FirstOrDefault()?.FloorPlanBlockID ?? 0;
|
||||||
string BlockClass(string blockType) => $"floor-plan-block floor-plan-block--{blockType.ToLowerInvariant()}";
|
string BlockClass(string blockType) => $"floor-plan-block floor-plan-block--{blockType.ToLowerInvariant()}";
|
||||||
string CssNumber(decimal value) => value.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture);
|
string CssNumber(decimal value) => value.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
bool RangesOverlap(int aStart, int aLength, int bStart, int bLength) => aStart < bStart + bLength && bStart < aStart + aLength;
|
||||||
|
string SharedWallClasses(FloorPlanBlockEditViewModel block, IReadOnlyList<FloorPlanBlockEditViewModel> floorBlocks)
|
||||||
|
{
|
||||||
|
var classes = new List<string>();
|
||||||
|
foreach (var other in floorBlocks)
|
||||||
|
{
|
||||||
|
if (other.FloorPlanBlockID == block.FloorPlanBlockID || other.LocationID != block.LocationID)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (block.X + block.WidthCells == other.X && RangesOverlap(block.Y, block.HeightCells, other.Y, other.HeightCells))
|
||||||
|
{
|
||||||
|
classes.Add("merge-east");
|
||||||
|
}
|
||||||
|
if (other.X + other.WidthCells == block.X && RangesOverlap(block.Y, block.HeightCells, other.Y, other.HeightCells))
|
||||||
|
{
|
||||||
|
classes.Add("merge-west");
|
||||||
|
}
|
||||||
|
if (block.Y + block.HeightCells == other.Y && RangesOverlap(block.X, block.WidthCells, other.X, other.WidthCells))
|
||||||
|
{
|
||||||
|
classes.Add("merge-south");
|
||||||
|
}
|
||||||
|
if (other.Y + other.HeightCells == block.Y && RangesOverlap(block.X, block.WidthCells, other.X, other.WidthCells))
|
||||||
|
{
|
||||||
|
classes.Add("merge-north");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join(" ", classes.Distinct());
|
||||||
|
}
|
||||||
|
string TransitionIcon(string transitionType) => transitionType switch
|
||||||
|
{
|
||||||
|
FloorPlanTransitionTypes.Archway => "Arch",
|
||||||
|
FloorPlanTransitionTypes.StairsUp => "Up",
|
||||||
|
FloorPlanTransitionTypes.StairsDown => "Down",
|
||||||
|
FloorPlanTransitionTypes.Ladder => "Ldr",
|
||||||
|
FloorPlanTransitionTypes.Window => "Win",
|
||||||
|
_ => "Door"
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
|
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
|
||||||
@ -128,6 +169,7 @@ else
|
|||||||
var floor = Model.Floors[i];
|
var floor = Model.Floors[i];
|
||||||
var isActive = floor.FloorPlanFloorID == activeFloor?.FloorPlanFloorID;
|
var isActive = floor.FloorPlanFloorID == activeFloor?.FloorPlanFloorID;
|
||||||
var floorBlocks = blocksByFloor.GetValueOrDefault(floor.FloorPlanFloorID) ?? [];
|
var floorBlocks = blocksByFloor.GetValueOrDefault(floor.FloorPlanFloorID) ?? [];
|
||||||
|
var floorTransitions = transitionsByFloor.GetValueOrDefault(floor.FloorPlanFloorID) ?? [];
|
||||||
<div class="floor-plan-floor @(isActive ? "active" : string.Empty)" data-floor-panel="@floor.FloorPlanFloorID">
|
<div class="floor-plan-floor @(isActive ? "active" : string.Empty)" data-floor-panel="@floor.FloorPlanFloorID">
|
||||||
<div class="floor-plan-grid-wrap">
|
<div class="floor-plan-grid-wrap">
|
||||||
<div class="floor-plan-grid"
|
<div class="floor-plan-grid"
|
||||||
@ -147,16 +189,29 @@ else
|
|||||||
{
|
{
|
||||||
var blockIndex = Model.Blocks.FindIndex(x => x.FloorPlanBlockID == block.FloorPlanBlockID);
|
var blockIndex = Model.Blocks.FindIndex(x => x.FloorPlanBlockID == block.FloorPlanBlockID);
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="@BlockClass(block.BlockType)"
|
class="@BlockClass(block.BlockType) @SharedWallClasses(block, floorBlocks)"
|
||||||
style="--x:@block.X;--y:@block.Y;--w:@block.WidthCells;--h:@block.HeightCells;"
|
style="--x:@block.X;--y:@block.Y;--w:@block.WidthCells;--h:@block.HeightCells;"
|
||||||
data-block="@block.FloorPlanBlockID"
|
data-block="@block.FloorPlanBlockID"
|
||||||
data-block-index="@blockIndex"
|
data-block-index="@blockIndex"
|
||||||
data-floor-id="@block.FloorPlanFloorID"
|
data-floor-id="@block.FloorPlanFloorID"
|
||||||
|
data-location-id="@block.LocationID"
|
||||||
title="@block.LocationPath">
|
title="@block.LocationPath">
|
||||||
<span class="floor-plan-block-label">@block.DisplayLabel</span>
|
<span class="floor-plan-block-label">@block.DisplayLabel</span>
|
||||||
<span class="floor-plan-resize-handle" data-resize-handle aria-hidden="true"></span>
|
<span class="floor-plan-resize-handle" data-resize-handle aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
@foreach (var transition in floorTransitions)
|
||||||
|
{
|
||||||
|
var tooltip = $"{transition.FromLocationName} to {transition.ToLocationName}: {transition.TransitionType}";
|
||||||
|
<div class="floor-plan-transition-marker"
|
||||||
|
data-transition-marker="@transition.FloorPlanTransitionID"
|
||||||
|
data-from-location-id="@transition.FromLocationID"
|
||||||
|
data-to-location-id="@transition.ToLocationID"
|
||||||
|
title="@tooltip">
|
||||||
|
<span class="floor-plan-transition-icon">@TransitionIcon(transition.TransitionType)</span>
|
||||||
|
<span class="floor-plan-transition-label">@transition.Label</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -169,6 +224,7 @@ else
|
|||||||
<button type="button" class="floor-plan-toolbox-tab active" id="floor-plan-tool-tab-add" role="tab" aria-selected="true" aria-controls="floor-plan-tool-add" data-toolbox-tab="add">Add block</button>
|
<button type="button" class="floor-plan-toolbox-tab active" id="floor-plan-tool-tab-add" role="tab" aria-selected="true" aria-controls="floor-plan-tool-add" data-toolbox-tab="add">Add block</button>
|
||||||
<button type="button" class="floor-plan-toolbox-tab" id="floor-plan-tool-tab-selected" role="tab" aria-selected="false" aria-controls="floor-plan-tool-selected" data-toolbox-tab="selected">Selected block</button>
|
<button type="button" class="floor-plan-toolbox-tab" id="floor-plan-tool-tab-selected" role="tab" aria-selected="false" aria-controls="floor-plan-tool-selected" data-toolbox-tab="selected">Selected block</button>
|
||||||
<button type="button" class="floor-plan-toolbox-tab" id="floor-plan-tool-tab-background" role="tab" aria-selected="false" aria-controls="floor-plan-tool-background" data-toolbox-tab="background">Background</button>
|
<button type="button" class="floor-plan-toolbox-tab" id="floor-plan-tool-tab-background" role="tab" aria-selected="false" aria-controls="floor-plan-tool-background" data-toolbox-tab="background">Background</button>
|
||||||
|
<button type="button" class="floor-plan-toolbox-tab" id="floor-plan-tool-tab-transitions" role="tab" aria-selected="false" aria-controls="floor-plan-tool-transitions" data-toolbox-tab="transitions">Transitions</button>
|
||||||
<button type="button" class="floor-plan-toolbox-tab" id="floor-plan-tool-tab-floors" role="tab" aria-selected="false" aria-controls="floor-plan-tool-floors" data-toolbox-tab="floors">Floors</button>
|
<button type="button" class="floor-plan-toolbox-tab" id="floor-plan-tool-tab-floors" role="tab" aria-selected="false" aria-controls="floor-plan-tool-floors" data-toolbox-tab="floors">Floors</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="floor-plan-toolbox-body">
|
<div class="floor-plan-toolbox-body">
|
||||||
@ -234,7 +290,7 @@ else
|
|||||||
<input asp-for="Blocks[i].LocationPath" type="hidden" />
|
<input asp-for="Blocks[i].LocationPath" type="hidden" />
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label">Location</label>
|
<label class="form-label">Location</label>
|
||||||
<select asp-for="Blocks[i].LocationID" asp-items="Model.LocationOptions" class="form-select form-select-sm"></select>
|
<select asp-for="Blocks[i].LocationID" asp-items="Model.LocationOptions" class="form-select form-select-sm" data-block-field="location"></select>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label">Type</label>
|
<label class="form-label">Type</label>
|
||||||
@ -383,6 +439,100 @@ else
|
|||||||
}
|
}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="floor-plan-tool-card" id="floor-plan-tool-transitions" role="tabpanel" aria-labelledby="floor-plan-tool-tab-transitions" data-toolbox-panel="transitions" hidden>
|
||||||
|
<h2>Transitions</h2>
|
||||||
|
<p class="muted">Create doors, archways, stairs, and other movement paths for <span data-current-floor-name>@activeFloor?.Name</span>.</p>
|
||||||
|
@foreach (var floor in Model.Floors)
|
||||||
|
{
|
||||||
|
var isActive = floor.FloorPlanFloorID == activeFloor?.FloorPlanFloorID;
|
||||||
|
var floorBlocks = blocksByFloor.GetValueOrDefault(floor.FloorPlanFloorID) ?? [];
|
||||||
|
var floorLocationOptions = floorBlocks
|
||||||
|
.Where(x => x.LocationID.HasValue)
|
||||||
|
.GroupBy(x => x.LocationID!.Value)
|
||||||
|
.Select(x => x.First())
|
||||||
|
.OrderBy(x => x.LocationName)
|
||||||
|
.ToList();
|
||||||
|
var floorTransitions = transitionsByFloor.GetValueOrDefault(floor.FloorPlanFloorID) ?? [];
|
||||||
|
<div class="floor-plan-transition-editor @(isActive ? string.Empty : "d-none")" data-transition-editor="@floor.FloorPlanFloorID">
|
||||||
|
<div class="floor-plan-tool-subsection">
|
||||||
|
<h3>Create transition</h3>
|
||||||
|
@if (floorLocationOptions.Count < 2)
|
||||||
|
{
|
||||||
|
<p class="muted">Add at least two placed Locations on this floor before creating a transition.</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<input type="hidden" name="FloorPlanTransitionID" value="0" form="floor-plan-transition-add-@floor.FloorPlanFloorID" />
|
||||||
|
<input type="hidden" name="FloorPlanFloorID" value="@floor.FloorPlanFloorID" form="floor-plan-transition-add-@floor.FloorPlanFloorID" />
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label" for="transition-from-@floor.FloorPlanFloorID">From Location</label>
|
||||||
|
<select id="transition-from-@floor.FloorPlanFloorID" class="form-select form-select-sm" name="FromLocationID" form="floor-plan-transition-add-@floor.FloorPlanFloorID">
|
||||||
|
<option value="">Choose a Location</option>
|
||||||
|
@foreach (var location in floorLocationOptions)
|
||||||
|
{
|
||||||
|
<option value="@location.LocationID">@location.LocationPath</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label" for="transition-to-@floor.FloorPlanFloorID">To Location</label>
|
||||||
|
<select id="transition-to-@floor.FloorPlanFloorID" class="form-select form-select-sm" name="ToLocationID" form="floor-plan-transition-add-@floor.FloorPlanFloorID">
|
||||||
|
<option value="">Choose a Location</option>
|
||||||
|
@foreach (var location in floorLocationOptions)
|
||||||
|
{
|
||||||
|
<option value="@location.LocationID">@location.LocationPath</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label" for="transition-type-@floor.FloorPlanFloorID">Transition Type</label>
|
||||||
|
<select id="transition-type-@floor.FloorPlanFloorID" class="form-select form-select-sm" name="TransitionType" asp-items="Model.TransitionTypeOptions" form="floor-plan-transition-add-@floor.FloorPlanFloorID"></select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label" for="transition-label-@floor.FloorPlanFloorID">Display Label</label>
|
||||||
|
<input id="transition-label-@floor.FloorPlanFloorID" class="form-control form-control-sm" name="DisplayLabel" form="floor-plan-transition-add-@floor.FloorPlanFloorID" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label" for="transition-notes-@floor.FloorPlanFloorID">Notes</label>
|
||||||
|
<textarea id="transition-notes-@floor.FloorPlanFloorID" class="form-control form-control-sm" name="Notes" rows="2" form="floor-plan-transition-add-@floor.FloorPlanFloorID"></textarea>
|
||||||
|
</div>
|
||||||
|
<label class="floor-plan-checkbox">
|
||||||
|
<input type="checkbox" name="IsLocked" value="true" form="floor-plan-transition-add-@floor.FloorPlanFloorID" />
|
||||||
|
Locked
|
||||||
|
</label>
|
||||||
|
<button class="btn btn-primary btn-sm w-100 mt-2" type="submit" form="floor-plan-transition-add-@floor.FloorPlanFloorID">Create transition</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="floor-plan-tool-subsection">
|
||||||
|
<h3>Existing transitions</h3>
|
||||||
|
@if (!floorTransitions.Any())
|
||||||
|
{
|
||||||
|
<p class="muted">No transitions on this floor yet.</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="floor-plan-transition-list">
|
||||||
|
@foreach (var transition in floorTransitions)
|
||||||
|
{
|
||||||
|
<article class="floor-plan-transition-item">
|
||||||
|
<div>
|
||||||
|
<strong>@transition.FromLocationName to @transition.ToLocationName</strong>
|
||||||
|
<span>@transition.TransitionType</span>
|
||||||
|
</div>
|
||||||
|
<div class="button-row">
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#edit-transition-@transition.FloorPlanTransitionID">Edit</button>
|
||||||
|
<button class="btn btn-outline-danger btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#delete-transition-@transition.FloorPlanTransitionID">Delete</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="floor-plan-tool-card" id="floor-plan-tool-floors" role="tabpanel" aria-labelledby="floor-plan-tool-tab-floors" data-toolbox-panel="floors" hidden>
|
<section class="floor-plan-tool-card" id="floor-plan-tool-floors" role="tabpanel" aria-labelledby="floor-plan-tool-tab-floors" data-toolbox-panel="floors" hidden>
|
||||||
<h2>Floors</h2>
|
<h2>Floors</h2>
|
||||||
<div class="floor-plan-tool-subsection">
|
<div class="floor-plan-tool-subsection">
|
||||||
@ -471,6 +621,10 @@ else
|
|||||||
<input type="hidden" name="floorPlanId" value="@Model.FloorPlan.FloorPlanID" />
|
<input type="hidden" name="floorPlanId" value="@Model.FloorPlan.FloorPlanID" />
|
||||||
<input type="hidden" name="floorPlanFloorId" value="@floor.FloorPlanFloorID" />
|
<input type="hidden" name="floorPlanFloorId" value="@floor.FloorPlanFloorID" />
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<form id="floor-plan-transition-add-@floor.FloorPlanFloorID" asp-action="SaveTransition" method="post">
|
||||||
|
<input type="hidden" name="floorPlanId" value="@Model.FloorPlan.FloorPlanID" />
|
||||||
|
</form>
|
||||||
}
|
}
|
||||||
|
|
||||||
@foreach (var floor in Model.Floors)
|
@foreach (var floor in Model.Floors)
|
||||||
@ -526,6 +680,100 @@ else
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@foreach (var transition in Model.Transitions)
|
||||||
|
{
|
||||||
|
var transitionFloorBlocks = blocksByFloor.GetValueOrDefault(transition.FloorPlanFloorID) ?? [];
|
||||||
|
var transitionLocationOptions = transitionFloorBlocks
|
||||||
|
.Where(x => x.LocationID.HasValue)
|
||||||
|
.GroupBy(x => x.LocationID!.Value)
|
||||||
|
.Select(x => x.First())
|
||||||
|
.OrderBy(x => x.LocationName)
|
||||||
|
.ToList();
|
||||||
|
<div class="modal fade" id="edit-transition-@transition.FloorPlanTransitionID" tabindex="-1" aria-labelledby="edit-transition-title-@transition.FloorPlanTransitionID" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 class="modal-title fs-5" id="edit-transition-title-@transition.FloorPlanTransitionID">Edit transition</h2>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<form asp-action="SaveTransition" method="post">
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" name="floorPlanId" value="@Model.FloorPlan.FloorPlanID" />
|
||||||
|
<input type="hidden" name="FloorPlanTransitionID" value="@transition.FloorPlanTransitionID" />
|
||||||
|
<input type="hidden" name="FloorPlanFloorID" value="@transition.FloorPlanFloorID" />
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label">From Location</label>
|
||||||
|
<select class="form-select form-select-sm" name="FromLocationID">
|
||||||
|
@foreach (var location in transitionLocationOptions)
|
||||||
|
{
|
||||||
|
<option value="@location.LocationID" selected="@(location.LocationID == transition.FromLocationID)">@location.LocationPath</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label">To Location</label>
|
||||||
|
<select class="form-select form-select-sm" name="ToLocationID">
|
||||||
|
@foreach (var location in transitionLocationOptions)
|
||||||
|
{
|
||||||
|
<option value="@location.LocationID" selected="@(location.LocationID == transition.ToLocationID)">@location.LocationPath</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label">Transition Type</label>
|
||||||
|
<select class="form-select form-select-sm" name="TransitionType">
|
||||||
|
@foreach (var option in Model.TransitionTypeOptions)
|
||||||
|
{
|
||||||
|
<option value="@option.Value" selected="@(option.Value == transition.TransitionType)">@option.Text</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label">Display Label</label>
|
||||||
|
<input class="form-control form-control-sm" name="DisplayLabel" value="@transition.DisplayLabel" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label">Notes</label>
|
||||||
|
<textarea class="form-control form-control-sm" name="Notes" rows="3">@transition.Notes</textarea>
|
||||||
|
</div>
|
||||||
|
<label class="floor-plan-checkbox">
|
||||||
|
<input type="checkbox" name="IsLocked" value="true" @(transition.IsLocked ? "checked" : string.Empty) />
|
||||||
|
Locked
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button class="btn btn-primary" type="submit">Save transition</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="delete-transition-@transition.FloorPlanTransitionID" tabindex="-1" aria-labelledby="delete-transition-title-@transition.FloorPlanTransitionID" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content plotline-confirm-modal">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 class="modal-title fs-5" id="delete-transition-title-@transition.FloorPlanTransitionID">Delete transition?</h2>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Delete the <strong>@transition.TransitionType</strong> transition from <strong>@transition.FromLocationName</strong> to <strong>@transition.ToLocationName</strong>?</p>
|
||||||
|
<p>Locations and blocks will not be deleted.</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<form asp-action="DeleteTransition" method="post" data-no-delete-confirm="true">
|
||||||
|
<input type="hidden" name="floorPlanId" value="@Model.FloorPlan.FloorPlanID" />
|
||||||
|
<input type="hidden" name="floorPlanTransitionId" value="@transition.FloorPlanTransitionID" />
|
||||||
|
<button class="btn btn-danger" type="submit">Delete transition</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
@foreach (var block in Model.Blocks)
|
@foreach (var block in Model.Blocks)
|
||||||
{
|
{
|
||||||
<div class="modal fade" id="delete-block-@block.FloorPlanBlockID" tabindex="-1" aria-labelledby="delete-block-title-@block.FloorPlanBlockID" aria-hidden="true">
|
<div class="modal fade" id="delete-block-@block.FloorPlanBlockID" tabindex="-1" aria-labelledby="delete-block-title-@block.FloorPlanBlockID" aria-hidden="true">
|
||||||
@ -564,6 +812,7 @@ else
|
|||||||
const panels = [...editor.querySelectorAll("[data-floor-panel]")];
|
const panels = [...editor.querySelectorAll("[data-floor-panel]")];
|
||||||
const floorEditors = [...editor.querySelectorAll("[data-floor-editor]")];
|
const floorEditors = [...editor.querySelectorAll("[data-floor-editor]")];
|
||||||
const backgroundEditors = [...editor.querySelectorAll("[data-background-editor]")];
|
const backgroundEditors = [...editor.querySelectorAll("[data-background-editor]")];
|
||||||
|
const transitionEditors = [...editor.querySelectorAll("[data-transition-editor]")];
|
||||||
const blocks = [...editor.querySelectorAll("[data-block]")];
|
const blocks = [...editor.querySelectorAll("[data-block]")];
|
||||||
const noSelection = editor.querySelector("[data-no-selection]");
|
const noSelection = editor.querySelector("[data-no-selection]");
|
||||||
const warning = editor.querySelector("[data-overlap-warning]");
|
const warning = editor.querySelector("[data-overlap-warning]");
|
||||||
@ -701,6 +950,11 @@ else
|
|||||||
panel.classList.toggle("d-none", !active);
|
panel.classList.toggle("d-none", !active);
|
||||||
panel.hidden = !active;
|
panel.hidden = !active;
|
||||||
});
|
});
|
||||||
|
transitionEditors.forEach(panel => {
|
||||||
|
const active = panel.dataset.transitionEditor === floorId;
|
||||||
|
panel.classList.toggle("d-none", !active);
|
||||||
|
panel.hidden = !active;
|
||||||
|
});
|
||||||
const activeTab = tabs.find(tab => tab.dataset.floorTab === floorId);
|
const activeTab = tabs.find(tab => tab.dataset.floorTab === floorId);
|
||||||
if (addBlockFloor) addBlockFloor.value = floorId;
|
if (addBlockFloor) addBlockFloor.value = floorId;
|
||||||
if (activeTab) {
|
if (activeTab) {
|
||||||
@ -817,6 +1071,7 @@ else
|
|||||||
y: panel?.querySelector("[data-block-field='y']"),
|
y: panel?.querySelector("[data-block-field='y']"),
|
||||||
w: panel?.querySelector("[data-block-field='w']"),
|
w: panel?.querySelector("[data-block-field='w']"),
|
||||||
h: panel?.querySelector("[data-block-field='h']"),
|
h: panel?.querySelector("[data-block-field='h']"),
|
||||||
|
location: panel?.querySelector("[data-block-field='location']"),
|
||||||
type: panel?.querySelector("[data-block-field='type']")
|
type: panel?.querySelector("[data-block-field='type']")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -860,6 +1115,51 @@ else
|
|||||||
checkOverlaps();
|
checkOverlaps();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rangesOverlap(aStart, aLength, bStart, bLength) {
|
||||||
|
return aStart < bStart + bLength && bStart < aStart + aLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshSharedWalls(activePanel) {
|
||||||
|
const visibleBlocks = [...activePanel.querySelectorAll("[data-block]")].map(block => ({ block, ...readBlock(block) }));
|
||||||
|
visibleBlocks.forEach(item => item.block.classList.remove("merge-east", "merge-west", "merge-north", "merge-south"));
|
||||||
|
visibleBlocks.forEach(a => {
|
||||||
|
visibleBlocks.forEach(b => {
|
||||||
|
if (a === b || !a.block.dataset.locationId || a.block.dataset.locationId !== b.block.dataset.locationId) return;
|
||||||
|
if (a.x + a.w === b.x && rangesOverlap(a.y, a.h, b.y, b.h)) a.block.classList.add("merge-east");
|
||||||
|
if (b.x + b.w === a.x && rangesOverlap(a.y, a.h, b.y, b.h)) a.block.classList.add("merge-west");
|
||||||
|
if (a.y + a.h === b.y && rangesOverlap(a.x, a.w, b.x, b.w)) a.block.classList.add("merge-south");
|
||||||
|
if (b.y + b.h === a.y && rangesOverlap(a.x, a.w, b.x, b.w)) a.block.classList.add("merge-north");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return visibleBlocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
function locationCenter(blocksForLocation) {
|
||||||
|
if (blocksForLocation.length === 0) return null;
|
||||||
|
const minX = Math.min(...blocksForLocation.map(item => item.x));
|
||||||
|
const minY = Math.min(...blocksForLocation.map(item => item.y));
|
||||||
|
const maxX = Math.max(...blocksForLocation.map(item => item.x + item.w));
|
||||||
|
const maxY = Math.max(...blocksForLocation.map(item => item.y + item.h));
|
||||||
|
return { x: (minX + maxX) / 2, y: (minY + maxY) / 2 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function positionTransitions(activePanel, visibleBlocks) {
|
||||||
|
activePanel.querySelectorAll("[data-transition-marker]").forEach(marker => {
|
||||||
|
const fromBlocks = visibleBlocks.filter(item => item.block.dataset.locationId === marker.dataset.fromLocationId);
|
||||||
|
const toBlocks = visibleBlocks.filter(item => item.block.dataset.locationId === marker.dataset.toLocationId);
|
||||||
|
const fromCenter = locationCenter(fromBlocks);
|
||||||
|
const toCenter = locationCenter(toBlocks);
|
||||||
|
if (!fromCenter || !toCenter) {
|
||||||
|
marker.hidden = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
marker.hidden = false;
|
||||||
|
marker.style.setProperty("--transition-x", (fromCenter.x + toCenter.x) / 2);
|
||||||
|
marker.style.setProperty("--transition-y", (fromCenter.y + toCenter.y) / 2);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function cellFromPointer(grid, event) {
|
function cellFromPointer(grid, event) {
|
||||||
const rect = grid.getBoundingClientRect();
|
const rect = grid.getBoundingClientRect();
|
||||||
const gridWidth = Number(grid.dataset.gridWidth || 24);
|
const gridWidth = Number(grid.dataset.gridWidth || 24);
|
||||||
@ -873,7 +1173,8 @@ else
|
|||||||
function checkOverlaps() {
|
function checkOverlaps() {
|
||||||
const activePanel = panels.find(panel => panel.classList.contains("active"));
|
const activePanel = panels.find(panel => panel.classList.contains("active"));
|
||||||
if (!activePanel) return;
|
if (!activePanel) return;
|
||||||
const visibleBlocks = [...activePanel.querySelectorAll("[data-block]")].map(block => ({ block, ...readBlock(block) }));
|
const visibleBlocks = refreshSharedWalls(activePanel);
|
||||||
|
positionTransitions(activePanel, visibleBlocks);
|
||||||
let overlaps = false;
|
let overlaps = false;
|
||||||
visibleBlocks.forEach(a => {
|
visibleBlocks.forEach(a => {
|
||||||
a.block.classList.remove("overlap");
|
a.block.classList.remove("overlap");
|
||||||
@ -981,10 +1282,20 @@ else
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
editor.querySelectorAll("[data-block-field='location']").forEach(select => {
|
||||||
|
select.addEventListener("change", () => {
|
||||||
|
if (!selectedBlock) return;
|
||||||
|
selectedBlock.dataset.locationId = select.value || "";
|
||||||
|
checkOverlaps();
|
||||||
|
markDirty();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
editor.querySelectorAll("input, select, textarea").forEach(input => {
|
editor.querySelectorAll("input, select, textarea").forEach(input => {
|
||||||
if (input.matches("[form='floor-plan-add-block-form'], [form='floor-plan-add-floor-form']")) return;
|
if (input.matches("[form='floor-plan-add-block-form'], [form='floor-plan-add-floor-form']")) return;
|
||||||
|
if (input.matches("[form^='floor-plan-background-'], [form^='floor-plan-transition-add-']")) return;
|
||||||
if (input.matches("[data-background-field]")) return;
|
if (input.matches("[data-background-field]")) return;
|
||||||
if (input.matches("[data-block-field='x'],[data-block-field='y'],[data-block-field='w'],[data-block-field='h'],[data-block-field='type']")) return;
|
if (input.matches("[data-block-field='x'],[data-block-field='y'],[data-block-field='w'],[data-block-field='h'],[data-block-field='type'],[data-block-field='location']")) return;
|
||||||
const eventName = input.tagName === "TEXTAREA" || input.type === "text" ? "input" : "change";
|
const eventName = input.tagName === "TEXTAREA" || input.type === "text" ? "input" : "change";
|
||||||
input.addEventListener(eventName, markDirty);
|
input.addEventListener(eventName, markDirty);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4662,11 +4662,67 @@ body.dragging-location [data-drag-type="location"] {
|
|||||||
z-index: 3;
|
z-index: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.floor-plan-block.merge-east {
|
||||||
|
border-right-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-plan-block.merge-west {
|
||||||
|
border-left-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-plan-block.merge-north {
|
||||||
|
border-top-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-plan-block.merge-south {
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-plan-block.selected.merge-east,
|
||||||
|
.floor-plan-block.selected.merge-west,
|
||||||
|
.floor-plan-block.selected.merge-north,
|
||||||
|
.floor-plan-block.selected.merge-south {
|
||||||
|
outline-offset: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.floor-plan-block.overlap {
|
.floor-plan-block.overlap {
|
||||||
border-color: #b45544;
|
border-color: #b45544;
|
||||||
box-shadow: inset 0 0 0 2px rgba(180, 85, 68, .18);
|
box-shadow: inset 0 0 0 2px rgba(180, 85, 68, .18);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.floor-plan-transition-marker {
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(255, 250, 242, .94);
|
||||||
|
border: 1px solid rgba(47, 111, 99, .3);
|
||||||
|
border-radius: 999px;
|
||||||
|
box-shadow: 0 4px 12px rgba(31, 37, 34, .16);
|
||||||
|
color: #12332c;
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 5px;
|
||||||
|
left: calc(var(--transition-x, 0) * var(--cell-size));
|
||||||
|
max-width: 150px;
|
||||||
|
padding: 3px 7px;
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
top: calc(var(--transition-y, 0) * var(--cell-size));
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-plan-transition-icon {
|
||||||
|
font-size: .68rem;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-plan-transition-label {
|
||||||
|
font-size: .68rem;
|
||||||
|
font-weight: 800;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.floor-plan-block--room {
|
.floor-plan-block--room {
|
||||||
background: #d9eee7;
|
background: #d9eee7;
|
||||||
}
|
}
|
||||||
@ -4726,7 +4782,7 @@ body.dragging-location [data-drag-type="location"] {
|
|||||||
|
|
||||||
.floor-plan-toolbox-tabs {
|
.floor-plan-toolbox-tabs {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
border-bottom: 1px solid var(--border-color, #d8ded9);
|
border-bottom: 1px solid var(--border-color, #d8ded9);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4808,6 +4864,29 @@ body.dragging-location [data-drag-type="location"] {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.floor-plan-transition-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-plan-transition-item {
|
||||||
|
border: 1px solid rgba(47, 111, 99, .14);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-plan-transition-item strong,
|
||||||
|
.floor-plan-transition-item span {
|
||||||
|
display: block;
|
||||||
|
font-size: .84rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-plan-transition-item span {
|
||||||
|
color: var(--muted-text, #5d6f65);
|
||||||
|
}
|
||||||
|
|
||||||
.floor-plan-checkbox {
|
.floor-plan-checkbox {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@ -4872,6 +4951,18 @@ body.dragging-location [data-drag-type="location"] {
|
|||||||
color: #f1f6f3;
|
color: #f1f6f3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-bs-theme="dark"] .floor-plan-transition-marker,
|
||||||
|
.dark .floor-plan-transition-marker {
|
||||||
|
background: rgba(42, 35, 30, .95);
|
||||||
|
border-color: rgba(212, 154, 98, .42);
|
||||||
|
color: #f4eadc;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-bs-theme="dark"] .floor-plan-transition-item,
|
||||||
|
.dark .floor-plan-transition-item {
|
||||||
|
border-color: rgba(212, 154, 98, .22);
|
||||||
|
}
|
||||||
|
|
||||||
[data-bs-theme="dark"] .floor-plan-block--room,
|
[data-bs-theme="dark"] .floor-plan-block--room,
|
||||||
.dark .floor-plan-block--room {
|
.dark .floor-plan-block--room {
|
||||||
background: #29584f;
|
background: #29584f;
|
||||||
|
|||||||
2
PlotLine/wwwroot/css/site.min.css
vendored
2
PlotLine/wwwroot/css/site.min.css
vendored
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user