Implemented the Secret Passages + Hidden Room detection pass.

Changed:
[Edit.cshtml (line 36)](C:/Source/PlotLine/PlotLine/Views/FloorPlans/Edit.cshtml:36)Secret Passage and Hidden Passage now render as dashed route overlays instead of wall markers.
Hidden Door remains a wall/shared-edge marker and follows existing adjacency rules.
Hidden locations are detected from existing transition data: visible entrances count, windows/hidden doors/secret passages/hidden passages/blocked doors do not.
Hidden room tooltip now includes Hidden location: no visible entrance.

[site.css (line 4793)](C:/Source/PlotLine/PlotLine/wwwroot/css/site.css:4793)Added subtle hidden-location hatch styling.
Added dashed secret-route styling with dark-theme support.

[site.min.css](C:/Source/PlotLine/PlotLine/wwwroot/css/site.min.css)Synced minified CSS.

No database changes. No stored procedure changes. No Features concept added.
This commit is contained in:
Nick Beckley 2026-06-21 22:14:25 +01:00
parent 1786d8c7b1
commit 2cf3c7da3a
3 changed files with 134 additions and 4 deletions

View File

@ -32,7 +32,8 @@
"blockeddoor" => "blocked-door",
"exteriordoor" => "exterior-door",
"stairs" or "stair" or "stairsup" or "stairsdown" => "stairs",
"passage" => "passage",
"passage" or "openpassage" => "passage",
"hiddenpassage" or "secretpassage" => "secret-route",
_ => "other"
};
}
@ -220,6 +221,7 @@
data-location-id="@block.LocationID"
data-location-name="@block.LocationName"
data-location-path="@block.LocationPath"
data-location-title="@block.LocationPath"
title="@block.LocationPath">
<span class="floor-plan-block-label">@block.DisplayLabel</span>
<span class="floor-plan-resize-handle" data-resize-handle aria-hidden="true"></span>
@ -1530,6 +1532,7 @@
button.dataset.locationId = block.locationID || "";
button.dataset.locationName = block.locationName || "";
button.dataset.locationPath = block.locationPath || "";
button.dataset.locationTitle = block.locationPath || "";
button.title = block.locationPath || "";
button.innerHTML = `<span class="floor-plan-block-label"></span><span class="floor-plan-resize-handle" data-resize-handle aria-hidden="true"></span>`;
button.querySelector(".floor-plan-block-label").textContent = block.displayLabel || block.locationName || "New block";
@ -1655,6 +1658,26 @@
return ["hiddenpassage", "openpassage", "secretpassage"].includes(normalizeTransitionType(type));
}
function isSecretRouteTransition(type) {
return ["hiddenpassage", "secretpassage"].includes(normalizeTransitionType(type));
}
function isVisibleEntranceTransition(type) {
return [
"door",
"doubledoor",
"frenchdoor",
"frenchdoors",
"slidingdoor",
"slidingdoors",
"exteriordoor",
"archway",
"openpassage",
"stairsup",
"stairsdown"
].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;
@ -1874,6 +1897,44 @@
return { x: (minX + maxX) / 2, y: (minY + maxY) / 2 };
}
function refreshHiddenLocationStyles(visibleBlocks) {
const visibleEntranceLocationIds = new Set();
editor.querySelectorAll("[data-transition-marker]").forEach(marker => {
if (!isVisibleEntranceTransition(marker.dataset.transitionKind)) return;
if (marker.dataset.fromLocationId) visibleEntranceLocationIds.add(marker.dataset.fromLocationId);
if (marker.dataset.toLocationId) visibleEntranceLocationIds.add(marker.dataset.toLocationId);
});
visibleBlocks.forEach(item => {
const locationId = item.block.dataset.locationId;
const isHidden = !!locationId && !visibleEntranceLocationIds.has(locationId);
item.block.classList.toggle("is-hidden-location", isHidden);
const baseTitle = item.block.dataset.locationTitle || item.block.dataset.locationPath || item.block.dataset.locationName || "";
item.block.title = isHidden
? `${baseTitle}${baseTitle ? "\n" : ""}Hidden location: no visible entrance`
: baseTitle;
});
}
function renderSecretRoute(activePanel, marker, fromCenter, toCenter) {
if (!fromCenter || !toCenter) return;
const dx = toCenter.x - fromCenter.x;
const dy = toCenter.y - fromCenter.y;
const length = Math.sqrt(dx * dx + dy * dy);
if (length <= 0) return;
const route = document.createElement("span");
route.className = "floor-plan-secret-route";
route.dataset.secretRoute = marker.dataset.transitionMarker || "";
route.style.setProperty("--route-x", fromCenter.x);
route.style.setProperty("--route-y", fromCenter.y);
route.style.setProperty("--route-length", length);
route.style.setProperty("--route-angle", `${Math.atan2(dy, dx)}rad`);
route.title = marker.title || "";
route.setAttribute("aria-hidden", "true");
activePanel.querySelector("[data-floor-grid]")?.append(route);
}
function sharedTransitionEdge(fromBlocks, toBlocks) {
let best = null;
fromBlocks.forEach(from => {
@ -1931,9 +1992,21 @@
function positionTransitions(activePanel, visibleBlocks) {
const markerPositions = [];
activePanel.querySelectorAll("[data-secret-route]").forEach(route => route.remove());
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 (isSecretRouteTransition(marker.dataset.transitionKind)) {
marker.hidden = true;
if (fromCenter && toCenter) {
renderSecretRoute(activePanel, marker, fromCenter, toCenter);
}
return;
}
if (fromBlocks.length === 0 || (!isStairTransition(marker.dataset.transitionKind) && toBlocks.length === 0)) {
marker.hidden = true;
return;
@ -1943,8 +2016,6 @@
? positionWithinLocation(fromBlocks, marker.dataset.positionWithinLocation)
: null;
const sharedEdge = stairPlacement ? null : sharedTransitionEdge(fromBlocks, toBlocks);
const fromCenter = locationCenter(fromBlocks);
const toCenter = locationCenter(toBlocks);
marker.hidden = false;
marker.classList.toggle("is-shared-edge", !!sharedEdge);
marker.classList.toggle("is-inside-location", !!stairPlacement);
@ -2005,6 +2076,7 @@
if (!activePanel) return;
const visibleBlocks = refreshSharedWalls(activePanel);
positionTransitions(activePanel, visibleBlocks);
refreshHiddenLocationStyles(visibleBlocks);
let overlaps = false;
visibleBlocks.forEach(a => {
a.block.classList.remove("overlap");

View File

@ -4790,6 +4790,46 @@ body.dragging-location [data-drag-type="location"] {
box-shadow: inset 0 0 0 2px rgba(180, 85, 68, .18);
}
.floor-plan-block.is-hidden-location {
background-image: repeating-linear-gradient(135deg, rgba(18, 51, 44, .08) 0, rgba(18, 51, 44, .08) 3px, transparent 3px, transparent 9px);
box-shadow: inset 0 0 0 1px rgba(43, 54, 49, .18);
}
.floor-plan-block.is-hidden-location.selected {
box-shadow: inset 0 0 0 1px rgba(43, 54, 49, .18), 0 0 0 3px rgba(47, 111, 99, .28);
}
.floor-plan-block.is-hidden-location.overlap {
box-shadow: inset 0 0 0 2px rgba(180, 85, 68, .18), inset 0 0 0 1px rgba(43, 54, 49, .18);
}
.floor-plan-secret-route {
border-top: 2px dashed rgba(69, 55, 42, .58);
height: 0;
left: calc(var(--route-x, 0) * var(--cell-size));
opacity: .72;
pointer-events: none;
position: absolute;
top: calc(var(--route-y, 0) * var(--cell-size));
transform: rotate(var(--route-angle, 0));
transform-origin: 0 50%;
width: calc(var(--route-length, 0) * var(--cell-size));
z-index: 3;
}
.floor-plan-secret-route::after {
background: rgba(255, 250, 242, .9);
border: 1px solid rgba(69, 55, 42, .5);
border-radius: 50%;
content: "";
height: 7px;
left: 50%;
position: absolute;
top: -5px;
transform: translateX(-50%);
width: 7px;
}
.floor-plan-transition-marker {
align-items: center;
--transition-marker-ink: #12332c;
@ -5151,6 +5191,24 @@ body.dragging-location [data-drag-type="location"] {
color: #f1f6f3;
}
[data-bs-theme="dark"] .floor-plan-block.is-hidden-location,
.dark .floor-plan-block.is-hidden-location {
background-image: repeating-linear-gradient(135deg, rgba(244, 234, 220, .09) 0, rgba(244, 234, 220, .09) 3px, transparent 3px, transparent 9px);
box-shadow: inset 0 0 0 1px rgba(244, 234, 220, .2);
}
[data-bs-theme="dark"] .floor-plan-secret-route,
.dark .floor-plan-secret-route {
border-top-color: rgba(244, 204, 160, .58);
opacity: .76;
}
[data-bs-theme="dark"] .floor-plan-secret-route::after,
.dark .floor-plan-secret-route::after {
background: rgba(42, 35, 30, .95);
border-color: rgba(244, 204, 160, .55);
}
[data-bs-theme="dark"] .floor-plan-coordinate-label,
.dark .floor-plan-coordinate-label {
color: rgba(244, 234, 220, .46);

File diff suppressed because one or more lines are too long