Implemented the targeted Scene Occupancy refinement pass.
This commit is contained in:
parent
cea82a922f
commit
a943ac3da4
@ -7771,7 +7771,10 @@ public sealed class FloorPlanService(
|
||||
var occupancyRows = await sceneOccupancy.ListByFloorPlanAsync(floorPlanId);
|
||||
var firstBlockByLocation = data.Blocks
|
||||
.GroupBy(x => x.LocationID)
|
||||
.ToDictionary(x => x.Key, x => x.OrderBy(block => block.FloorPlanFloorID).ThenBy(block => block.Y).ThenBy(block => block.X).First());
|
||||
.ToDictionary(x => x.Key, x => x.OrderBy(block => block.Y).ThenBy(block => block.X).First());
|
||||
var firstBlockByFloorLocation = data.Blocks
|
||||
.GroupBy(x => (x.FloorPlanFloorID, x.LocationID))
|
||||
.ToDictionary(x => x.Key, x => x.OrderBy(block => block.Y).ThenBy(block => block.X).First());
|
||||
return new FloorPlanEditorViewModel
|
||||
{
|
||||
Project = project,
|
||||
@ -7825,10 +7828,23 @@ public sealed class FloorPlanService(
|
||||
})
|
||||
.ToList(),
|
||||
OccupancyTokens = occupancyRows
|
||||
.Where(x => firstBlockByLocation.ContainsKey(x.LocationID))
|
||||
.Select(x =>
|
||||
{
|
||||
var block = firstBlockByLocation[x.LocationID];
|
||||
FloorPlanBlock? block = null;
|
||||
if (x.FloorPlanFloorID.HasValue)
|
||||
{
|
||||
firstBlockByFloorLocation.TryGetValue((x.FloorPlanFloorID.Value, x.LocationID), out block);
|
||||
}
|
||||
else
|
||||
{
|
||||
firstBlockByLocation.TryGetValue(x.LocationID, out block);
|
||||
}
|
||||
|
||||
if (block is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new FloorPlanOccupancyTokenViewModel
|
||||
{
|
||||
SceneID = x.SceneID,
|
||||
@ -7842,6 +7858,8 @@ public sealed class FloorPlanService(
|
||||
ThumbnailPath = x.CharacterThumbnailPath ?? x.AssetThumbnailPath
|
||||
};
|
||||
})
|
||||
.Where(x => x is not null)
|
||||
.Cast<FloorPlanOccupancyTokenViewModel>()
|
||||
.ToList()
|
||||
};
|
||||
}
|
||||
|
||||
@ -5,13 +5,19 @@
|
||||
var activeFloor = Model.Floors.OrderBy(x => x.SortOrder).ThenBy(x => x.Name).FirstOrDefault();
|
||||
var blocksByFloor = Model.Blocks.GroupBy(x => x.FloorPlanFloorID).ToDictionary(x => x.Key, x => x.ToList());
|
||||
var transitionsByFloor = Model.Transitions.GroupBy(x => x.FloorPlanFloorID).ToDictionary(x => x.Key, x => x.ToList());
|
||||
var firstBlockIdByLocation = Model.Blocks
|
||||
var firstBlockIdByFloorLocation = Model.Blocks
|
||||
.Where(x => x.LocationID.HasValue)
|
||||
.GroupBy(x => x.LocationID)
|
||||
.ToDictionary(x => x.Key!.Value, x => x.OrderBy(block => block.FloorPlanFloorID).ThenBy(block => block.Y).ThenBy(block => block.X).First().FloorPlanBlockID);
|
||||
var occupancyTokensByLocation = Model.OccupancyTokens
|
||||
.GroupBy(x => $"{x.SceneID}:{x.LocationID}")
|
||||
.GroupBy(x => $"{x.FloorPlanFloorID}:{x.LocationID!.Value}")
|
||||
.ToDictionary(x => x.Key, x => x.OrderBy(block => block.Y).ThenBy(block => block.X).First().FloorPlanBlockID);
|
||||
var occupancyTokensByFloorLocation = Model.OccupancyTokens
|
||||
.GroupBy(x => $"{x.SceneID}:{x.FloorPlanFloorID}:{x.LocationID}")
|
||||
.ToDictionary(x => x.Key, x => x.OrderBy(token => token.EntityType == "Character" ? 0 : 1).ThenBy(token => token.DisplayName).ToList());
|
||||
var occupancyTokenCountByScene = Model.OccupancyTokens
|
||||
.GroupBy(x => x.SceneID)
|
||||
.ToDictionary(x => x.Key, x => x.Count());
|
||||
var occupancyTokenCountBySceneFloor = Model.OccupancyTokens
|
||||
.GroupBy(x => $"{x.SceneID}:{x.FloorPlanFloorID}")
|
||||
.ToDictionary(x => x.Key, x => x.Count());
|
||||
var activeOccupancyScene = Model.OccupancyScenes.FirstOrDefault();
|
||||
var selectedBlockId = int.TryParse(Context.Request.Query["selectedBlockId"], out var parsedSelectedBlockId) ? parsedSelectedBlockId : 0;
|
||||
string BlockClass(string blockType) => $"floor-plan-block floor-plan-block--{blockType.ToLowerInvariant()}";
|
||||
@ -170,36 +176,46 @@
|
||||
<input asp-for="FloorPlan.Description" type="hidden" />
|
||||
|
||||
<section class="floor-plan-editor-toolbar">
|
||||
<div>
|
||||
<span class="muted">Active floor</span>
|
||||
<strong data-current-floor-name>@activeFloor?.Name</strong>
|
||||
</div>
|
||||
<div class="floor-plan-tabs" role="tablist" aria-label="Floors">
|
||||
@foreach (var floor in Model.Floors.OrderBy(x => x.SortOrder).ThenBy(x => x.Name))
|
||||
{
|
||||
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-order="@floor.SortOrder" data-floor-location-id="@floor.LocationID">@floor.Name</button>
|
||||
}
|
||||
</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>
|
||||
<div class="btn-group btn-group-sm" role="group" aria-label="Floor plan mode">
|
||||
<button class="btn btn-outline-secondary active" type="button" data-floor-plan-mode="structure">Structure</button>
|
||||
<button class="btn btn-outline-secondary" type="button" data-floor-plan-mode="occupancy">Occupancy</button>
|
||||
<div class="floor-plan-toolbar-main">
|
||||
<div class="floor-plan-toolbar-floor-group">
|
||||
<div class="floor-plan-active-floor">
|
||||
<span class="muted">Active floor</span>
|
||||
<strong data-current-floor-name>@activeFloor?.Name</strong>
|
||||
</div>
|
||||
<div class="floor-plan-tabs" role="tablist" aria-label="Floors">
|
||||
@foreach (var floor in Model.Floors.OrderBy(x => x.SortOrder).ThenBy(x => x.Name))
|
||||
{
|
||||
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-order="@floor.SortOrder" data-floor-location-id="@floor.LocationID">@floor.Name</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<label class="floor-plan-reference-control floor-plan-occupancy-control" data-occupancy-controls hidden>
|
||||
<div class="floor-plan-toolbar-control-group">
|
||||
<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>
|
||||
</div>
|
||||
<div class="floor-plan-toolbar-action-group">
|
||||
<span class="floor-plan-save-status" data-save-status>Saved</span>
|
||||
<div class="btn-group btn-group-sm" role="group" aria-label="Floor plan mode">
|
||||
<button class="btn btn-outline-secondary active" type="button" data-floor-plan-mode="structure">Structure</button>
|
||||
<button class="btn btn-outline-secondary" type="button" data-floor-plan-mode="occupancy">Occupancy</button>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="floor-plan-occupancy-scene-row" data-occupancy-controls hidden>
|
||||
<label class="floor-plan-reference-control">
|
||||
<span class="muted">Scene</span>
|
||||
<select class="form-select form-select-sm" data-occupancy-scene-select>
|
||||
@foreach (var scene in Model.OccupancyScenes)
|
||||
@ -208,10 +224,8 @@
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<button class="btn btn-outline-secondary btn-sm floor-plan-occupancy-control" type="button" data-occupancy-prev hidden>Previous Scene</button>
|
||||
<button class="btn btn-outline-secondary btn-sm floor-plan-occupancy-control" type="button" data-occupancy-next hidden>Next Scene</button>
|
||||
<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>
|
||||
<button class="btn btn-outline-secondary btn-sm" type="button" data-occupancy-prev>Previous Scene</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" type="button" data-occupancy-next>Next Scene</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -255,10 +269,10 @@
|
||||
<span class="floor-plan-block-label">@block.DisplayLabel</span>
|
||||
<span class="floor-plan-resize-handle" data-resize-handle aria-hidden="true"></span>
|
||||
</button>
|
||||
if (block.LocationID.HasValue && firstBlockIdByLocation.GetValueOrDefault(block.LocationID.Value) == block.FloorPlanBlockID)
|
||||
if (block.LocationID.HasValue && firstBlockIdByFloorLocation.GetValueOrDefault($"{block.FloorPlanFloorID}:{block.LocationID.Value}") == block.FloorPlanBlockID)
|
||||
{
|
||||
var locationTokens = Model.OccupancyScenes
|
||||
.SelectMany(scene => occupancyTokensByLocation.GetValueOrDefault($"{scene.SceneID}:{block.LocationID.Value}") ?? [])
|
||||
.SelectMany(scene => occupancyTokensByFloorLocation.GetValueOrDefault($"{scene.SceneID}:{block.FloorPlanFloorID}:{block.LocationID.Value}") ?? [])
|
||||
.ToList();
|
||||
if (locationTokens.Any())
|
||||
{
|
||||
@ -268,6 +282,8 @@
|
||||
var tokenTitle = string.Join(", ", tokens.Select(x => x.DisplayName));
|
||||
<div class="floor-plan-occupancy-tray"
|
||||
style="--x:@block.X;--y:@block.Y;--w:@block.WidthCells;--h:@block.HeightCells;"
|
||||
data-occupancy-tray
|
||||
data-occupancy-floor="@block.FloorPlanFloorID"
|
||||
data-occupancy-location="@block.LocationID"
|
||||
data-occupancy-scene="@group.Key"
|
||||
title="@tokenTitle">
|
||||
@ -294,6 +310,23 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
@foreach (var scene in Model.OccupancyScenes)
|
||||
{
|
||||
var totalTokens = occupancyTokenCountByScene.GetValueOrDefault(scene.SceneID);
|
||||
var floorTokens = occupancyTokenCountBySceneFloor.GetValueOrDefault($"{scene.SceneID}:{floor.FloorPlanFloorID}");
|
||||
if (totalTokens == 0)
|
||||
{
|
||||
<div class="floor-plan-occupancy-message" data-occupancy-message data-occupancy-scene="@scene.SceneID" data-occupancy-floor="@floor.FloorPlanFloorID" hidden>No occupancy has been set for this scene.</div>
|
||||
}
|
||||
else if (floorTokens == 0)
|
||||
{
|
||||
<div class="floor-plan-occupancy-message" data-occupancy-message data-occupancy-scene="@scene.SceneID" data-occupancy-floor="@floor.FloorPlanFloorID" hidden>Scene has occupancy, but none of the assigned locations are placed on this floor.</div>
|
||||
}
|
||||
else if (floorTokens < totalTokens)
|
||||
{
|
||||
<div class="floor-plan-occupancy-message" data-occupancy-message data-occupancy-scene="@scene.SceneID" data-occupancy-floor="@floor.FloorPlanFloorID" hidden>Some occupants are on other floors.</div>
|
||||
}
|
||||
}
|
||||
@foreach (var transition in floorTransitions)
|
||||
{
|
||||
var transitionDetails = TransitionDetails(transition);
|
||||
@ -975,11 +1008,12 @@
|
||||
const gridCoordinatesToggle = editor.querySelector("[data-grid-coordinates-toggle]");
|
||||
const referenceFloorSelect = editor.querySelector("[data-reference-floor-select]");
|
||||
const modeButtons = [...editor.querySelectorAll("[data-floor-plan-mode]")];
|
||||
const occupancyControls = [...editor.querySelectorAll("[data-occupancy-controls], .floor-plan-occupancy-control")];
|
||||
const occupancyControls = [...editor.querySelectorAll("[data-occupancy-controls]")];
|
||||
const occupancySceneSelect = editor.querySelector("[data-occupancy-scene-select]");
|
||||
const occupancyPrev = editor.querySelector("[data-occupancy-prev]");
|
||||
const occupancyNext = editor.querySelector("[data-occupancy-next]");
|
||||
const occupancyTrays = [...editor.querySelectorAll("[data-occupancy-scene]")];
|
||||
const occupancyTrays = [...editor.querySelectorAll("[data-occupancy-tray]")];
|
||||
const occupancyMessages = [...editor.querySelectorAll("[data-occupancy-message]")];
|
||||
const initialSelectedBlockId = options.selectedBlockId || "@selectedBlockId";
|
||||
const saveStatusText = {
|
||||
saved: "Saved",
|
||||
@ -1057,8 +1091,12 @@
|
||||
function refreshOccupancyTrays() {
|
||||
const sceneId = selectedOccupancySceneId();
|
||||
const showOccupancy = editor.classList.contains("floor-plan-editor--occupancy");
|
||||
const activeFloorId = tabs.find(tab => tab.classList.contains("active"))?.dataset.floorTab || "";
|
||||
occupancyTrays.forEach(tray => {
|
||||
tray.hidden = !showOccupancy || tray.dataset.occupancyScene !== sceneId;
|
||||
tray.hidden = !showOccupancy || tray.dataset.occupancyScene !== sceneId || tray.dataset.occupancyFloor !== activeFloorId;
|
||||
});
|
||||
occupancyMessages.forEach(message => {
|
||||
message.hidden = !showOccupancy || message.dataset.occupancyScene !== sceneId || message.dataset.occupancyFloor !== activeFloorId;
|
||||
});
|
||||
}
|
||||
|
||||
@ -1411,6 +1449,7 @@
|
||||
checkOverlaps();
|
||||
refreshReferenceOverlay();
|
||||
refreshTransitionDestination(floorId);
|
||||
refreshOccupancyTrays();
|
||||
}
|
||||
|
||||
function backgroundFields(floorId) {
|
||||
|
||||
@ -4511,13 +4511,11 @@ body.dragging-location [data-drag-type="location"] {
|
||||
}
|
||||
|
||||
.floor-plan-editor-toolbar {
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, .92);
|
||||
border: 1px solid var(--border-color, #d8ded9);
|
||||
border-radius: 8px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
padding: 10px 12px;
|
||||
position: sticky;
|
||||
@ -4525,6 +4523,58 @@ body.dragging-location [data-drag-type="location"] {
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.floor-plan-toolbar-main,
|
||||
.floor-plan-toolbar-floor-group,
|
||||
.floor-plan-toolbar-control-group,
|
||||
.floor-plan-toolbar-action-group,
|
||||
.floor-plan-occupancy-scene-row {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.floor-plan-toolbar-main {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.floor-plan-toolbar-floor-group {
|
||||
flex: 1 1 420px;
|
||||
min-width: min(100%, 320px);
|
||||
}
|
||||
|
||||
.floor-plan-active-floor {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.floor-plan-toolbar-control-group {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.floor-plan-toolbar-action-group {
|
||||
flex: 1 1 360px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.floor-plan-occupancy-scene-row {
|
||||
border-top: 1px solid rgba(47, 111, 99, .12);
|
||||
padding-top: 9px;
|
||||
}
|
||||
|
||||
.floor-plan-occupancy-scene-row[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.floor-plan-occupancy-scene-row .floor-plan-reference-control {
|
||||
flex: 1 1 320px;
|
||||
min-width: min(100%, 320px);
|
||||
}
|
||||
|
||||
.floor-plan-occupancy-scene-row .form-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.floor-plan-editor-toolbar .muted {
|
||||
display: block;
|
||||
font-size: .76rem;
|
||||
@ -5244,16 +5294,16 @@ body.dragging-location [data-drag-type="location"] {
|
||||
.floor-plan-occupancy-tray {
|
||||
position: absolute;
|
||||
z-index: 14;
|
||||
grid-column: calc(var(--x) + 1) / span var(--w);
|
||||
grid-row: calc(var(--y) + 1) / span var(--h);
|
||||
align-self: end;
|
||||
justify-self: center;
|
||||
height: calc(var(--h) * var(--cell-size));
|
||||
left: calc(var(--x) * var(--cell-size));
|
||||
top: calc(var(--y) * var(--cell-size));
|
||||
width: calc(var(--w) * var(--cell-size));
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.18rem;
|
||||
margin: 0 0 0.25rem;
|
||||
padding: 0 0 0.25rem;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@ -5299,6 +5349,29 @@ body.dragging-location [data-drag-type="location"] {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.floor-plan-occupancy-message {
|
||||
align-self: start;
|
||||
background: rgba(255, 255, 255, .94);
|
||||
border: 1px solid rgba(47, 111, 99, .18);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 6px 16px rgba(30, 37, 43, .08);
|
||||
color: var(--muted-color, #6b756f);
|
||||
font-size: .82rem;
|
||||
font-weight: 700;
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 1;
|
||||
justify-self: center;
|
||||
margin-top: .55rem;
|
||||
max-width: min(92%, 520px);
|
||||
padding: .45rem .7rem;
|
||||
position: relative;
|
||||
z-index: 16;
|
||||
}
|
||||
|
||||
.floor-plan-occupancy-message[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.floor-plan-transition-accordion .accordion-item {
|
||||
background: transparent;
|
||||
border-color: var(--border-color, #d8ded9);
|
||||
@ -5502,6 +5575,13 @@ body.dragging-location [data-drag-type="location"] {
|
||||
background: rgba(31, 38, 48, .92);
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .floor-plan-occupancy-message,
|
||||
.dark .floor-plan-occupancy-message {
|
||||
background: rgba(31, 38, 48, .94);
|
||||
border-color: rgba(212, 154, 98, .24);
|
||||
color: #d9c8b8;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .floor-plan-transition-accordion .accordion-item,
|
||||
.dark .floor-plan-transition-accordion .accordion-item {
|
||||
border-color: rgba(212, 154, 98, .22);
|
||||
|
||||
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