Implemented the next Floor Plans enhancement pass.
This commit is contained in:
parent
9fc4374df3
commit
f57913014c
@ -663,7 +663,8 @@ public sealed class FloorPlanRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
transition.TransitionType,
|
transition.TransitionType,
|
||||||
transition.Notes,
|
transition.Notes,
|
||||||
transition.IsLocked,
|
transition.IsLocked,
|
||||||
transition.DisplayLabel
|
transition.DisplayLabel,
|
||||||
|
transition.PositionWithinLocation
|
||||||
},
|
},
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1201,8 +1201,12 @@ public static class FloorPlanTransitionTypes
|
|||||||
public const string DoubleDoor = "Double Door";
|
public const string DoubleDoor = "Double Door";
|
||||||
public const string Archway = "Archway";
|
public const string Archway = "Archway";
|
||||||
public const string OpenPassage = "Open Passage";
|
public const string OpenPassage = "Open Passage";
|
||||||
|
public const string HiddenPassage = "Hidden Passage";
|
||||||
public const string FrenchDoors = "French Doors";
|
public const string FrenchDoors = "French Doors";
|
||||||
public const string SlidingDoors = "Sliding Doors";
|
public const string SlidingDoors = "Sliding Doors";
|
||||||
|
public const string ExteriorDoor = "Exterior Door";
|
||||||
|
public const string HiddenDoor = "Hidden Door";
|
||||||
|
public const string BlockedDoor = "Blocked Door";
|
||||||
public const string StairsUp = "Stairs Up";
|
public const string StairsUp = "Stairs Up";
|
||||||
public const string StairsDown = "Stairs Down";
|
public const string StairsDown = "Stairs Down";
|
||||||
public const string Ladder = "Ladder";
|
public const string Ladder = "Ladder";
|
||||||
@ -1215,8 +1219,12 @@ public static class FloorPlanTransitionTypes
|
|||||||
DoubleDoor,
|
DoubleDoor,
|
||||||
Archway,
|
Archway,
|
||||||
OpenPassage,
|
OpenPassage,
|
||||||
|
HiddenPassage,
|
||||||
FrenchDoors,
|
FrenchDoors,
|
||||||
SlidingDoors,
|
SlidingDoors,
|
||||||
|
ExteriorDoor,
|
||||||
|
HiddenDoor,
|
||||||
|
BlockedDoor,
|
||||||
StairsUp,
|
StairsUp,
|
||||||
StairsDown,
|
StairsDown,
|
||||||
Ladder,
|
Ladder,
|
||||||
@ -1224,7 +1232,44 @@ public static class FloorPlanTransitionTypes
|
|||||||
Window
|
Window
|
||||||
];
|
];
|
||||||
|
|
||||||
public static bool IsValid(string? transitionType) => All.Contains(transitionType ?? string.Empty, StringComparer.OrdinalIgnoreCase);
|
public static bool IsValid(string? transitionType)
|
||||||
|
{
|
||||||
|
if (All.Contains(transitionType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = new string((transitionType ?? string.Empty).Where(char.IsLetterOrDigit).Select(char.ToLowerInvariant).ToArray());
|
||||||
|
return normalized is "frenchdoor" or "slidingdoor";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class FloorPlanTransitionPositions
|
||||||
|
{
|
||||||
|
public const string Centre = "Centre";
|
||||||
|
public const string North = "North";
|
||||||
|
public const string South = "South";
|
||||||
|
public const string East = "East";
|
||||||
|
public const string West = "West";
|
||||||
|
public const string NorthWest = "North West";
|
||||||
|
public const string NorthEast = "North East";
|
||||||
|
public const string SouthWest = "South West";
|
||||||
|
public const string SouthEast = "South East";
|
||||||
|
|
||||||
|
public static readonly IReadOnlyList<string> All =
|
||||||
|
[
|
||||||
|
Centre,
|
||||||
|
North,
|
||||||
|
South,
|
||||||
|
East,
|
||||||
|
West,
|
||||||
|
NorthWest,
|
||||||
|
NorthEast,
|
||||||
|
SouthWest,
|
||||||
|
SouthEast
|
||||||
|
];
|
||||||
|
|
||||||
|
public static bool IsValid(string? position) => All.Contains(position ?? string.Empty, StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class FloorPlanTransition
|
public sealed class FloorPlanTransition
|
||||||
@ -1241,6 +1286,7 @@ public sealed class FloorPlanTransition
|
|||||||
public string? Notes { get; set; }
|
public string? Notes { get; set; }
|
||||||
public bool IsLocked { get; set; }
|
public bool IsLocked { get; set; }
|
||||||
public string? DisplayLabel { get; set; }
|
public string? DisplayLabel { get; set; }
|
||||||
|
public string PositionWithinLocation { get; set; } = FloorPlanTransitionPositions.Centre;
|
||||||
public DateTime CreatedDate { get; set; }
|
public DateTime CreatedDate { get; set; }
|
||||||
public DateTime? ModifiedDate { get; set; }
|
public DateTime? ModifiedDate { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7598,7 +7598,8 @@ public sealed class FloorPlanService(
|
|||||||
NewTransition = new FloorPlanTransitionEditViewModel
|
NewTransition = new FloorPlanTransitionEditViewModel
|
||||||
{
|
{
|
||||||
FloorPlanFloorID = orderedFloors.FirstOrDefault()?.FloorPlanFloorID ?? 0,
|
FloorPlanFloorID = orderedFloors.FirstOrDefault()?.FloorPlanFloorID ?? 0,
|
||||||
TransitionType = FloorPlanTransitionTypes.Door
|
TransitionType = FloorPlanTransitionTypes.Door,
|
||||||
|
PositionWithinLocation = FloorPlanTransitionPositions.Centre
|
||||||
},
|
},
|
||||||
LocationOptions = ToOptionalLocationOptions(projectLocations, "Create/link automatically"),
|
LocationOptions = ToOptionalLocationOptions(projectLocations, "Create/link automatically"),
|
||||||
Locations = projectLocations,
|
Locations = projectLocations,
|
||||||
@ -7732,7 +7733,11 @@ public sealed class FloorPlanService(
|
|||||||
var floor = data.Floors.FirstOrDefault(x => x.FloorPlanFloorID == model.FloorPlanFloorID)
|
var floor = data.Floors.FirstOrDefault(x => x.FloorPlanFloorID == model.FloorPlanFloorID)
|
||||||
?? throw new InvalidOperationException("Floor not found.");
|
?? throw new InvalidOperationException("Floor not found.");
|
||||||
|
|
||||||
ValidateTransition(model, floor, data.Blocks, data.Transitions);
|
ValidateTransition(model, floor, data.Blocks, data.Transitions, data.Floors);
|
||||||
|
if (!IsStairTransition(model.TransitionType))
|
||||||
|
{
|
||||||
|
model.PositionWithinLocation = FloorPlanTransitionPositions.Centre;
|
||||||
|
}
|
||||||
var transitionId = await floorPlans.SaveTransitionAsync(ToTransition(model));
|
var transitionId = await floorPlans.SaveTransitionAsync(ToTransition(model));
|
||||||
await activity.RecordAsync(data.FloorPlan.ProjectID, model.FloorPlanTransitionID == 0 ? "Created" : "Updated", "Floor Plan Transition", transitionId, model.TransitionType);
|
await activity.RecordAsync(data.FloorPlan.ProjectID, model.FloorPlanTransitionID == 0 ? "Created" : "Updated", "Floor Plan Transition", transitionId, model.TransitionType);
|
||||||
return transitionId;
|
return transitionId;
|
||||||
@ -7860,11 +7865,43 @@ public sealed class FloorPlanService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string NormalizeTransitionType(string? transitionType)
|
||||||
|
=> new((transitionType ?? string.Empty).Where(char.IsLetterOrDigit).Select(char.ToLowerInvariant).ToArray());
|
||||||
|
|
||||||
|
private static bool IsSameFloorAdjacentTransition(string? transitionType)
|
||||||
|
=> NormalizeTransitionType(transitionType) is "door"
|
||||||
|
or "doubledoor"
|
||||||
|
or "frenchdoor"
|
||||||
|
or "frenchdoors"
|
||||||
|
or "slidingdoor"
|
||||||
|
or "slidingdoors"
|
||||||
|
or "window"
|
||||||
|
or "archway"
|
||||||
|
or "exteriordoor"
|
||||||
|
or "hiddendoor"
|
||||||
|
or "blockeddoor";
|
||||||
|
|
||||||
|
private static bool IsUnrestrictedTransition(string? transitionType)
|
||||||
|
=> NormalizeTransitionType(transitionType) is "hiddenpassage" or "openpassage" or "secretpassage";
|
||||||
|
|
||||||
|
private static bool IsStairTransition(string? transitionType)
|
||||||
|
=> NormalizeTransitionType(transitionType) is "stairsup" or "stairsdown";
|
||||||
|
|
||||||
|
private static bool BlocksTouchAlongEdge(FloorPlanBlock a, FloorPlanBlock b)
|
||||||
|
{
|
||||||
|
var verticalTouch = a.X + a.WidthCells == b.X || b.X + b.WidthCells == a.X;
|
||||||
|
var verticalOverlap = a.Y < b.Y + b.HeightCells && b.Y < a.Y + a.HeightCells;
|
||||||
|
var horizontalTouch = a.Y + a.HeightCells == b.Y || b.Y + b.HeightCells == a.Y;
|
||||||
|
var horizontalOverlap = a.X < b.X + b.WidthCells && b.X < a.X + a.WidthCells;
|
||||||
|
return (verticalTouch && verticalOverlap) || (horizontalTouch && horizontalOverlap);
|
||||||
|
}
|
||||||
|
|
||||||
private static void ValidateTransition(
|
private static void ValidateTransition(
|
||||||
FloorPlanTransitionEditViewModel model,
|
FloorPlanTransitionEditViewModel model,
|
||||||
FloorPlanFloor floor,
|
FloorPlanFloor floor,
|
||||||
IReadOnlyList<FloorPlanBlock> blocks,
|
IReadOnlyList<FloorPlanBlock> blocks,
|
||||||
IReadOnlyList<FloorPlanTransition> transitions)
|
IReadOnlyList<FloorPlanTransition> transitions,
|
||||||
|
IReadOnlyList<FloorPlanFloor> floors)
|
||||||
{
|
{
|
||||||
if (!FloorPlanTransitionTypes.IsValid(model.TransitionType))
|
if (!FloorPlanTransitionTypes.IsValid(model.TransitionType))
|
||||||
{
|
{
|
||||||
@ -7881,16 +7918,65 @@ public sealed class FloorPlanService(
|
|||||||
throw new InvalidOperationException("From and To locations must be different.");
|
throw new InvalidOperationException("From and To locations must be different.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var floorLocationIds = blocks
|
if (!FloorPlanTransitionPositions.IsValid(model.PositionWithinLocation))
|
||||||
.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.");
|
throw new InvalidOperationException("Choose a valid stair position.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var currentFloorBlocks = blocks
|
||||||
|
.Where(x => x.FloorPlanFloorID == floor.FloorPlanFloorID)
|
||||||
|
.ToList();
|
||||||
|
var floorLocationIds = currentFloorBlocks.Select(x => x.LocationID).ToHashSet();
|
||||||
|
|
||||||
|
if (!floorLocationIds.Contains(model.FromLocationID.Value))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("From Location must exist on the active floor.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsStairTransition(model.TransitionType))
|
||||||
|
{
|
||||||
|
var targetLevel = floor.SortOrder + (NormalizeTransitionType(model.TransitionType) == "stairsup" ? 1 : -1);
|
||||||
|
var targetFloorIds = floors.Where(x => x.SortOrder == targetLevel).Select(x => x.FloorPlanFloorID).ToHashSet();
|
||||||
|
var targetLocationIds = blocks
|
||||||
|
.Where(x => targetFloorIds.Contains(x.FloorPlanFloorID))
|
||||||
|
.Select(x => x.LocationID)
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
if (!targetLocationIds.Contains(model.ToLocationID.Value))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Stairs must connect to a location on the adjacent floor level.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsUnrestrictedTransition(model.TransitionType))
|
||||||
|
{
|
||||||
|
var planLocationIds = blocks.Select(x => x.LocationID).ToHashSet();
|
||||||
|
if (!planLocationIds.Contains(model.ToLocationID.Value))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("To Location must exist in this floor plan.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsSameFloorAdjacentTransition(model.TransitionType))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Choose a valid transition type.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!floorLocationIds.Contains(model.ToLocationID.Value))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("To Location must exist on the active floor.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var fromBlocks = currentFloorBlocks.Where(x => x.LocationID == model.FromLocationID.Value).ToList();
|
||||||
|
var toBlocks = currentFloorBlocks.Where(x => x.LocationID == model.ToLocationID.Value).ToList();
|
||||||
|
if (!fromBlocks.Any(from => toBlocks.Any(to => BlocksTouchAlongEdge(from, to))))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Door and window transitions must connect adjacent locations.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<(FloorPlan Plan, FloorPlanFloor Floor)> GetPlanFloorAsync(int floorPlanId, int floorPlanFloorId)
|
private async Task<(FloorPlan Plan, FloorPlanFloor Floor)> GetPlanFloorAsync(int floorPlanId, int floorPlanFloorId)
|
||||||
@ -8111,7 +8197,10 @@ public sealed class FloorPlanService(
|
|||||||
TransitionType = transition.TransitionType,
|
TransitionType = transition.TransitionType,
|
||||||
Notes = transition.Notes,
|
Notes = transition.Notes,
|
||||||
IsLocked = transition.IsLocked,
|
IsLocked = transition.IsLocked,
|
||||||
DisplayLabel = transition.DisplayLabel
|
DisplayLabel = transition.DisplayLabel,
|
||||||
|
PositionWithinLocation = string.IsNullOrWhiteSpace(transition.PositionWithinLocation)
|
||||||
|
? FloorPlanTransitionPositions.Centre
|
||||||
|
: transition.PositionWithinLocation
|
||||||
};
|
};
|
||||||
|
|
||||||
private static FloorPlanTransition ToTransition(FloorPlanTransitionEditViewModel model) => new()
|
private static FloorPlanTransition ToTransition(FloorPlanTransitionEditViewModel model) => new()
|
||||||
@ -8123,7 +8212,10 @@ public sealed class FloorPlanService(
|
|||||||
TransitionType = model.TransitionType,
|
TransitionType = model.TransitionType,
|
||||||
Notes = model.Notes,
|
Notes = model.Notes,
|
||||||
IsLocked = model.IsLocked,
|
IsLocked = model.IsLocked,
|
||||||
DisplayLabel = model.DisplayLabel
|
DisplayLabel = model.DisplayLabel,
|
||||||
|
PositionWithinLocation = string.IsNullOrWhiteSpace(model.PositionWithinLocation)
|
||||||
|
? FloorPlanTransitionPositions.Centre
|
||||||
|
: model.PositionWithinLocation
|
||||||
};
|
};
|
||||||
|
|
||||||
private static string BlockTypeLabel(string blockType) => blockType switch
|
private static string BlockTypeLabel(string blockType) => blockType switch
|
||||||
|
|||||||
145
PlotLine/Sql/077_FloorPlanStairTransitionPlacement.sql
Normal file
145
PlotLine/Sql/077_FloorPlanStairTransitionPlacement.sql
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
IF COL_LENGTH(N'dbo.FloorPlanTransitions', N'PositionWithinLocation') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE dbo.FloorPlanTransitions
|
||||||
|
ADD PositionWithinLocation nvarchar(20) NOT NULL
|
||||||
|
CONSTRAINT DF_FloorPlanTransitions_PositionWithinLocation DEFAULT N'Centre';
|
||||||
|
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.PositionWithinLocation,
|
||||||
|
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,
|
||||||
|
@PositionWithinLocation nvarchar(20) = N'Centre'
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
IF @FloorPlanTransitionID = 0
|
||||||
|
BEGIN
|
||||||
|
INSERT dbo.FloorPlanTransitions
|
||||||
|
(
|
||||||
|
FloorPlanFloorID,
|
||||||
|
FromLocationID,
|
||||||
|
ToLocationID,
|
||||||
|
TransitionType,
|
||||||
|
Notes,
|
||||||
|
IsLocked,
|
||||||
|
DisplayLabel,
|
||||||
|
PositionWithinLocation
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@FloorPlanFloorID,
|
||||||
|
@FromLocationID,
|
||||||
|
@ToLocationID,
|
||||||
|
@TransitionType,
|
||||||
|
@Notes,
|
||||||
|
@IsLocked,
|
||||||
|
@DisplayLabel,
|
||||||
|
COALESCE(NULLIF(@PositionWithinLocation, N''), N'Centre')
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT CONVERT(int, SCOPE_IDENTITY()) AS FloorPlanTransitionID;
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
|
||||||
|
UPDATE dbo.FloorPlanTransitions
|
||||||
|
SET
|
||||||
|
FromLocationID = @FromLocationID,
|
||||||
|
ToLocationID = @ToLocationID,
|
||||||
|
TransitionType = @TransitionType,
|
||||||
|
Notes = @Notes,
|
||||||
|
IsLocked = @IsLocked,
|
||||||
|
DisplayLabel = @DisplayLabel,
|
||||||
|
PositionWithinLocation = COALESCE(NULLIF(@PositionWithinLocation, N''), N'Centre'),
|
||||||
|
ModifiedDate = SYSUTCDATETIME()
|
||||||
|
WHERE FloorPlanTransitionID = @FloorPlanTransitionID
|
||||||
|
AND FloorPlanFloorID = @FloorPlanFloorID;
|
||||||
|
|
||||||
|
SELECT @FloorPlanTransitionID AS FloorPlanTransitionID;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
@ -1975,6 +1975,9 @@ public sealed class FloorPlanTransitionEditViewModel
|
|||||||
[StringLength(255)]
|
[StringLength(255)]
|
||||||
public string? DisplayLabel { get; set; }
|
public string? DisplayLabel { get; set; }
|
||||||
|
|
||||||
|
[StringLength(20)]
|
||||||
|
public string PositionWithinLocation { get; set; } = FloorPlanTransitionPositions.Centre;
|
||||||
|
|
||||||
public string Label => string.IsNullOrWhiteSpace(DisplayLabel) ? TransitionType : DisplayLabel;
|
public string Label => string.IsNullOrWhiteSpace(DisplayLabel) ? TransitionType : DisplayLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -28,10 +28,10 @@
|
|||||||
"doubledoor" => "double-door",
|
"doubledoor" => "double-door",
|
||||||
"window" => "window",
|
"window" => "window",
|
||||||
"archway" or "opening" => "archway",
|
"archway" or "opening" => "archway",
|
||||||
"secretdoor" => "secret-door",
|
"secretdoor" or "hiddendoor" => "secret-door",
|
||||||
"blockeddoor" => "blocked-door",
|
"blockeddoor" => "blocked-door",
|
||||||
"exteriordoor" => "exterior-door",
|
"exteriordoor" => "exterior-door",
|
||||||
"stairs" or "stair" => "stairs",
|
"stairs" or "stair" or "stairsup" or "stairsdown" => "stairs",
|
||||||
"passage" => "passage",
|
"passage" => "passage",
|
||||||
_ => "other"
|
_ => "other"
|
||||||
};
|
};
|
||||||
@ -50,6 +50,10 @@
|
|||||||
"passage" => @"<svg viewBox=""0 0 18 18"" aria-hidden=""true"" focusable=""false""><path class=""marker-line"" d=""M3 7h12""/><path class=""marker-line"" d=""M3 11h12""/><path class=""marker-line"" d=""M11 5l4 4-4 4""/><path class=""marker-line"" d=""M7 5L3 9l4 4""/></svg>",
|
"passage" => @"<svg viewBox=""0 0 18 18"" aria-hidden=""true"" focusable=""false""><path class=""marker-line"" d=""M3 7h12""/><path class=""marker-line"" d=""M3 11h12""/><path class=""marker-line"" d=""M11 5l4 4-4 4""/><path class=""marker-line"" d=""M7 5L3 9l4 4""/></svg>",
|
||||||
_ => @"<span aria-hidden=""true"">x</span>"
|
_ => @"<span aria-hidden=""true"">x</span>"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var transitionPositions = FloorPlanTransitionPositions.All
|
||||||
|
.Select(x => new SelectListItem(x, x))
|
||||||
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
|
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
|
||||||
@ -159,7 +163,7 @@
|
|||||||
@foreach (var floor in Model.Floors.OrderBy(x => x.SortOrder).ThenBy(x => x.Name))
|
@foreach (var floor in Model.Floors.OrderBy(x => x.SortOrder).ThenBy(x => x.Name))
|
||||||
{
|
{
|
||||||
var isActive = floor.FloorPlanFloorID == activeFloor?.FloorPlanFloorID;
|
var isActive = floor.FloorPlanFloorID == activeFloor?.FloorPlanFloorID;
|
||||||
<button type="button" class="floor-plan-tab @(isActive ? "active" : string.Empty)" data-floor-tab="@floor.FloorPlanFloorID" data-floor-name="@floor.Name" data-floor-location-id="@floor.LocationID">@floor.Name</button>
|
<button type="button" class="floor-plan-tab @(isActive ? "active" : string.Empty)" data-floor-tab="@floor.FloorPlanFloorID" data-floor-name="@floor.Name" data-floor-order="@floor.SortOrder" data-floor-location-id="@floor.LocationID">@floor.Name</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="button-row">
|
<div class="button-row">
|
||||||
@ -177,7 +181,7 @@
|
|||||||
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) ?? [];
|
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" data-floor-order="@floor.SortOrder">
|
||||||
<div class="floor-plan-grid-wrap">
|
<div class="floor-plan-grid-wrap">
|
||||||
<div class="floor-plan-grid"
|
<div class="floor-plan-grid"
|
||||||
style="--grid-width:@floor.GridWidth;--grid-height:@floor.GridHeight;"
|
style="--grid-width:@floor.GridWidth;--grid-height:@floor.GridHeight;"
|
||||||
@ -202,6 +206,8 @@
|
|||||||
data-block-index="@blockIndex"
|
data-block-index="@blockIndex"
|
||||||
data-floor-id="@block.FloorPlanFloorID"
|
data-floor-id="@block.FloorPlanFloorID"
|
||||||
data-location-id="@block.LocationID"
|
data-location-id="@block.LocationID"
|
||||||
|
data-location-name="@block.LocationName"
|
||||||
|
data-location-path="@block.LocationPath"
|
||||||
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>
|
||||||
@ -216,6 +222,8 @@
|
|||||||
data-from-location-id="@transition.FromLocationID"
|
data-from-location-id="@transition.FromLocationID"
|
||||||
data-to-location-id="@transition.ToLocationID"
|
data-to-location-id="@transition.ToLocationID"
|
||||||
data-transition-type="@markerType"
|
data-transition-type="@markerType"
|
||||||
|
data-transition-kind="@transition.TransitionType"
|
||||||
|
data-position-within-location="@transition.PositionWithinLocation"
|
||||||
title="@transitionDetails"
|
title="@transitionDetails"
|
||||||
aria-label="@transitionDetails">
|
aria-label="@transitionDetails">
|
||||||
@Html.Raw(TransitionMarkerSvg(markerType))
|
@Html.Raw(TransitionMarkerSvg(markerType))
|
||||||
@ -472,17 +480,21 @@
|
|||||||
</h3>
|
</h3>
|
||||||
<div id="transition-create-@floor.FloorPlanFloorID" class="accordion-collapse collapse show" aria-labelledby="transition-create-heading-@floor.FloorPlanFloorID" data-bs-parent="#transition-accordion-@floor.FloorPlanFloorID">
|
<div id="transition-create-@floor.FloorPlanFloorID" class="accordion-collapse collapse show" aria-labelledby="transition-create-heading-@floor.FloorPlanFloorID" data-bs-parent="#transition-accordion-@floor.FloorPlanFloorID">
|
||||||
<div class="accordion-body">
|
<div class="accordion-body">
|
||||||
@if (floorLocationOptions.Count < 2)
|
@if (floorLocationOptions.Count < 1)
|
||||||
{
|
{
|
||||||
<p class="muted mb-0">Add at least two placed Locations on this floor before creating a transition.</p>
|
<p class="muted mb-0">Add at least one placed Location on this floor before creating a transition.</p>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<input type="hidden" name="FloorPlanTransitionID" value="0" form="floor-plan-transition-add-@floor.FloorPlanFloorID" />
|
<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" />
|
<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-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" data-transition-type-select="@floor.FloorPlanFloorID"></select>
|
||||||
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label" for="transition-from-@floor.FloorPlanFloorID">From Location</label>
|
<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">
|
<select id="transition-from-@floor.FloorPlanFloorID" class="form-select form-select-sm" name="FromLocationID" form="floor-plan-transition-add-@floor.FloorPlanFloorID" data-transition-from-select="@floor.FloorPlanFloorID">
|
||||||
<option value="">Choose a Location</option>
|
<option value="">Choose a Location</option>
|
||||||
@foreach (var location in floorLocationOptions)
|
@foreach (var location in floorLocationOptions)
|
||||||
{
|
{
|
||||||
@ -492,17 +504,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label" for="transition-to-@floor.FloorPlanFloorID">To Location</label>
|
<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">
|
<select id="transition-to-@floor.FloorPlanFloorID" class="form-select form-select-sm" name="ToLocationID" form="floor-plan-transition-add-@floor.FloorPlanFloorID" data-transition-to-select="@floor.FloorPlanFloorID">
|
||||||
<option value="">Choose a Location</option>
|
<option value="">Choose a From Location first</option>
|
||||||
@foreach (var location in floorLocationOptions)
|
|
||||||
{
|
|
||||||
<option value="@location.LocationID">@location.LocationPath</option>
|
|
||||||
}
|
|
||||||
</select>
|
</select>
|
||||||
|
<div class="form-text" data-transition-to-message="@floor.FloorPlanFloorID"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2 d-none" data-transition-position-panel="@floor.FloorPlanFloorID">
|
||||||
<label class="form-label" for="transition-type-@floor.FloorPlanFloorID">Transition Type</label>
|
<label class="form-label" for="transition-position-@floor.FloorPlanFloorID">Position Within Location</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>
|
<select id="transition-position-@floor.FloorPlanFloorID" class="form-select form-select-sm" name="PositionWithinLocation" asp-items="transitionPositions" form="floor-plan-transition-add-@floor.FloorPlanFloorID"></select>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label" for="transition-label-@floor.FloorPlanFloorID">Display Label</label>
|
<label class="form-label" for="transition-label-@floor.FloorPlanFloorID">Display Label</label>
|
||||||
@ -605,7 +614,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label">Level</label>
|
<label class="form-label">Level</label>
|
||||||
<input asp-for="Floors[i].SortOrder" class="form-control form-control-sm" min="-10" max="50" step="1" data-floor-level="@floor.FloorPlanFloorID" />
|
<input asp-for="Floors[i].SortOrder" class="form-control form-control-sm" min="-10" max="50" step="1" data-floor-level-input="@floor.FloorPlanFloorID" />
|
||||||
<div class="form-text">Used to order floors and later to connect stairs up/down.</div>
|
<div class="form-text">Used to order floors and later to connect stairs up/down.</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="muted">Location: @(string.IsNullOrWhiteSpace(floor.LocationPath) ? "A child Location will be created or linked on save." : floor.LocationPath)</p>
|
<p class="muted">Location: @(string.IsNullOrWhiteSpace(floor.LocationPath) ? "A child Location will be created or linked on save." : floor.LocationPath)</p>
|
||||||
@ -720,8 +729,7 @@
|
|||||||
|
|
||||||
@foreach (var transition in Model.Transitions)
|
@foreach (var transition in Model.Transitions)
|
||||||
{
|
{
|
||||||
var transitionFloorBlocks = blocksByFloor.GetValueOrDefault(transition.FloorPlanFloorID) ?? [];
|
var transitionLocationOptions = Model.Blocks
|
||||||
var transitionLocationOptions = transitionFloorBlocks
|
|
||||||
.Where(x => x.LocationID.HasValue)
|
.Where(x => x.LocationID.HasValue)
|
||||||
.GroupBy(x => x.LocationID!.Value)
|
.GroupBy(x => x.LocationID!.Value)
|
||||||
.Select(x => x.First())
|
.Select(x => x.First())
|
||||||
@ -759,13 +767,22 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label">Transition Type</label>
|
<label class="form-label">Transition Type</label>
|
||||||
<select class="form-select form-select-sm" name="TransitionType">
|
<select class="form-select form-select-sm" name="TransitionType" data-edit-transition-type>
|
||||||
@foreach (var option in Model.TransitionTypeOptions)
|
@foreach (var option in Model.TransitionTypeOptions)
|
||||||
{
|
{
|
||||||
<option value="@option.Value" selected="@(option.Value == transition.TransitionType)">@option.Text</option>
|
<option value="@option.Value" selected="@(option.Value == transition.TransitionType)">@option.Text</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-2" data-edit-transition-position-panel>
|
||||||
|
<label class="form-label">Position Within Location</label>
|
||||||
|
<select class="form-select form-select-sm" name="PositionWithinLocation">
|
||||||
|
@foreach (var option in transitionPositions)
|
||||||
|
{
|
||||||
|
<option value="@option.Value" selected="@(option.Value == transition.PositionWithinLocation)">@option.Text</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label">Display Label</label>
|
<label class="form-label">Display Label</label>
|
||||||
<input class="form-control form-control-sm" name="DisplayLabel" value="@transition.DisplayLabel" />
|
<input class="form-control form-control-sm" name="DisplayLabel" value="@transition.DisplayLabel" />
|
||||||
@ -1489,6 +1506,116 @@
|
|||||||
return aStart < bStart + bLength && bStart < aStart + aLength;
|
return aStart < bStart + bLength && bStart < aStart + aLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeTransitionType(type) {
|
||||||
|
return (type || "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStairTransition(type) {
|
||||||
|
return normalizeTransitionType(type) === "stairsup" || normalizeTransitionType(type) === "stairsdown";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUnrestrictedTransition(type) {
|
||||||
|
return ["hiddenpassage", "openpassage", "secretpassage"].includes(normalizeTransitionType(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
function blocksTouchAlongEdge(a, b) {
|
||||||
|
const verticalTouch = a.x + a.w === b.x || b.x + b.w === a.x;
|
||||||
|
const verticalOverlap = a.y < b.y + b.h && b.y < a.y + a.h;
|
||||||
|
const horizontalTouch = a.y + a.h === b.y || b.y + b.h === a.y;
|
||||||
|
const horizontalOverlap = a.x < b.x + b.w && b.x < a.x + a.w;
|
||||||
|
return (verticalTouch && verticalOverlap) || (horizontalTouch && horizontalOverlap);
|
||||||
|
}
|
||||||
|
|
||||||
|
function blockItemsForFloor(floorId) {
|
||||||
|
return [...editor.querySelectorAll(`[data-block][data-floor-id="${floorId}"]`)].map(block => ({ block, ...readBlock(block) }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function locationOptionsFromBlocks(blockItems) {
|
||||||
|
const byLocation = new Map();
|
||||||
|
blockItems.forEach(item => {
|
||||||
|
const id = item.block.dataset.locationId;
|
||||||
|
if (!id || byLocation.has(id)) return;
|
||||||
|
byLocation.set(id, {
|
||||||
|
id,
|
||||||
|
label: item.block.dataset.locationPath || item.block.dataset.locationName || item.block.textContent.trim()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return [...byLocation.values()].sort((a, b) => a.label.localeCompare(b.label));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTransitionOptions(select, options, messageElement, emptyMessage) {
|
||||||
|
const previous = select.value;
|
||||||
|
select.innerHTML = "";
|
||||||
|
const placeholder = document.createElement("option");
|
||||||
|
placeholder.value = "";
|
||||||
|
placeholder.textContent = options.length ? "Choose a Location" : emptyMessage;
|
||||||
|
select.append(placeholder);
|
||||||
|
options.forEach(option => {
|
||||||
|
const element = document.createElement("option");
|
||||||
|
element.value = option.id;
|
||||||
|
element.textContent = option.label;
|
||||||
|
element.selected = option.id === previous;
|
||||||
|
select.append(element);
|
||||||
|
});
|
||||||
|
select.disabled = options.length === 0;
|
||||||
|
if (messageElement) {
|
||||||
|
messageElement.textContent = options.length ? "" : emptyMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshTransitionDestination(floorId) {
|
||||||
|
const typeSelect = editor.querySelector(`[data-transition-type-select="${floorId}"]`);
|
||||||
|
const fromSelect = editor.querySelector(`[data-transition-from-select="${floorId}"]`);
|
||||||
|
const toSelect = editor.querySelector(`[data-transition-to-select="${floorId}"]`);
|
||||||
|
const message = editor.querySelector(`[data-transition-to-message="${floorId}"]`);
|
||||||
|
const positionPanel = editor.querySelector(`[data-transition-position-panel="${floorId}"]`);
|
||||||
|
if (!typeSelect || !fromSelect || !toSelect) return;
|
||||||
|
|
||||||
|
const type = typeSelect.value;
|
||||||
|
const fromLocationId = fromSelect.value;
|
||||||
|
const stair = isStairTransition(type);
|
||||||
|
positionPanel?.classList.toggle("d-none", !stair);
|
||||||
|
if (!fromLocationId) {
|
||||||
|
setTransitionOptions(toSelect, [], message, "Choose a From Location first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let options = [];
|
||||||
|
let emptyMessage = "No available locations.";
|
||||||
|
if (stair) {
|
||||||
|
const currentFloor = tabs.find(tab => tab.dataset.floorTab === String(floorId));
|
||||||
|
const currentLevel = Number(currentFloor?.dataset.floorOrder || 0);
|
||||||
|
const targetLevel = currentLevel + (normalizeTransitionType(type) === "stairsup" ? 1 : -1);
|
||||||
|
const targetFloor = tabs.find(tab => Number(tab.dataset.floorOrder || 0) === targetLevel);
|
||||||
|
emptyMessage = normalizeTransitionType(type) === "stairsup"
|
||||||
|
? "No floor exists above this level."
|
||||||
|
: "No floor exists below this level.";
|
||||||
|
if (targetFloor) {
|
||||||
|
options = locationOptionsFromBlocks(blockItemsForFloor(targetFloor.dataset.floorTab))
|
||||||
|
.filter(option => option.id !== fromLocationId);
|
||||||
|
emptyMessage = options.length ? "" : "No locations are placed on the adjacent floor.";
|
||||||
|
}
|
||||||
|
} else if (isUnrestrictedTransition(type)) {
|
||||||
|
options = locationOptionsFromBlocks([...editor.querySelectorAll("[data-block]")].map(block => ({ block, ...readBlock(block) })))
|
||||||
|
.filter(option => option.id !== fromLocationId);
|
||||||
|
} else {
|
||||||
|
const floorBlocks = blockItemsForFloor(floorId);
|
||||||
|
const fromBlocks = floorBlocks.filter(item => item.block.dataset.locationId === fromLocationId);
|
||||||
|
const adjacentIds = new Set();
|
||||||
|
fromBlocks.forEach(from => {
|
||||||
|
floorBlocks.forEach(to => {
|
||||||
|
const toLocationId = to.block.dataset.locationId;
|
||||||
|
if (!toLocationId || toLocationId === fromLocationId) return;
|
||||||
|
if (blocksTouchAlongEdge(from, to)) adjacentIds.add(toLocationId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
options = locationOptionsFromBlocks(floorBlocks).filter(option => adjacentIds.has(option.id));
|
||||||
|
emptyMessage = "No adjacent locations are available.";
|
||||||
|
}
|
||||||
|
|
||||||
|
setTransitionOptions(toSelect, options, message, emptyMessage);
|
||||||
|
}
|
||||||
|
|
||||||
function subtractRange(segments, start, end) {
|
function subtractRange(segments, start, end) {
|
||||||
return segments.flatMap(segment => {
|
return segments.flatMap(segment => {
|
||||||
const clippedStart = Math.max(segment.start, start);
|
const clippedStart = Math.max(segment.start, start);
|
||||||
@ -1618,28 +1745,49 @@
|
|||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function positionWithinLocation(blocksForLocation, position) {
|
||||||
|
const center = locationCenter(blocksForLocation);
|
||||||
|
if (!center) 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));
|
||||||
|
const inset = 0.35;
|
||||||
|
const normalized = (position || "Centre").toLowerCase().replace(/\s+/g, "");
|
||||||
|
const x = normalized.includes("west") ? minX + inset : normalized.includes("east") ? maxX - inset : center.x;
|
||||||
|
const y = normalized.includes("north") ? minY + inset : normalized.includes("south") ? maxY - inset : center.y;
|
||||||
|
return { x, y };
|
||||||
|
}
|
||||||
|
|
||||||
function positionTransitions(activePanel, visibleBlocks) {
|
function positionTransitions(activePanel, visibleBlocks) {
|
||||||
const markerPositions = [];
|
const markerPositions = [];
|
||||||
activePanel.querySelectorAll("[data-transition-marker]").forEach(marker => {
|
activePanel.querySelectorAll("[data-transition-marker]").forEach(marker => {
|
||||||
const fromBlocks = visibleBlocks.filter(item => item.block.dataset.locationId === marker.dataset.fromLocationId);
|
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 toBlocks = visibleBlocks.filter(item => item.block.dataset.locationId === marker.dataset.toLocationId);
|
||||||
if (fromBlocks.length === 0 || toBlocks.length === 0) {
|
if (fromBlocks.length === 0 || (!isStairTransition(marker.dataset.transitionKind) && toBlocks.length === 0)) {
|
||||||
marker.hidden = true;
|
marker.hidden = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sharedEdge = sharedTransitionEdge(fromBlocks, toBlocks);
|
const stairPlacement = isStairTransition(marker.dataset.transitionKind)
|
||||||
|
? positionWithinLocation(fromBlocks, marker.dataset.positionWithinLocation)
|
||||||
|
: null;
|
||||||
|
const sharedEdge = stairPlacement ? null : sharedTransitionEdge(fromBlocks, toBlocks);
|
||||||
const fromCenter = locationCenter(fromBlocks);
|
const fromCenter = locationCenter(fromBlocks);
|
||||||
const toCenter = locationCenter(toBlocks);
|
const toCenter = locationCenter(toBlocks);
|
||||||
marker.hidden = false;
|
marker.hidden = false;
|
||||||
marker.classList.toggle("is-shared-edge", !!sharedEdge);
|
marker.classList.toggle("is-shared-edge", !!sharedEdge);
|
||||||
|
marker.classList.toggle("is-inside-location", !!stairPlacement);
|
||||||
marker.classList.toggle("is-horizontal", sharedEdge?.orientation === "horizontal");
|
marker.classList.toggle("is-horizontal", sharedEdge?.orientation === "horizontal");
|
||||||
marker.classList.toggle("is-vertical", sharedEdge?.orientation === "vertical");
|
marker.classList.toggle("is-vertical", sharedEdge?.orientation === "vertical");
|
||||||
const pairKey = [marker.dataset.fromLocationId, marker.dataset.toLocationId].sort().join("-");
|
const pairKey = [marker.dataset.fromLocationId, marker.dataset.toLocationId].sort().join("-");
|
||||||
const key = sharedEdge
|
const key = stairPlacement
|
||||||
|
? `${pairKey}:stair:${marker.dataset.positionWithinLocation || "Centre"}:${stairPlacement.x}:${stairPlacement.y}`
|
||||||
|
: sharedEdge
|
||||||
? `${pairKey}:${sharedEdge.orientation}:${sharedEdge.x}:${sharedEdge.y}:${sharedEdge.start}:${sharedEdge.end}`
|
? `${pairKey}:${sharedEdge.orientation}:${sharedEdge.x}:${sharedEdge.y}:${sharedEdge.start}:${sharedEdge.end}`
|
||||||
: `${pairKey}:fallback`;
|
: `${pairKey}:fallback`;
|
||||||
markerPositions.push({ marker, sharedEdge, fromCenter, toCenter, key });
|
markerPositions.push({ marker, stairPlacement, sharedEdge, fromCenter, toCenter, key });
|
||||||
});
|
});
|
||||||
|
|
||||||
const groups = markerPositions.reduce((map, item) => {
|
const groups = markerPositions.reduce((map, item) => {
|
||||||
@ -1653,7 +1801,10 @@
|
|||||||
const fraction = (index + 1) / (group.length + 1);
|
const fraction = (index + 1) / (group.length + 1);
|
||||||
let x;
|
let x;
|
||||||
let y;
|
let y;
|
||||||
if (item.sharedEdge?.orientation === "vertical") {
|
if (item.stairPlacement) {
|
||||||
|
x = item.stairPlacement.x;
|
||||||
|
y = item.stairPlacement.y;
|
||||||
|
} else if (item.sharedEdge?.orientation === "vertical") {
|
||||||
x = item.sharedEdge.x;
|
x = item.sharedEdge.x;
|
||||||
y = item.sharedEdge.start + item.sharedEdge.length * fraction;
|
y = item.sharedEdge.start + item.sharedEdge.length * fraction;
|
||||||
} else if (item.sharedEdge?.orientation === "horizontal") {
|
} else if (item.sharedEdge?.orientation === "horizontal") {
|
||||||
@ -1723,6 +1874,17 @@
|
|||||||
|
|
||||||
tabs.forEach(tab => tab.addEventListener("click", () => setActiveFloor(tab.dataset.floorTab)));
|
tabs.forEach(tab => tab.addEventListener("click", () => setActiveFloor(tab.dataset.floorTab)));
|
||||||
toolboxTabs.forEach(tab => tab.addEventListener("click", () => setToolboxTab(tab.dataset.toolboxTab)));
|
toolboxTabs.forEach(tab => tab.addEventListener("click", () => setToolboxTab(tab.dataset.toolboxTab)));
|
||||||
|
editor.querySelectorAll("[data-transition-type-select],[data-transition-from-select]").forEach(input => {
|
||||||
|
const floorId = input.dataset.transitionTypeSelect || input.dataset.transitionFromSelect;
|
||||||
|
input.addEventListener("change", () => refreshTransitionDestination(floorId));
|
||||||
|
refreshTransitionDestination(floorId);
|
||||||
|
});
|
||||||
|
document.querySelectorAll("[data-edit-transition-type]").forEach(select => {
|
||||||
|
const panel = select.closest(".modal-body")?.querySelector("[data-edit-transition-position-panel]");
|
||||||
|
const refreshPositionPanel = () => panel?.classList.toggle("d-none", !isStairTransition(select.value));
|
||||||
|
select.addEventListener("change", refreshPositionPanel);
|
||||||
|
refreshPositionPanel();
|
||||||
|
});
|
||||||
addBlockForm?.addEventListener("submit", addBlockAsync);
|
addBlockForm?.addEventListener("submit", addBlockAsync);
|
||||||
editor.addEventListener("floor-plan-restore-state", event => restoreWorkspaceState(event.detail || {}));
|
editor.addEventListener("floor-plan-restore-state", event => restoreWorkspaceState(event.detail || {}));
|
||||||
workspace?.addEventListener("submit", async event => {
|
workspace?.addEventListener("submit", async event => {
|
||||||
@ -1848,7 +2010,7 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.querySelectorAll("[data-floor-level]").forEach(input => {
|
editor.querySelectorAll("[data-floor-level-input]").forEach(input => {
|
||||||
const updateFloorLevel = (clampValue = false) => {
|
const updateFloorLevel = (clampValue = false) => {
|
||||||
const parsed = Number(input.value);
|
const parsed = Number(input.value);
|
||||||
if (!Number.isFinite(parsed)) return;
|
if (!Number.isFinite(parsed)) return;
|
||||||
@ -1866,7 +2028,7 @@
|
|||||||
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("[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-floor-width],[data-floor-height],[data-floor-level]")) return;
|
if (input.matches("[data-floor-width],[data-floor-height],[data-floor-level-input]")) 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;
|
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);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user