510 lines
28 KiB
Plaintext
510 lines
28 KiB
Plaintext
@model FloorPlanEditorViewModel
|
|
@{
|
|
ViewData["Title"] = Model.FloorPlan.FloorPlanID == 0 ? "New Floor Plan" : Model.FloorPlan.Name;
|
|
ViewData["ProjectSection"] = "Floor Plans";
|
|
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 selectedBlockId = int.TryParse(Context.Request.Query["selectedBlockId"], out var parsedSelectedBlockId) ? parsedSelectedBlockId : Model.Blocks.FirstOrDefault()?.FloorPlanBlockID ?? 0;
|
|
string BlockClass(string blockType) => $"floor-plan-block floor-plan-block--{blockType.ToLowerInvariant()}";
|
|
}
|
|
|
|
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
|
|
<a asp-controller="Projects" asp-action="Index">Projects</a>
|
|
<a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">@Model.Project.ProjectName</a>
|
|
<a asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Floor Plans</a>
|
|
<span>@Model.FloorPlan.Name</span>
|
|
</nav>
|
|
|
|
<partial name="_ProjectSectionNav" model="Model.Project" />
|
|
|
|
<div class="page-heading compact">
|
|
<div>
|
|
<p class="eyebrow">Location floor plan</p>
|
|
<h1>@Model.FloorPlan.Name</h1>
|
|
</div>
|
|
<a class="btn btn-outline-secondary" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Back to floor plans</a>
|
|
</div>
|
|
|
|
@if (TempData["FloorPlanMessage"] is string message)
|
|
{
|
|
<div class="alert alert-success">@message</div>
|
|
}
|
|
@if (TempData["FloorPlanError"] is string error)
|
|
{
|
|
<div class="alert alert-danger">@error</div>
|
|
}
|
|
|
|
<section class="edit-panel floor-plan-meta">
|
|
<form asp-action="SavePlan" method="post" class="row g-3">
|
|
<input asp-for="FloorPlan.FloorPlanID" type="hidden" name="FloorPlanID" />
|
|
<input asp-for="FloorPlan.ProjectID" type="hidden" name="ProjectID" />
|
|
<div class="col-md-5">
|
|
<label asp-for="FloorPlan.Name" class="form-label"></label>
|
|
<input asp-for="FloorPlan.Name" class="form-control" name="Name" />
|
|
<span asp-validation-for="FloorPlan.Name" class="text-danger"></span>
|
|
</div>
|
|
<div class="col-md-5">
|
|
<label asp-for="FloorPlan.Description" class="form-label"></label>
|
|
<input asp-for="FloorPlan.Description" class="form-control" name="Description" />
|
|
</div>
|
|
<div class="col-md-2 d-flex align-items-end">
|
|
<button class="btn btn-outline-primary w-100" type="submit">Save details</button>
|
|
</div>
|
|
</form>
|
|
</section>
|
|
|
|
<section class="edit-panel floor-plan-tools">
|
|
<form asp-action="AddFloor" method="post" class="row g-2 align-items-end">
|
|
<input asp-for="NewFloor.FloorPlanID" type="hidden" name="FloorPlanID" />
|
|
<input asp-for="NewFloor.SortOrder" type="hidden" name="SortOrder" />
|
|
<div class="col-md-4">
|
|
<label class="form-label">New floor</label>
|
|
<input asp-for="NewFloor.Name" class="form-control form-control-sm" name="Name" />
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label">Width</label>
|
|
<input asp-for="NewFloor.GridWidth" class="form-control form-control-sm" name="GridWidth" min="8" max="80" />
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label">Height</label>
|
|
<input asp-for="NewFloor.GridHeight" class="form-control form-control-sm" name="GridHeight" min="8" max="80" />
|
|
</div>
|
|
<div class="col-md-2">
|
|
<button class="btn btn-outline-primary btn-sm w-100" type="submit">Add floor</button>
|
|
</div>
|
|
</form>
|
|
</section>
|
|
|
|
@if (!Model.Floors.Any())
|
|
{
|
|
<section class="empty-panel">
|
|
<h2>No floors yet</h2>
|
|
<p>Add a floor to start arranging linked locations.</p>
|
|
</section>
|
|
}
|
|
else
|
|
{
|
|
<form asp-action="SaveLayout" method="post" class="floor-plan-editor" data-floor-plan-editor>
|
|
<input asp-for="FloorPlan.FloorPlanID" type="hidden" />
|
|
<input type="hidden" name="floorPlanId" value="@Model.FloorPlan.FloorPlanID" />
|
|
<input asp-for="FloorPlan.ProjectID" type="hidden" />
|
|
<input asp-for="FloorPlan.Name" type="hidden" />
|
|
<input asp-for="FloorPlan.Description" type="hidden" />
|
|
|
|
<section 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">@floor.Name</button>
|
|
}
|
|
</section>
|
|
|
|
<div class="floor-plan-layout">
|
|
<section class="floor-plan-stage">
|
|
@for (var i = 0; i < Model.Floors.Count; i++)
|
|
{
|
|
var floor = Model.Floors[i];
|
|
var isActive = floor.FloorPlanFloorID == activeFloor?.FloorPlanFloorID;
|
|
var floorBlocks = blocksByFloor.GetValueOrDefault(floor.FloorPlanFloorID) ?? [];
|
|
<div class="floor-plan-floor @(isActive ? "active" : string.Empty)" data-floor-panel="@floor.FloorPlanFloorID">
|
|
<div class="floor-plan-floor-header">
|
|
<div>
|
|
<input asp-for="Floors[i].FloorPlanFloorID" type="hidden" />
|
|
<input asp-for="Floors[i].FloorPlanID" type="hidden" />
|
|
<input asp-for="Floors[i].SortOrder" type="hidden" />
|
|
<label class="form-label">Floor name</label>
|
|
<input asp-for="Floors[i].Name" class="form-control form-control-sm" />
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Grid width</label>
|
|
<input asp-for="Floors[i].GridWidth" class="form-control form-control-sm" min="8" max="80" data-floor-width="@floor.FloorPlanFloorID" />
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Grid height</label>
|
|
<input asp-for="Floors[i].GridHeight" class="form-control form-control-sm" min="8" max="80" data-floor-height="@floor.FloorPlanFloorID" />
|
|
</div>
|
|
<button class="btn btn-outline-danger btn-sm"
|
|
type="button"
|
|
data-bs-toggle="modal"
|
|
data-bs-target="#delete-floor-@floor.FloorPlanFloorID">Delete floor</button>
|
|
</div>
|
|
|
|
<div class="floor-plan-grid-wrap">
|
|
<div class="floor-plan-grid"
|
|
style="--grid-width:@floor.GridWidth;--grid-height:@floor.GridHeight;"
|
|
data-floor-grid="@floor.FloorPlanFloorID"
|
|
data-grid-width="@floor.GridWidth"
|
|
data-grid-height="@floor.GridHeight">
|
|
@foreach (var block in floorBlocks)
|
|
{
|
|
var blockIndex = Model.Blocks.FindIndex(x => x.FloorPlanBlockID == block.FloorPlanBlockID);
|
|
<button type="button"
|
|
class="@BlockClass(block.BlockType)"
|
|
style="--x:@block.X;--y:@block.Y;--w:@block.WidthCells;--h:@block.HeightCells;"
|
|
data-block="@block.FloorPlanBlockID"
|
|
data-block-index="@blockIndex"
|
|
data-floor-id="@block.FloorPlanFloorID">
|
|
<span>@block.DisplayLabel</span>
|
|
<span class="floor-plan-resize-handle" data-resize-handle aria-hidden="true"></span>
|
|
</button>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</section>
|
|
|
|
<aside class="floor-plan-side-panel">
|
|
@if (Model.LocationOptions.Any())
|
|
{
|
|
<section class="floor-plan-add-block-panel">
|
|
<h2>Add block</h2>
|
|
<p class="muted">Adds a block to <span data-current-floor-name>@activeFloor?.Name</span>.</p>
|
|
<input type="hidden" name="FloorPlanBlockID" value="0" form="floor-plan-add-block-form" />
|
|
<input type="hidden" name="FloorPlanFloorID" value="@activeFloor?.FloorPlanFloorID" data-add-block-floor-id form="floor-plan-add-block-form" />
|
|
<input type="hidden" name="X" value="0" form="floor-plan-add-block-form" />
|
|
<input type="hidden" name="Y" value="0" form="floor-plan-add-block-form" />
|
|
<input type="hidden" name="WidthCells" value="1" data-add-block-width form="floor-plan-add-block-form" />
|
|
<input type="hidden" name="HeightCells" value="1" data-add-block-height form="floor-plan-add-block-form" />
|
|
<div class="mb-2">
|
|
<label class="form-label" for="add-block-location">Location</label>
|
|
<select id="add-block-location" class="form-select form-select-sm" name="LocationID" asp-items="Model.LocationOptions" form="floor-plan-add-block-form"></select>
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label" for="add-block-type">Block type</label>
|
|
<select id="add-block-type" class="form-select form-select-sm" name="BlockType" asp-items="Model.BlockTypeOptions" form="floor-plan-add-block-form"></select>
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label" for="add-block-size-preset">Size preset</label>
|
|
<select id="add-block-size-preset" class="form-select form-select-sm" data-size-preset>
|
|
<option value="1x1">Small Room 1 x 1</option>
|
|
<option value="2x1" selected>Medium Room 2 x 1</option>
|
|
<option value="2x2">Large Room 2 x 2</option>
|
|
<option value="1x3">Corridor 1 x 3</option>
|
|
<option value="3x2">Wide Hall 3 x 2</option>
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" for="add-block-label">Label override</label>
|
|
<input id="add-block-label" class="form-control form-control-sm" name="LabelOverride" form="floor-plan-add-block-form" />
|
|
</div>
|
|
<button class="btn btn-primary btn-sm w-100" type="submit" form="floor-plan-add-block-form">Add block</button>
|
|
</section>
|
|
}
|
|
|
|
<h2>Selected block</h2>
|
|
<p class="muted" data-no-selection>Select a block on the grid to edit its details.</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++)
|
|
{
|
|
<div class="floor-plan-block-editor d-none" data-block-editor="@Model.Blocks[i].FloorPlanBlockID">
|
|
<input asp-for="Blocks[i].FloorPlanBlockID" type="hidden" />
|
|
<input asp-for="Blocks[i].FloorPlanFloorID" type="hidden" data-block-field="floor" />
|
|
<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</label>
|
|
<select asp-for="Blocks[i].LocationID" asp-items="Model.LocationOptions" class="form-select form-select-sm"></select>
|
|
</div>
|
|
<div class="mb-2">
|
|
<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</label>
|
|
<input asp-for="Blocks[i].X" class="form-control form-control-sm" min="0" max="79" data-block-field="x" />
|
|
</div>
|
|
<div class="col-6">
|
|
<label class="form-label">Y</label>
|
|
<input asp-for="Blocks[i].Y" class="form-control form-control-sm" min="0" max="79" data-block-field="y" />
|
|
</div>
|
|
<div class="col-6">
|
|
<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</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</label>
|
|
<input asp-for="Blocks[i].LabelOverride" class="form-control form-control-sm" />
|
|
</div>
|
|
<div class="mt-2">
|
|
<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">
|
|
<button class="btn btn-outline-danger btn-sm"
|
|
type="button"
|
|
data-bs-toggle="modal"
|
|
data-bs-target="#delete-block-@Model.Blocks[i].FloorPlanBlockID">Delete block</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
</aside>
|
|
</div>
|
|
|
|
<div class="button-row mt-3">
|
|
<button class="btn btn-primary" type="submit">Save layout</button>
|
|
</div>
|
|
</form>
|
|
|
|
<form id="floor-plan-add-block-form" asp-action="AddBlock" method="post">
|
|
<input type="hidden" name="floorPlanId" value="@Model.FloorPlan.FloorPlanID" />
|
|
</form>
|
|
|
|
@foreach (var floor in Model.Floors)
|
|
{
|
|
var placedBlockCount = Model.Blocks.Count(x => x.FloorPlanFloorID == floor.FloorPlanFloorID);
|
|
<div class="modal fade" id="delete-floor-@floor.FloorPlanFloorID" tabindex="-1" aria-labelledby="delete-floor-title-@floor.FloorPlanFloorID" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content plotline-confirm-modal">
|
|
<div class="modal-header">
|
|
<h2 class="modal-title fs-5" id="delete-floor-title-@floor.FloorPlanFloorID">Delete floor?</h2>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p>Delete <strong>@floor.Name</strong>?</p>
|
|
<p>This deletes @placedBlockCount placed block(s) on this floor. Linked Locations will not be deleted.</p>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<form asp-action="DeleteFloor" method="post" data-no-delete-confirm="true">
|
|
<input type="hidden" name="floorPlanId" value="@Model.FloorPlan.FloorPlanID" />
|
|
<input type="hidden" name="floorPlanFloorId" value="@floor.FloorPlanFloorID" />
|
|
<button class="btn btn-danger" type="submit">Delete floor</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@foreach (var block in Model.Blocks)
|
|
{
|
|
<div class="modal fade" id="delete-block-@block.FloorPlanBlockID" tabindex="-1" aria-labelledby="delete-block-title-@block.FloorPlanBlockID" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content plotline-confirm-modal">
|
|
<div class="modal-header">
|
|
<h2 class="modal-title fs-5" id="delete-block-title-@block.FloorPlanBlockID">Delete placed block?</h2>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p>Remove the placed block for <strong>@block.DisplayLabel</strong>?</p>
|
|
<p>Only this block is removed from the floor plan. The linked Location will not be deleted.</p>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<form asp-action="DeleteBlock" method="post" data-no-delete-confirm="true">
|
|
<input type="hidden" name="floorPlanId" value="@Model.FloorPlan.FloorPlanID" />
|
|
<input type="hidden" name="floorPlanBlockId" value="@block.FloorPlanBlockID" />
|
|
<button class="btn btn-danger" type="submit">Delete block</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
@section Scripts {
|
|
<partial name="_ValidationScriptsPartial" />
|
|
<script>
|
|
(() => {
|
|
const editor = document.querySelector("[data-floor-plan-editor]");
|
|
if (!editor) return;
|
|
|
|
const tabs = [...editor.querySelectorAll("[data-floor-tab]")];
|
|
const panels = [...editor.querySelectorAll("[data-floor-panel]")];
|
|
const blocks = [...editor.querySelectorAll("[data-block]")];
|
|
const noSelection = editor.querySelector("[data-no-selection]");
|
|
const warning = editor.querySelector("[data-overlap-warning]");
|
|
const addBlockFloor = document.querySelector("[data-add-block-floor-id]");
|
|
const addBlockFloorName = document.querySelector("[data-current-floor-name]");
|
|
const sizePreset = document.querySelector("[data-size-preset]");
|
|
const addBlockWidth = document.querySelector("[data-add-block-width]");
|
|
const addBlockHeight = document.querySelector("[data-add-block-height]");
|
|
const initialSelectedBlockId = "@selectedBlockId";
|
|
let selectedBlock = null;
|
|
let pointerState = null;
|
|
|
|
function setActiveFloor(floorId) {
|
|
tabs.forEach(tab => tab.classList.toggle("active", tab.dataset.floorTab === floorId));
|
|
panels.forEach(panel => panel.classList.toggle("active", panel.dataset.floorPanel === floorId));
|
|
const activeTab = tabs.find(tab => tab.dataset.floorTab === floorId);
|
|
if (addBlockFloor) addBlockFloor.value = floorId;
|
|
if (addBlockFloorName && activeTab) addBlockFloorName.textContent = activeTab.dataset.floorName || activeTab.textContent || "the current floor";
|
|
checkOverlaps();
|
|
}
|
|
|
|
function selectBlock(block) {
|
|
selectedBlock = block;
|
|
blocks.forEach(item => item.classList.toggle("selected", item === block));
|
|
editor.querySelectorAll("[data-block-editor]").forEach(panel => panel.classList.add("d-none"));
|
|
const panel = editor.querySelector(`[data-block-editor="${block.dataset.block}"]`);
|
|
if (panel) panel.classList.remove("d-none");
|
|
if (noSelection) noSelection.classList.add("d-none");
|
|
}
|
|
|
|
function fieldsFor(block) {
|
|
const panel = editor.querySelector(`[data-block-editor="${block.dataset.block}"]`);
|
|
return {
|
|
panel,
|
|
floor: panel?.querySelector("[data-block-field='floor']"),
|
|
x: panel?.querySelector("[data-block-field='x']"),
|
|
y: panel?.querySelector("[data-block-field='y']"),
|
|
w: panel?.querySelector("[data-block-field='w']"),
|
|
h: panel?.querySelector("[data-block-field='h']"),
|
|
type: panel?.querySelector("[data-block-field='type']")
|
|
};
|
|
}
|
|
|
|
function gridFor(block) {
|
|
return editor.querySelector(`[data-floor-grid="${block.dataset.floorId}"]`);
|
|
}
|
|
|
|
function clampBlock(block, values) {
|
|
const grid = gridFor(block);
|
|
const gridWidth = Number(grid?.dataset.gridWidth || 24);
|
|
const gridHeight = Number(grid?.dataset.gridHeight || 16);
|
|
const w = Math.max(1, Math.min(Number(values.w), gridWidth));
|
|
const h = Math.max(1, Math.min(Number(values.h), gridHeight));
|
|
const x = Math.max(0, Math.min(Number(values.x), gridWidth - w));
|
|
const y = Math.max(0, Math.min(Number(values.y), gridHeight - h));
|
|
return { x, y, w, h };
|
|
}
|
|
|
|
function readBlock(block) {
|
|
const fields = fieldsFor(block);
|
|
return {
|
|
x: Number(fields.x?.value || block.style.getPropertyValue("--x") || 0),
|
|
y: Number(fields.y?.value || block.style.getPropertyValue("--y") || 0),
|
|
w: Number(fields.w?.value || block.style.getPropertyValue("--w") || 1),
|
|
h: Number(fields.h?.value || block.style.getPropertyValue("--h") || 1)
|
|
};
|
|
}
|
|
|
|
function writeBlock(block, values) {
|
|
const next = clampBlock(block, values);
|
|
block.style.setProperty("--x", next.x);
|
|
block.style.setProperty("--y", next.y);
|
|
block.style.setProperty("--w", next.w);
|
|
block.style.setProperty("--h", next.h);
|
|
const fields = fieldsFor(block);
|
|
if (fields.x) fields.x.value = next.x;
|
|
if (fields.y) fields.y.value = next.y;
|
|
if (fields.w) fields.w.value = next.w;
|
|
if (fields.h) fields.h.value = next.h;
|
|
checkOverlaps();
|
|
}
|
|
|
|
function cellFromPointer(grid, event) {
|
|
const rect = grid.getBoundingClientRect();
|
|
const gridWidth = Number(grid.dataset.gridWidth || 24);
|
|
const gridHeight = Number(grid.dataset.gridHeight || 16);
|
|
return {
|
|
x: Math.floor((event.clientX - rect.left) / (rect.width / gridWidth)),
|
|
y: Math.floor((event.clientY - rect.top) / (rect.height / gridHeight))
|
|
};
|
|
}
|
|
|
|
function checkOverlaps() {
|
|
const activePanel = panels.find(panel => panel.classList.contains("active"));
|
|
if (!activePanel) return;
|
|
const visibleBlocks = [...activePanel.querySelectorAll("[data-block]")].map(block => ({ block, ...readBlock(block) }));
|
|
let overlaps = false;
|
|
visibleBlocks.forEach(a => {
|
|
a.block.classList.remove("overlap");
|
|
visibleBlocks.forEach(b => {
|
|
if (a === b) return;
|
|
const separated = a.x + a.w <= b.x || b.x + b.w <= a.x || a.y + a.h <= b.y || b.y + b.h <= a.y;
|
|
if (!separated) {
|
|
overlaps = true;
|
|
a.block.classList.add("overlap");
|
|
b.block.classList.add("overlap");
|
|
}
|
|
});
|
|
});
|
|
warning?.classList.toggle("d-none", !overlaps);
|
|
}
|
|
|
|
tabs.forEach(tab => tab.addEventListener("click", () => setActiveFloor(tab.dataset.floorTab)));
|
|
|
|
blocks.forEach(block => {
|
|
block.addEventListener("pointerdown", event => {
|
|
selectBlock(block);
|
|
const grid = gridFor(block);
|
|
if (!grid) return;
|
|
const start = cellFromPointer(grid, event);
|
|
const current = readBlock(block);
|
|
const resizing = event.target.matches("[data-resize-handle]");
|
|
pointerState = { block, grid, start, current, resizing };
|
|
block.setPointerCapture(event.pointerId);
|
|
event.preventDefault();
|
|
});
|
|
});
|
|
|
|
editor.addEventListener("pointermove", event => {
|
|
if (!pointerState) return;
|
|
const currentCell = cellFromPointer(pointerState.grid, event);
|
|
const dx = currentCell.x - pointerState.start.x;
|
|
const dy = currentCell.y - pointerState.start.y;
|
|
if (pointerState.resizing) {
|
|
writeBlock(pointerState.block, {
|
|
...pointerState.current,
|
|
w: pointerState.current.w + dx,
|
|
h: pointerState.current.h + dy
|
|
});
|
|
} else {
|
|
writeBlock(pointerState.block, {
|
|
...pointerState.current,
|
|
x: pointerState.current.x + dx,
|
|
y: pointerState.current.y + dy
|
|
});
|
|
}
|
|
});
|
|
|
|
editor.addEventListener("pointerup", () => pointerState = null);
|
|
editor.addEventListener("pointercancel", () => pointerState = null);
|
|
|
|
editor.querySelectorAll("[data-block-field='x'],[data-block-field='y'],[data-block-field='w'],[data-block-field='h']").forEach(input => {
|
|
input.addEventListener("change", () => {
|
|
if (!selectedBlock) return;
|
|
writeBlock(selectedBlock, readBlock(selectedBlock));
|
|
});
|
|
});
|
|
|
|
editor.querySelectorAll("[data-block-field='type']").forEach(select => {
|
|
select.addEventListener("change", () => {
|
|
if (!selectedBlock) return;
|
|
selectedBlock.className = selectedBlock.className
|
|
.split(" ")
|
|
.filter(name => !name.startsWith("floor-plan-block--"))
|
|
.concat(`floor-plan-block--${select.value.toLowerCase()}`)
|
|
.join(" ");
|
|
});
|
|
});
|
|
|
|
sizePreset?.addEventListener("change", () => {
|
|
const [width, height] = (sizePreset.value || "2x1").split("x");
|
|
if (addBlockWidth) addBlockWidth.value = width || "2";
|
|
if (addBlockHeight) addBlockHeight.value = height || "1";
|
|
});
|
|
sizePreset?.dispatchEvent(new Event("change"));
|
|
|
|
const initialBlock = blocks.find(block => block.dataset.block === initialSelectedBlockId) || blocks[0];
|
|
if (initialBlock) {
|
|
setActiveFloor(initialBlock.dataset.floorId);
|
|
selectBlock(initialBlock);
|
|
} else if (tabs[0]) {
|
|
setActiveFloor(tabs[0].dataset.floorTab);
|
|
}
|
|
checkOverlaps();
|
|
})();
|
|
</script>
|
|
}
|