Implemented the focused Floor Plans refinement pass.
Changed: [Edit.cshtml (line 172)](C:/Source/PlotLine/PlotLine/Views/FloorPlans/Edit.cshtml:172)Added optional grid coordinate toggle with localStorage persistence. Added adjacent-floor ghost/reference overlay selector. Added overlap-aware Stairs Up/Down target filtering. Added explicit fallback button: “Show all locations on floor above/below”. Kept door/window same-floor adjacency filtering and Hidden/Open/Secret Passage unrestricted behaviour. [site.css (line 4534)](C:/Source/PlotLine/PlotLine/wwwroot/css/site.css:4534)Added subtle coordinate labels, ghost floor blocks, warning styling, and dark-theme support. [site.min.css](C:/Source/PlotLine/PlotLine/wwwroot/css/site.min.css)Synced minified CSS. Database changes: none. Stored procedures modified: none. No Features, no transition redesign, no schema changes.
This commit is contained in:
parent
89c381e6bc
commit
1786d8c7b1
@ -167,6 +167,18 @@
|
||||
}
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<label class="floor-plan-toolbar-check">
|
||||
<input type="checkbox" data-grid-coordinates-toggle />
|
||||
Show grid coordinates
|
||||
</label>
|
||||
<label class="floor-plan-reference-control">
|
||||
<span class="muted">Reference floor</span>
|
||||
<select class="form-select form-select-sm" data-reference-floor-select>
|
||||
<option value="none">None</option>
|
||||
<option value="below">Floor below</option>
|
||||
<option value="above">Floor above</option>
|
||||
</select>
|
||||
</label>
|
||||
<span class="floor-plan-save-status" data-save-status>Saved</span>
|
||||
<a class="btn btn-outline-secondary btn-sm" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Back to floor plans</a>
|
||||
<button class="btn btn-primary btn-sm" type="submit" data-save-layout-button>Save layout</button>
|
||||
@ -508,6 +520,7 @@
|
||||
<option value="">Choose a From Location first</option>
|
||||
</select>
|
||||
<div class="form-text" data-transition-to-message="@floor.FloorPlanFloorID"></div>
|
||||
<button class="btn btn-link btn-sm px-0 d-none" type="button" data-transition-stair-fallback="@floor.FloorPlanFloorID">Show all locations on adjacent floor</button>
|
||||
</div>
|
||||
<div class="mb-2 d-none" data-transition-position-panel="@floor.FloorPlanFloorID">
|
||||
<label class="form-label" for="transition-position-@floor.FloorPlanFloorID">Position Within Location</label>
|
||||
@ -890,6 +903,8 @@
|
||||
const toolboxPanels = [...editor.querySelectorAll("[data-toolbox-panel]")];
|
||||
const saveStatus = editor.querySelector("[data-save-status]");
|
||||
const saveButtons = [...editor.querySelectorAll("[data-save-layout-button]")];
|
||||
const gridCoordinatesToggle = editor.querySelector("[data-grid-coordinates-toggle]");
|
||||
const referenceFloorSelect = editor.querySelector("[data-reference-floor-select]");
|
||||
const initialSelectedBlockId = options.selectedBlockId || "@selectedBlockId";
|
||||
const saveStatusText = {
|
||||
saved: "Saved",
|
||||
@ -938,6 +953,8 @@
|
||||
let saveAgain = false;
|
||||
let refreshAfterSave = false;
|
||||
const backgroundTimers = new Map();
|
||||
const gridCoordinatesStorageKey = "floorPlan.showGridCoordinates";
|
||||
const referenceFloorStorageKey = "floorPlan.referenceFloor";
|
||||
|
||||
function setToolboxTab(tabName) {
|
||||
toolboxTabs.forEach(tab => {
|
||||
@ -958,6 +975,118 @@
|
||||
saveStatus.textContent = message || saveStatusText[state] || state;
|
||||
}
|
||||
|
||||
function gridColumnLabel(index) {
|
||||
let label = "";
|
||||
let value = index + 1;
|
||||
while (value > 0) {
|
||||
value--;
|
||||
label = String.fromCharCode(65 + (value % 26)) + label;
|
||||
value = Math.floor(value / 26);
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
function refreshGridCoordinateLabels() {
|
||||
const show = gridCoordinatesToggle?.checked === true;
|
||||
panels.forEach(panel => {
|
||||
const grid = panel.querySelector("[data-floor-grid]");
|
||||
if (!grid) return;
|
||||
grid.classList.toggle("show-grid-coordinates", show);
|
||||
grid.querySelectorAll("[data-grid-coordinate-label]").forEach(label => label.remove());
|
||||
if (!show) return;
|
||||
|
||||
const width = Number(grid.dataset.gridWidth || 0);
|
||||
const height = Number(grid.dataset.gridHeight || 0);
|
||||
for (let x = 0; x < width; x++) {
|
||||
const label = document.createElement("span");
|
||||
label.dataset.gridCoordinateLabel = "column";
|
||||
label.className = "floor-plan-coordinate-label floor-plan-coordinate-label--column";
|
||||
label.style.setProperty("--coord-x", x);
|
||||
label.textContent = gridColumnLabel(x);
|
||||
grid.append(label);
|
||||
}
|
||||
for (let y = 0; y < height; y++) {
|
||||
const label = document.createElement("span");
|
||||
label.dataset.gridCoordinateLabel = "row";
|
||||
label.className = "floor-plan-coordinate-label floor-plan-coordinate-label--row";
|
||||
label.style.setProperty("--coord-y", y);
|
||||
label.textContent = String(y + 1);
|
||||
grid.append(label);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function floorForLevel(currentFloorId, offset) {
|
||||
const current = tabs.find(tab => tab.dataset.floorTab === String(currentFloorId));
|
||||
if (!current) return null;
|
||||
const targetLevel = Number(current.dataset.floorOrder || 0) + offset;
|
||||
return tabs.find(tab => Number(tab.dataset.floorOrder || 0) === targetLevel) || null;
|
||||
}
|
||||
|
||||
function referenceFloorForActivePanel() {
|
||||
const activePanel = panels.find(panel => panel.classList.contains("active"));
|
||||
if (!activePanel || !referenceFloorSelect || referenceFloorSelect.value === "none") return null;
|
||||
return floorForLevel(activePanel.dataset.floorPanel, referenceFloorSelect.value === "above" ? 1 : -1);
|
||||
}
|
||||
|
||||
function refreshReferenceFloorOptions() {
|
||||
if (!referenceFloorSelect) return;
|
||||
const activePanel = panels.find(panel => panel.classList.contains("active"));
|
||||
const belowOption = [...referenceFloorSelect.options].find(option => option.value === "below");
|
||||
const aboveOption = [...referenceFloorSelect.options].find(option => option.value === "above");
|
||||
const belowFloor = activePanel ? floorForLevel(activePanel.dataset.floorPanel, -1) : null;
|
||||
const aboveFloor = activePanel ? floorForLevel(activePanel.dataset.floorPanel, 1) : null;
|
||||
if (belowOption) {
|
||||
belowOption.disabled = !belowFloor;
|
||||
belowOption.textContent = belowFloor ? `Floor below (${belowFloor.textContent.trim()})` : "No floor below";
|
||||
}
|
||||
if (aboveOption) {
|
||||
aboveOption.disabled = !aboveFloor;
|
||||
aboveOption.textContent = aboveFloor ? `Floor above (${aboveFloor.textContent.trim()})` : "No floor above";
|
||||
}
|
||||
if (referenceFloorSelect.selectedOptions[0]?.disabled) {
|
||||
referenceFloorSelect.value = "none";
|
||||
window.localStorage?.setItem(referenceFloorStorageKey, "none");
|
||||
}
|
||||
}
|
||||
|
||||
function refreshReferenceOverlay() {
|
||||
refreshReferenceFloorOptions();
|
||||
panels.forEach(panel => {
|
||||
panel.querySelectorAll("[data-reference-floor-overlay], [data-reference-grid-warning]").forEach(item => item.remove());
|
||||
});
|
||||
|
||||
const activePanel = panels.find(panel => panel.classList.contains("active"));
|
||||
const grid = activePanel?.querySelector("[data-floor-grid]");
|
||||
const referenceFloor = referenceFloorForActivePanel();
|
||||
if (!activePanel || !grid || !referenceFloor) return;
|
||||
|
||||
const referenceGrid = editor.querySelector(`[data-floor-grid="${referenceFloor.dataset.floorTab}"]`);
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "floor-plan-reference-overlay";
|
||||
overlay.dataset.referenceFloorOverlay = referenceFloor.dataset.floorTab;
|
||||
overlay.setAttribute("aria-hidden", "true");
|
||||
blockItemsForFloor(referenceFloor.dataset.floorTab).forEach(item => {
|
||||
const block = document.createElement("span");
|
||||
block.className = "floor-plan-reference-block";
|
||||
block.style.setProperty("--x", item.x);
|
||||
block.style.setProperty("--y", item.y);
|
||||
block.style.setProperty("--w", item.w);
|
||||
block.style.setProperty("--h", item.h);
|
||||
block.textContent = item.block.dataset.locationName || item.block.textContent.trim();
|
||||
overlay.append(block);
|
||||
});
|
||||
grid.prepend(overlay);
|
||||
|
||||
if (referenceGrid && (referenceGrid.dataset.gridWidth !== grid.dataset.gridWidth || referenceGrid.dataset.gridHeight !== grid.dataset.gridHeight)) {
|
||||
const warning = document.createElement("div");
|
||||
warning.className = "floor-plan-reference-warning";
|
||||
warning.dataset.referenceGridWarning = "true";
|
||||
warning.textContent = "Reference floor uses a different grid size.";
|
||||
grid.append(warning);
|
||||
}
|
||||
}
|
||||
|
||||
function markDirty() {
|
||||
setSaveStatus("dirty");
|
||||
window.clearTimeout(saveTimer);
|
||||
@ -1131,6 +1260,9 @@
|
||||
|
||||
grid.querySelectorAll("[data-block]").forEach(block => writeBlock(block, readBlock(block)));
|
||||
checkOverlaps();
|
||||
refreshGridCoordinateLabels();
|
||||
refreshReferenceOverlay();
|
||||
refreshTransitionDestination(floorId);
|
||||
}
|
||||
|
||||
function setActiveFloor(floorId) {
|
||||
@ -1154,6 +1286,8 @@
|
||||
}
|
||||
filterAddBlockLocations(activeTab?.dataset.floorLocationId || "");
|
||||
checkOverlaps();
|
||||
refreshReferenceOverlay();
|
||||
refreshTransitionDestination(floorId);
|
||||
}
|
||||
|
||||
function backgroundFields(floorId) {
|
||||
@ -1296,6 +1430,7 @@
|
||||
if (!selectedBlock) return;
|
||||
selectedBlock.dataset.locationId = locationSelect.value || "";
|
||||
checkOverlaps();
|
||||
refreshTransitionDestination(selectedBlock.dataset.floorId);
|
||||
markDirty();
|
||||
});
|
||||
locationWrap.append(locationSelect);
|
||||
@ -1528,6 +1663,10 @@
|
||||
return (verticalTouch && verticalOverlap) || (horizontalTouch && horizontalOverlap);
|
||||
}
|
||||
|
||||
function blocksOverlapArea(a, b) {
|
||||
return a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
|
||||
}
|
||||
|
||||
function blockItemsForFloor(floorId) {
|
||||
return [...editor.querySelectorAll(`[data-block][data-floor-id="${floorId}"]`)].map(block => ({ block, ...readBlock(block) }));
|
||||
}
|
||||
@ -1565,18 +1704,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
function refreshTransitionDestination(floorId) {
|
||||
function refreshTransitionDestination(floorId, showAllStairTargets = false) {
|
||||
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}"]`);
|
||||
const fallbackButton = editor.querySelector(`[data-transition-stair-fallback="${floorId}"]`);
|
||||
if (!typeSelect || !fromSelect || !toSelect) return;
|
||||
|
||||
const type = typeSelect.value;
|
||||
const fromLocationId = fromSelect.value;
|
||||
const stair = isStairTransition(type);
|
||||
positionPanel?.classList.toggle("d-none", !stair);
|
||||
fallbackButton?.classList.add("d-none");
|
||||
if (!fromLocationId) {
|
||||
setTransitionOptions(toSelect, [], message, "Choose a From Location first.");
|
||||
return;
|
||||
@ -1587,15 +1728,41 @@
|
||||
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 isUp = normalizeTransitionType(type) === "stairsup";
|
||||
const targetLevel = currentLevel + (isUp ? 1 : -1);
|
||||
const targetFloor = tabs.find(tab => Number(tab.dataset.floorOrder || 0) === targetLevel);
|
||||
emptyMessage = normalizeTransitionType(type) === "stairsup"
|
||||
emptyMessage = isUp
|
||||
? "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.";
|
||||
const fromBlocks = blockItemsForFloor(floorId)
|
||||
.filter(item => item.block.dataset.locationId === fromLocationId);
|
||||
const targetBlocks = blockItemsForFloor(targetFloor.dataset.floorTab);
|
||||
const overlappingLocationIds = new Set();
|
||||
if (!showAllStairTargets) {
|
||||
targetBlocks.forEach(target => {
|
||||
const targetLocationId = target.block.dataset.locationId;
|
||||
if (!targetLocationId || targetLocationId === fromLocationId) return;
|
||||
if (fromBlocks.some(from => blocksOverlapArea(from, target))) {
|
||||
overlappingLocationIds.add(targetLocationId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
options = locationOptionsFromBlocks(targetBlocks)
|
||||
.filter(option => option.id !== fromLocationId)
|
||||
.filter(option => showAllStairTargets || overlappingLocationIds.has(option.id));
|
||||
emptyMessage = isUp
|
||||
? "No locations directly above this location."
|
||||
: "No locations directly below this location.";
|
||||
if (!options.length && locationOptionsFromBlocks(targetBlocks).some(option => option.id !== fromLocationId)) {
|
||||
fallbackButton?.classList.remove("d-none");
|
||||
if (fallbackButton) {
|
||||
fallbackButton.textContent = isUp
|
||||
? "Show all locations on floor above"
|
||||
: "Show all locations on floor below";
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (isUnrestrictedTransition(type)) {
|
||||
options = locationOptionsFromBlocks([...editor.querySelectorAll("[data-block]")].map(block => ({ block, ...readBlock(block) })))
|
||||
@ -1881,6 +2048,26 @@
|
||||
input.addEventListener("change", () => refreshTransitionDestination(floorId));
|
||||
refreshTransitionDestination(floorId);
|
||||
});
|
||||
editor.querySelectorAll("[data-transition-stair-fallback]").forEach(button => {
|
||||
button.addEventListener("click", () => refreshTransitionDestination(button.dataset.transitionStairFallback, true));
|
||||
});
|
||||
if (gridCoordinatesToggle) {
|
||||
gridCoordinatesToggle.checked = window.localStorage?.getItem(gridCoordinatesStorageKey) === "true";
|
||||
gridCoordinatesToggle.addEventListener("change", () => {
|
||||
window.localStorage?.setItem(gridCoordinatesStorageKey, gridCoordinatesToggle.checked ? "true" : "false");
|
||||
refreshGridCoordinateLabels();
|
||||
});
|
||||
}
|
||||
if (referenceFloorSelect) {
|
||||
const storedReferenceFloor = window.localStorage?.getItem(referenceFloorStorageKey);
|
||||
if (storedReferenceFloor && [...referenceFloorSelect.options].some(option => option.value === storedReferenceFloor)) {
|
||||
referenceFloorSelect.value = storedReferenceFloor;
|
||||
}
|
||||
referenceFloorSelect.addEventListener("change", () => {
|
||||
window.localStorage?.setItem(referenceFloorStorageKey, referenceFloorSelect.value);
|
||||
refreshReferenceOverlay();
|
||||
});
|
||||
}
|
||||
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));
|
||||
@ -1960,11 +2147,19 @@
|
||||
});
|
||||
|
||||
editor.addEventListener("pointerup", () => {
|
||||
if (pointerState) markDirty();
|
||||
if (pointerState) {
|
||||
const floorId = pointerState.block.dataset.floorId;
|
||||
markDirty();
|
||||
refreshTransitionDestination(floorId);
|
||||
}
|
||||
pointerState = null;
|
||||
});
|
||||
editor.addEventListener("pointercancel", () => {
|
||||
if (pointerState) markDirty();
|
||||
if (pointerState) {
|
||||
const floorId = pointerState.block.dataset.floorId;
|
||||
markDirty();
|
||||
refreshTransitionDestination(floorId);
|
||||
}
|
||||
pointerState = null;
|
||||
});
|
||||
|
||||
@ -1995,6 +2190,7 @@
|
||||
if (!selectedBlock) return;
|
||||
selectedBlock.dataset.locationId = select.value || "";
|
||||
checkOverlaps();
|
||||
refreshTransitionDestination(selectedBlock.dataset.floorId);
|
||||
markDirty();
|
||||
});
|
||||
});
|
||||
@ -2054,6 +2250,8 @@
|
||||
} else if (tabs[0]) {
|
||||
setActiveFloor(tabs[0].dataset.floorTab);
|
||||
}
|
||||
refreshGridCoordinateLabels();
|
||||
refreshReferenceOverlay();
|
||||
checkOverlaps();
|
||||
};
|
||||
|
||||
|
||||
@ -4531,6 +4531,26 @@ body.dragging-location [data-drag-type="location"] {
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.floor-plan-toolbar-check,
|
||||
.floor-plan-reference-control {
|
||||
align-items: center;
|
||||
color: var(--muted-color, #6b756f);
|
||||
display: inline-flex;
|
||||
font-size: .82rem;
|
||||
font-weight: 700;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.floor-plan-reference-control {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.floor-plan-reference-control .form-select {
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.floor-plan-save-status {
|
||||
border: 1px solid rgba(47, 111, 99, .2);
|
||||
border-radius: 999px;
|
||||
@ -4594,6 +4614,71 @@ body.dragging-location [data-drag-type="location"] {
|
||||
width: calc(var(--grid-width) * var(--cell-size));
|
||||
}
|
||||
|
||||
.floor-plan-coordinate-label {
|
||||
color: rgba(33, 61, 55, .42);
|
||||
font-size: .62rem;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, .7);
|
||||
user-select: none;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.floor-plan-coordinate-label--column {
|
||||
left: calc(var(--coord-x) * var(--cell-size) + 4px);
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
.floor-plan-coordinate-label--row {
|
||||
left: 4px;
|
||||
top: calc(var(--coord-y) * var(--cell-size) + 4px);
|
||||
}
|
||||
|
||||
.floor-plan-reference-overlay {
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.floor-plan-reference-block {
|
||||
align-items: center;
|
||||
background: rgba(47, 111, 99, .08);
|
||||
border: 1px dashed rgba(47, 111, 99, .34);
|
||||
border-radius: 2px;
|
||||
color: rgba(27, 55, 49, .46);
|
||||
display: flex;
|
||||
font-size: .62rem;
|
||||
font-weight: 800;
|
||||
height: calc(var(--h) * var(--cell-size));
|
||||
justify-content: center;
|
||||
left: calc(var(--x) * var(--cell-size));
|
||||
line-height: 1.05;
|
||||
overflow: hidden;
|
||||
padding: 3px;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: calc(var(--y) * var(--cell-size));
|
||||
width: calc(var(--w) * var(--cell-size));
|
||||
}
|
||||
|
||||
.floor-plan-reference-warning {
|
||||
background: rgba(251, 236, 211, .92);
|
||||
border: 1px solid rgba(184, 118, 43, .34);
|
||||
border-radius: 999px;
|
||||
color: #7b4b12;
|
||||
font-size: .76rem;
|
||||
font-weight: 800;
|
||||
left: 10px;
|
||||
padding: 4px 10px;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
z-index: 6;
|
||||
}
|
||||
|
||||
.floor-plan-background-image {
|
||||
height: auto;
|
||||
left: 0;
|
||||
@ -5066,6 +5151,26 @@ body.dragging-location [data-drag-type="location"] {
|
||||
color: #f1f6f3;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .floor-plan-coordinate-label,
|
||||
.dark .floor-plan-coordinate-label {
|
||||
color: rgba(244, 234, 220, .46);
|
||||
text-shadow: 0 1px 0 rgba(0, 0, 0, .55);
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .floor-plan-reference-block,
|
||||
.dark .floor-plan-reference-block {
|
||||
background: rgba(131, 199, 242, .08);
|
||||
border-color: rgba(131, 199, 242, .32);
|
||||
color: rgba(244, 234, 220, .46);
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .floor-plan-reference-warning,
|
||||
.dark .floor-plan-reference-warning {
|
||||
background: rgba(84, 57, 25, .92);
|
||||
border-color: rgba(244, 177, 100, .32);
|
||||
color: #ffd9a5;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .floor-plan-transition-marker,
|
||||
.dark .floor-plan-transition-marker {
|
||||
--transition-marker-ink: #f4eadc;
|
||||
|
||||
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