Implemented the transition refinement pass.
Changed: [Edit.cshtml (line 29)](C:/Source/PlotLine/PlotLine/Views/FloorPlans/Edit.cshtml:29): added Sliding Door, French Door, and Ladder marker mappings/SVGs. [Edit.cshtml (line 1729)](C:/Source/PlotLine/PlotLine/Views/FloorPlans/Edit.cshtml:1729): Ladder To Location filtering now lists placed locations on floors above, sorted by level/floor/name with clean labels like First Floor > East Hallway 1st. [Edit.cshtml (line 2072)](C:/Source/PlotLine/PlotLine/Views/FloorPlans/Edit.cshtml:2072): Ladder renders inside the From Location using the existing position system. [CoreServices.cs (line 7966)](C:/Source/PlotLine/PlotLine/Services/CoreServices.cs:7966): server validation now treats Ladder as a vertical upward transition, without stairs reciprocity. [CoreModels.cs (line 1243)](C:/Source/PlotLine/PlotLine/Models/CoreModels.cs:1243): normalisation accepts Ladders. [site.css (line 4902)](C:/Source/PlotLine/PlotLine/wwwroot/css/site.css:4902) and minified CSS: added light/dark styling for the new marker types. No database changes. No new tables. No Features, architecture, AJAX, or secret/hidden room logic changes.
This commit is contained in:
parent
2cf3c7da3a
commit
b91bdeb138
@ -1240,7 +1240,7 @@ public static class FloorPlanTransitionTypes
|
||||
}
|
||||
|
||||
var normalized = new string((transitionType ?? string.Empty).Where(char.IsLetterOrDigit).Select(char.ToLowerInvariant).ToArray());
|
||||
return normalized is "frenchdoor" or "slidingdoor";
|
||||
return normalized is "frenchdoor" or "slidingdoor" or "ladders";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -7740,7 +7740,7 @@ public sealed class FloorPlanService(
|
||||
?? throw new InvalidOperationException("Floor not found.");
|
||||
|
||||
ValidateTransition(model, floor, data.Blocks, data.Transitions, data.Floors);
|
||||
if (!IsStairTransition(model.TransitionType))
|
||||
if (!IsInsideLocationTransition(model.TransitionType))
|
||||
{
|
||||
model.PositionWithinLocation = FloorPlanTransitionPositions.Centre;
|
||||
}
|
||||
@ -7962,6 +7962,12 @@ public sealed class FloorPlanService(
|
||||
private static bool IsStairTransition(string? transitionType)
|
||||
=> NormalizeTransitionType(transitionType) is "stairsup" or "stairsdown";
|
||||
|
||||
private static bool IsLadderTransition(string? transitionType)
|
||||
=> NormalizeTransitionType(transitionType) is "ladder" or "ladders";
|
||||
|
||||
private static bool IsInsideLocationTransition(string? transitionType)
|
||||
=> IsStairTransition(transitionType) || IsLadderTransition(transitionType);
|
||||
|
||||
private static bool BlocksTouchAlongEdge(FloorPlanBlock a, FloorPlanBlock b)
|
||||
{
|
||||
var verticalTouch = a.X + a.WidthCells == b.X || b.X + b.WidthCells == a.X;
|
||||
@ -8025,6 +8031,25 @@ public sealed class FloorPlanService(
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsLadderTransition(model.TransitionType))
|
||||
{
|
||||
var targetFloorIds = floors
|
||||
.Where(x => x.SortOrder > floor.SortOrder)
|
||||
.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("Ladders must connect to a placed location on a floor above.");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsUnrestrictedTransition(model.TransitionType))
|
||||
{
|
||||
var planLocationIds = blocks.Select(x => x.LocationID).ToHashSet();
|
||||
|
||||
@ -26,12 +26,15 @@
|
||||
{
|
||||
"door" => "door",
|
||||
"doubledoor" => "double-door",
|
||||
"frenchdoor" or "frenchdoors" => "french-door",
|
||||
"slidingdoor" or "slidingdoors" => "sliding-door",
|
||||
"window" => "window",
|
||||
"archway" or "opening" => "archway",
|
||||
"secretdoor" or "hiddendoor" => "secret-door",
|
||||
"blockeddoor" => "blocked-door",
|
||||
"exteriordoor" => "exterior-door",
|
||||
"stairs" or "stair" or "stairsup" or "stairsdown" => "stairs",
|
||||
"ladder" or "ladders" => "ladder",
|
||||
"passage" or "openpassage" => "passage",
|
||||
"hiddenpassage" or "secretpassage" => "secret-route",
|
||||
_ => "other"
|
||||
@ -42,12 +45,15 @@
|
||||
{
|
||||
"door" => @"<svg viewBox=""0 0 18 18"" aria-hidden=""true"" focusable=""false""><path class=""marker-line"" d=""M2 9h6""/><path class=""marker-line"" d=""M10 9h6""/><path class=""marker-arc"" d=""M8 9a6 6 0 0 1 6-6""/></svg>",
|
||||
"double-door" => @"<svg viewBox=""0 0 18 18"" aria-hidden=""true"" focusable=""false""><path class=""marker-line"" d=""M2 9h4""/><path class=""marker-line"" d=""M12 9h4""/><path class=""marker-arc"" d=""M6 9a4 4 0 0 1 4-4""/><path class=""marker-arc"" d=""M12 9a4 4 0 0 0-4-4""/></svg>",
|
||||
"french-door" => @"<svg viewBox=""0 0 18 18"" aria-hidden=""true"" focusable=""false""><path class=""marker-line marker-strong"" d=""M2 9h3""/><path class=""marker-line marker-strong"" d=""M13 9h3""/><path class=""marker-arc"" d=""M5 9a4 4 0 0 1 4-4""/><path class=""marker-arc"" d=""M13 9a4 4 0 0 0-4-4""/><path class=""marker-line"" d=""M9 5v8""/></svg>",
|
||||
"sliding-door" => @"<svg viewBox=""0 0 18 18"" aria-hidden=""true"" focusable=""false""><path class=""marker-line"" d=""M2 9h14""/><path class=""marker-line marker-track"" d=""M4 6h10""/><path class=""marker-line marker-track"" d=""M9 6l3 3""/></svg>",
|
||||
"window" => @"<svg viewBox=""0 0 18 18"" aria-hidden=""true"" focusable=""false""><path class=""marker-window"" d=""M3 7h12""/><path class=""marker-window"" d=""M3 11h12""/><path class=""marker-line"" d=""M5 9h8""/></svg>",
|
||||
"archway" => @"<svg viewBox=""0 0 18 18"" aria-hidden=""true"" focusable=""false""><path class=""marker-line"" d=""M2 14h4""/><path class=""marker-line"" d=""M12 14h4""/><path class=""marker-arc"" d=""M6 14a3 3 0 0 1 6 0""/></svg>",
|
||||
"secret-door" => @"<svg viewBox=""0 0 18 18"" aria-hidden=""true"" focusable=""false""><path class=""marker-line marker-dotted"" d=""M2 9h6""/><path class=""marker-line marker-dotted"" d=""M10 9h6""/><path class=""marker-arc marker-dotted"" d=""M8 9a6 6 0 0 1 6-6""/></svg>",
|
||||
"blocked-door" => @"<svg viewBox=""0 0 18 18"" aria-hidden=""true"" focusable=""false""><path class=""marker-line"" d=""M2 9h14""/><path class=""marker-blocked"" d=""M6 5l6 8""/><path class=""marker-blocked"" d=""M12 5l-6 8""/></svg>",
|
||||
"exterior-door" => @"<svg viewBox=""0 0 18 18"" aria-hidden=""true"" focusable=""false""><path class=""marker-line marker-strong"" d=""M2 9h6""/><path class=""marker-line marker-strong"" d=""M10 9h6""/><path class=""marker-arc marker-strong"" d=""M8 9a6 6 0 0 1 6-6""/></svg>",
|
||||
"stairs" => @"<svg viewBox=""0 0 18 18"" aria-hidden=""true"" focusable=""false""><path class=""marker-line"" d=""M4 13h10""/><path class=""marker-line"" d=""M5 10h8""/><path class=""marker-line"" d=""M6 7h6""/><path class=""marker-line"" d=""M7 4h4""/></svg>",
|
||||
"ladder" => @"<svg viewBox=""0 0 18 18"" aria-hidden=""true"" focusable=""false""><path class=""marker-line"" d=""M6 3v12""/><path class=""marker-line"" d=""M12 3v12""/><path class=""marker-line"" d=""M6 5h6""/><path class=""marker-line"" d=""M6 8h6""/><path class=""marker-line"" d=""M6 11h6""/><path class=""marker-line"" d=""M6 14h6""/></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>"
|
||||
};
|
||||
@ -1654,6 +1660,14 @@
|
||||
return normalizeTransitionType(type) === "stairsup" || normalizeTransitionType(type) === "stairsdown";
|
||||
}
|
||||
|
||||
function isLadderTransition(type) {
|
||||
return ["ladder", "ladders"].includes(normalizeTransitionType(type));
|
||||
}
|
||||
|
||||
function isInsideLocationTransition(type) {
|
||||
return isStairTransition(type) || isLadderTransition(type);
|
||||
}
|
||||
|
||||
function isUnrestrictedTransition(type) {
|
||||
return ["hiddenpassage", "openpassage", "secretpassage"].includes(normalizeTransitionType(type));
|
||||
}
|
||||
@ -1694,6 +1708,11 @@
|
||||
return [...editor.querySelectorAll(`[data-block][data-floor-id="${floorId}"]`)].map(block => ({ block, ...readBlock(block) }));
|
||||
}
|
||||
|
||||
function floorNameFor(floorId) {
|
||||
const tab = tabs.find(item => item.dataset.floorTab === String(floorId));
|
||||
return tab?.dataset.floorName || tab?.textContent.trim() || "Floor";
|
||||
}
|
||||
|
||||
function locationOptionsFromBlocks(blockItems) {
|
||||
const byLocation = new Map();
|
||||
blockItems.forEach(item => {
|
||||
@ -1707,6 +1726,42 @@
|
||||
return [...byLocation.values()].sort((a, b) => a.label.localeCompare(b.label));
|
||||
}
|
||||
|
||||
function ladderLocationOptions(currentFloorId, fromLocationId) {
|
||||
const currentFloor = tabs.find(tab => tab.dataset.floorTab === String(currentFloorId));
|
||||
const currentLevel = Number(currentFloor?.dataset.floorOrder || 0);
|
||||
const floorsAbove = tabs
|
||||
.filter(tab => Number(tab.dataset.floorOrder || 0) > currentLevel)
|
||||
.sort((a, b) => Number(a.dataset.floorOrder || 0) - Number(b.dataset.floorOrder || 0)
|
||||
|| (a.dataset.floorName || a.textContent.trim()).localeCompare(b.dataset.floorName || b.textContent.trim()));
|
||||
|
||||
const options = [];
|
||||
floorsAbove.forEach(floor => {
|
||||
locationOptionsFromBlocks(blockItemsForFloor(floor.dataset.floorTab))
|
||||
.filter(option => option.id !== fromLocationId)
|
||||
.forEach(option => {
|
||||
const floorName = floorNameFor(floor.dataset.floorTab);
|
||||
const parts = option.label.split(" > ").map(part => part.trim()).filter(Boolean);
|
||||
const floorIndex = parts.lastIndexOf(floorName);
|
||||
const locationLabel = floorIndex >= 0 && parts.length > floorIndex + 1
|
||||
? parts.slice(floorIndex + 1).join(" > ")
|
||||
: parts[parts.length - 1] || option.label;
|
||||
options.push({
|
||||
id: option.id,
|
||||
label: `${floorName} > ${locationLabel}`,
|
||||
level: Number(floor.dataset.floorOrder || 0),
|
||||
floorName
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
floorsAbove,
|
||||
options: options.sort((a, b) => a.level - b.level
|
||||
|| a.floorName.localeCompare(b.floorName)
|
||||
|| a.label.localeCompare(b.label))
|
||||
};
|
||||
}
|
||||
|
||||
function setTransitionOptions(select, options, messageElement, emptyMessage) {
|
||||
const previous = select.value;
|
||||
select.innerHTML = "";
|
||||
@ -1739,7 +1794,8 @@
|
||||
const type = typeSelect.value;
|
||||
const fromLocationId = fromSelect.value;
|
||||
const stair = isStairTransition(type);
|
||||
positionPanel?.classList.toggle("d-none", !stair);
|
||||
const ladder = isLadderTransition(type);
|
||||
positionPanel?.classList.toggle("d-none", !isInsideLocationTransition(type));
|
||||
fallbackButton?.classList.add("d-none");
|
||||
if (!fromLocationId) {
|
||||
setTransitionOptions(toSelect, [], message, "Choose a From Location first.");
|
||||
@ -1787,6 +1843,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (ladder) {
|
||||
const ladderTargets = ladderLocationOptions(floorId, fromLocationId);
|
||||
options = ladderTargets.options;
|
||||
emptyMessage = ladderTargets.floorsAbove.length
|
||||
? "No placed locations exist on floors above."
|
||||
: "No floors above this level.";
|
||||
} else if (isUnrestrictedTransition(type)) {
|
||||
options = locationOptionsFromBlocks([...editor.querySelectorAll("[data-block]")].map(block => ({ block, ...readBlock(block) })))
|
||||
.filter(option => option.id !== fromLocationId);
|
||||
@ -2007,27 +2069,27 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (fromBlocks.length === 0 || (!isStairTransition(marker.dataset.transitionKind) && toBlocks.length === 0)) {
|
||||
if (fromBlocks.length === 0 || (!isInsideLocationTransition(marker.dataset.transitionKind) && toBlocks.length === 0)) {
|
||||
marker.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const stairPlacement = isStairTransition(marker.dataset.transitionKind)
|
||||
const insidePlacement = isInsideLocationTransition(marker.dataset.transitionKind)
|
||||
? positionWithinLocation(fromBlocks, marker.dataset.positionWithinLocation)
|
||||
: null;
|
||||
const sharedEdge = stairPlacement ? null : sharedTransitionEdge(fromBlocks, toBlocks);
|
||||
const sharedEdge = insidePlacement ? null : sharedTransitionEdge(fromBlocks, toBlocks);
|
||||
marker.hidden = false;
|
||||
marker.classList.toggle("is-shared-edge", !!sharedEdge);
|
||||
marker.classList.toggle("is-inside-location", !!stairPlacement);
|
||||
marker.classList.toggle("is-inside-location", !!insidePlacement);
|
||||
marker.classList.toggle("is-horizontal", sharedEdge?.orientation === "horizontal");
|
||||
marker.classList.toggle("is-vertical", sharedEdge?.orientation === "vertical");
|
||||
const pairKey = [marker.dataset.fromLocationId, marker.dataset.toLocationId].sort().join("-");
|
||||
const key = stairPlacement
|
||||
? `${pairKey}:stair:${marker.dataset.positionWithinLocation || "Centre"}:${stairPlacement.x}:${stairPlacement.y}`
|
||||
const key = insidePlacement
|
||||
? `${pairKey}:inside:${normalizeTransitionType(marker.dataset.transitionKind)}:${marker.dataset.positionWithinLocation || "Centre"}:${insidePlacement.x}:${insidePlacement.y}`
|
||||
: sharedEdge
|
||||
? `${pairKey}:${sharedEdge.orientation}:${sharedEdge.x}:${sharedEdge.y}:${sharedEdge.start}:${sharedEdge.end}`
|
||||
: `${pairKey}:fallback`;
|
||||
markerPositions.push({ marker, stairPlacement, sharedEdge, fromCenter, toCenter, key });
|
||||
markerPositions.push({ marker, insidePlacement, sharedEdge, fromCenter, toCenter, key });
|
||||
});
|
||||
|
||||
const groups = markerPositions.reduce((map, item) => {
|
||||
@ -2041,9 +2103,9 @@
|
||||
const fraction = (index + 1) / (group.length + 1);
|
||||
let x;
|
||||
let y;
|
||||
if (item.stairPlacement) {
|
||||
x = item.stairPlacement.x;
|
||||
y = item.stairPlacement.y;
|
||||
if (item.insidePlacement) {
|
||||
x = item.insidePlacement.x;
|
||||
y = item.insidePlacement.y;
|
||||
} else if (item.sharedEdge?.orientation === "vertical") {
|
||||
x = item.sharedEdge.x;
|
||||
y = item.sharedEdge.start + item.sharedEdge.length * fraction;
|
||||
@ -2142,7 +2204,7 @@
|
||||
}
|
||||
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));
|
||||
const refreshPositionPanel = () => panel?.classList.toggle("d-none", !isInsideLocationTransition(select.value));
|
||||
select.addEventListener("change", refreshPositionPanel);
|
||||
refreshPositionPanel();
|
||||
});
|
||||
|
||||
@ -4899,16 +4899,35 @@ body.dragging-location [data-drag-type="location"] {
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.floor-plan-transition-marker .marker-track {
|
||||
opacity: .72;
|
||||
}
|
||||
|
||||
.floor-plan-transition-marker--window {
|
||||
background: rgba(239, 249, 255, .98);
|
||||
border-color: rgba(36, 111, 159, .48);
|
||||
}
|
||||
|
||||
.floor-plan-transition-marker--sliding-door {
|
||||
background: rgba(248, 252, 249, .98);
|
||||
border-color: rgba(47, 111, 99, .58);
|
||||
}
|
||||
|
||||
.floor-plan-transition-marker--french-door {
|
||||
background: rgba(255, 252, 246, .98);
|
||||
border-color: rgba(119, 86, 45, .5);
|
||||
}
|
||||
|
||||
.floor-plan-transition-marker--secret-door {
|
||||
background: rgba(255, 250, 242, .88);
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.floor-plan-transition-marker--ladder {
|
||||
background: rgba(246, 248, 245, .98);
|
||||
border-color: rgba(77, 91, 67, .55);
|
||||
}
|
||||
|
||||
.floor-plan-transition-marker--blocked-door {
|
||||
background: rgba(255, 244, 241, .98);
|
||||
border-color: rgba(155, 52, 43, .52);
|
||||
@ -5252,6 +5271,16 @@ body.dragging-location [data-drag-type="location"] {
|
||||
border-color: rgba(131, 199, 242, .66);
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .floor-plan-transition-marker--sliding-door,
|
||||
.dark .floor-plan-transition-marker--sliding-door,
|
||||
[data-bs-theme="dark"] .floor-plan-transition-marker--french-door,
|
||||
.dark .floor-plan-transition-marker--french-door,
|
||||
[data-bs-theme="dark"] .floor-plan-transition-marker--ladder,
|
||||
.dark .floor-plan-transition-marker--ladder {
|
||||
background: rgba(39, 43, 37, .98);
|
||||
border-color: rgba(212, 154, 98, .58);
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .floor-plan-transition-marker--blocked-door,
|
||||
.dark .floor-plan-transition-marker--blocked-door,
|
||||
[data-bs-theme="dark"] .floor-plan-transition-marker--blocked-door.is-shared-edge,
|
||||
|
||||
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