Implemented the focused Floor Plans performance pass.

Measured findings:
Authenticated Calendar House editor load measured around 600 ms locally.
SQL calls were generally around 50-65 ms after warm-up.
The main recent bloat was the Help UI wiring: the editor had 615 help icons and about 1.5 MB of HTML.
Optimisations made:
Reduced repeated hidden help icons in [Edit.cshtml (line 409)](C:/Source/PlotLine/PlotLine/Views/FloorPlans/Edit.cshtml:409), especially per-block editors and repeated transition edit modals.
Help icons now measure at 107, with HTML reduced to about 1.26 MB.
Optimised occupancy warning calculation in [CoreServices.cs (line 7883)](C:/Source/PlotLine/PlotLine/Services/CoreServices.cs:7883) by grouping occupancy rows once instead of repeatedly scanning them per scene.
Added new index migration [083_FloorPlanEditorOccupancyIndexes.sql](C:/Source/PlotLine/PlotLine/Sql/083_FloorPlanEditorOccupancyIndexes.sql) for Floor Plan scene/occupancy lookups. Applied locally.
Verified:
Structure/Occupancy mode still works.
Previous/Next scene changes the selected scene.
Tokens and transitions still render.
Help icons still resolve to articles.
dotnet build PlotLine.slnx passes with 0 warnings and 0 errors.
No Floor Plans behaviour, transition logic, occupancy data model, or feature architecture changes.
This commit is contained in:
Nick Beckley 2026-06-22 16:58:20 +01:00
parent b998760e96
commit 350b8ba9ab
3 changed files with 63 additions and 16 deletions

View File

@ -7880,6 +7880,7 @@ public sealed class FloorPlanService(
var warnings = new List<FloorPlanOccupancyWarningViewModel>();
var blocksByLocation = blocks.GroupBy(x => x.LocationID).ToDictionary(x => x.Key, x => x.ToList());
var floors = blocks.Select(x => x.FloorPlanFloorID).Distinct().ToList();
var occupancyRowsByScene = occupancyRows.GroupBy(x => x.SceneID).ToDictionary(x => x.Key, x => x.ToList());
var visibleEntranceLocationIds = transitions
.Where(x => IsVisibleEntranceTransition(x.TransitionType))
.SelectMany(x => new[] { x.FromLocationID, x.ToLocationID })
@ -7888,7 +7889,7 @@ public sealed class FloorPlanService(
foreach (var sceneGroup in participants.GroupBy(x => x.SceneID))
{
var sceneId = sceneGroup.Key;
var sceneRows = occupancyRows.Where(x => x.SceneID == sceneId).ToList();
var sceneRows = occupancyRowsByScene.GetValueOrDefault(sceneId) ?? [];
var occupiedCharacterIds = sceneRows.Where(x => x.CharacterID.HasValue).Select(x => x.CharacterID!.Value).ToHashSet();
var occupiedAssetIds = sceneRows.Where(x => x.StoryAssetID.HasValue).Select(x => x.StoryAssetID!.Value).ToHashSet();

View File

@ -0,0 +1,36 @@
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_Scenes_FloorPlanID' AND object_id = OBJECT_ID(N'dbo.Scenes'))
CREATE INDEX IX_Scenes_FloorPlanID ON dbo.Scenes(FloorPlanID, IsArchived, ChapterID, SceneNumber, SortOrder)
INCLUDE (SceneTitle, InitialFloorPlanFloorID);
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_Scenes_InitialFloorPlanFloorID' AND object_id = OBJECT_ID(N'dbo.Scenes'))
CREATE INDEX IX_Scenes_InitialFloorPlanFloorID ON dbo.Scenes(InitialFloorPlanFloorID)
WHERE InitialFloorPlanFloorID IS NOT NULL;
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_SceneFloorPlanOccupancy_FloorPlanID' AND object_id = OBJECT_ID(N'dbo.SceneFloorPlanOccupancy'))
CREATE INDEX IX_SceneFloorPlanOccupancy_FloorPlanID ON dbo.SceneFloorPlanOccupancy(FloorPlanID, SceneID, LocationID)
INCLUDE (FloorPlanFloorID, CharacterID, StoryAssetID);
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_SceneFloorPlanOccupancy_LocationID' AND object_id = OBJECT_ID(N'dbo.SceneFloorPlanOccupancy'))
CREATE INDEX IX_SceneFloorPlanOccupancy_LocationID ON dbo.SceneFloorPlanOccupancy(LocationID)
INCLUDE (SceneID, FloorPlanID, FloorPlanFloorID, CharacterID, StoryAssetID);
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_SceneFloorPlanOccupancy_CharacterID' AND object_id = OBJECT_ID(N'dbo.SceneFloorPlanOccupancy'))
CREATE INDEX IX_SceneFloorPlanOccupancy_CharacterID ON dbo.SceneFloorPlanOccupancy(CharacterID)
INCLUDE (SceneID, FloorPlanID, LocationID)
WHERE CharacterID IS NOT NULL;
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_SceneFloorPlanOccupancy_StoryAssetID' AND object_id = OBJECT_ID(N'dbo.SceneFloorPlanOccupancy'))
CREATE INDEX IX_SceneFloorPlanOccupancy_StoryAssetID ON dbo.SceneFloorPlanOccupancy(StoryAssetID)
INCLUDE (SceneID, FloorPlanID, LocationID)
WHERE StoryAssetID IS NOT NULL;
GO

View File

@ -407,6 +407,17 @@
<section class="floor-plan-tool-card" id="floor-plan-tool-selected" role="tabpanel" aria-labelledby="floor-plan-tool-tab-selected" data-toolbox-panel="selected" hidden>
<h2>Selected block <help-icon key="floorPlans.addBlock" /></h2>
<p class="muted" data-no-selection>Select a block on the grid to edit its details.</p>
<p class="muted">
Field help:
<help-icon key="floorPlans.fields.location" mode="inline" />
<help-icon key="floorPlans.fields.blockType" mode="inline" />
<help-icon key="floorPlans.fields.xCoordinate" mode="inline" />
<help-icon key="floorPlans.fields.yCoordinate" mode="inline" />
<help-icon key="floorPlans.fields.dimensions" mode="inline" />
<help-icon key="floorPlans.fields.labelOverride" mode="inline" />
<help-icon key="floorPlans.fields.notes" mode="inline" />
<help-icon key="floorPlans.deleteBlock" mode="inline" />
</p>
<div class="floor-plan-overlap-warning d-none" data-overlap-warning>Some blocks overlap on this floor.</div>
@for (var i = 0; i < Model.Blocks.Count; i++)
@ -417,16 +428,16 @@
<input asp-for="Blocks[i].LocationName" type="hidden" />
<input asp-for="Blocks[i].LocationPath" type="hidden" />
<div class="mb-2">
<label class="form-label">Location <help-icon key="floorPlans.fields.location" mode="inline" /></label>
<label class="form-label">Location</label>
<select asp-for="Blocks[i].LocationID" asp-items="Model.LocationOptions" class="form-select form-select-sm" data-block-field="location"></select>
</div>
<div class="mb-2">
<label class="form-label">Type <help-icon key="floorPlans.fields.blockType" mode="inline" /></label>
<label class="form-label">Type</label>
<select asp-for="Blocks[i].BlockType" asp-items="Model.BlockTypeOptions" class="form-select form-select-sm" data-block-field="type"></select>
</div>
<div class="row g-2">
<div class="col-6">
<label class="form-label">X <help-icon key="floorPlans.fields.xCoordinate" mode="inline" /></label>
<label class="form-label">X</label>
<select class="form-select form-select-sm"
id="Blocks_@(i)__X"
name="Blocks[@i].X"
@ -435,7 +446,7 @@
data-coordinate-value="@Model.Blocks[i].X"></select>
</div>
<div class="col-6">
<label class="form-label">Y <help-icon key="floorPlans.fields.yCoordinate" mode="inline" /></label>
<label class="form-label">Y</label>
<select class="form-select form-select-sm"
id="Blocks_@(i)__Y"
name="Blocks[@i].Y"
@ -444,20 +455,20 @@
data-coordinate-value="@Model.Blocks[i].Y"></select>
</div>
<div class="col-6">
<label class="form-label">Width <help-icon key="floorPlans.fields.dimensions" mode="inline" /></label>
<label class="form-label">Width</label>
<input asp-for="Blocks[i].WidthCells" class="form-control form-control-sm" min="1" max="80" data-block-field="w" />
</div>
<div class="col-6">
<label class="form-label">Height <help-icon key="floorPlans.fields.dimensions" mode="inline" /></label>
<label class="form-label">Height</label>
<input asp-for="Blocks[i].HeightCells" class="form-control form-control-sm" min="1" max="80" data-block-field="h" />
</div>
</div>
<div class="mt-2">
<label class="form-label">Label override <help-icon key="floorPlans.fields.labelOverride" mode="inline" /></label>
<label class="form-label">Label override</label>
<input asp-for="Blocks[i].LabelOverride" class="form-control form-control-sm" />
</div>
<div class="mt-2">
<label class="form-label">Notes <help-icon key="floorPlans.fields.notes" mode="inline" /></label>
<label class="form-label">Notes</label>
<textarea asp-for="Blocks[i].Notes" class="form-control form-control-sm" rows="3"></textarea>
</div>
<div class="button-row mt-3">
@ -465,7 +476,6 @@
type="button"
data-bs-toggle="modal"
data-bs-target="#delete-block-@Model.Blocks[i].FloorPlanBlockID">Delete block</button>
<help-icon key="floorPlans.deleteBlock" mode="inline" />
</div>
</div>
}
@ -886,7 +896,7 @@
<input type="hidden" name="FloorPlanTransitionID" value="@transition.FloorPlanTransitionID" />
<input type="hidden" name="FloorPlanFloorID" value="@transition.FloorPlanFloorID" />
<div class="mb-2">
<label class="form-label">From Location <help-icon key="floorPlans.fields.fromLocation" mode="inline" /></label>
<label class="form-label">From Location</label>
<select class="form-select form-select-sm" name="FromLocationID">
@foreach (var location in transitionLocationOptions)
{
@ -895,7 +905,7 @@
</select>
</div>
<div class="mb-2">
<label class="form-label">To Location <help-icon key="floorPlans.fields.toLocation" mode="inline" /></label>
<label class="form-label">To Location</label>
<select class="form-select form-select-sm" name="ToLocationID">
@foreach (var location in transitionLocationOptions)
{
@ -904,7 +914,7 @@
</select>
</div>
<div class="mb-2">
<label class="form-label">Transition Type <help-icon key="floorPlans.fields.transitionType" mode="inline" /></label>
<label class="form-label">Transition Type</label>
<select class="form-select form-select-sm" name="TransitionType" data-edit-transition-type>
@foreach (var option in Model.TransitionTypeOptions)
{
@ -913,7 +923,7 @@
</select>
</div>
<div class="mb-2" data-edit-transition-position-panel>
<label class="form-label">Position Within Location <help-icon key="floorPlans.fields.positionWithinLocation" mode="inline" /></label>
<label class="form-label">Position Within Location</label>
<select class="form-select form-select-sm" name="PositionWithinLocation">
@foreach (var option in transitionPositions)
{
@ -922,11 +932,11 @@
</select>
</div>
<div class="mb-2">
<label class="form-label">Display Label <help-icon key="floorPlans.fields.labelOverride" mode="inline" /></label>
<label class="form-label">Display Label</label>
<input class="form-control form-control-sm" name="DisplayLabel" value="@transition.DisplayLabel" />
</div>
<div class="mb-2">
<label class="form-label">Notes <help-icon key="floorPlans.fields.notes" mode="inline" /></label>
<label class="form-label">Notes</label>
<textarea class="form-control form-control-sm" name="Notes" rows="3">@transition.Notes</textarea>
</div>
<label class="floor-plan-checkbox">