Implement inspect mode for shape metadata and enhance overlay interactions
This commit is contained in:
parent
06e67d8341
commit
ab5e514e61
6 changed files with 325 additions and 38 deletions
|
|
@ -19,18 +19,33 @@ Phase 1 implementation choice:
|
|||
|
||||
Goal: promote editor-only content from "baked into the raster" to interactive overlay objects.
|
||||
|
||||
- keep the base map rendered as a flat server-generated tile surface
|
||||
- extract editor-only objects into a standalone overlay data stream
|
||||
- render those overlay items in the client as positioned interactive markers or sprites above the base map
|
||||
- on hover, slightly scale the hovered item and show a tooltip with its decoded metadata payload
|
||||
- completed: keep the base map rendered as a flat server-generated tile surface
|
||||
- completed: extract editor-only objects into a standalone overlay data stream
|
||||
- completed: render editor-only overlay items in the client as positioned sprite overlays above the base map
|
||||
- completed: remove editor-only records from the base raster so overlay shapes are not duplicated in the map tiles
|
||||
- completed: fix overlay transparency so the sprite background stays transparent instead of fading black
|
||||
- completed: add an inspection mode checkbox that shows metadata for any rendered shape under the cursor, not just editor overlays
|
||||
- improve roof detection before or during the overlay split because the current roof filtering still lets some roofs render when they should not
|
||||
- identify occluding helper geometry such as invisible walls and render those semitransparently so they remain legible without hiding too much of the map beneath them
|
||||
- fix pipe rendering because pipes currently are not showing up correctly
|
||||
- investigate force-field rendering because they appear yellow instead of the expected blue semitransparent look; this may be a debug-shape choice issue or a palette/color-rendering issue
|
||||
- likely revisit ScummVM Crusader handling in `D:\source\scummvm` to confirm what editor/debug records carry and how best to decode them for display
|
||||
|
||||
Current phase 2 status:
|
||||
|
||||
- the server now builds three outputs from one map build: base raster tiles, editor-only overlay sprites, and a shared inspectable-shape metadata stream
|
||||
- editor-only shapes are interactive overlay sprites rendered from their original decoded frames rather than synthetic markers
|
||||
- when inspect mode is active, the cursor reports metadata for whichever rendered shape is currently under it, including base-map shapes
|
||||
|
||||
Next steps:
|
||||
|
||||
- use inspect mode on representative maps to identify which visible structures are true roofs versus normal geometry so roof filtering can be tightened with evidence
|
||||
- decide which helper/occluder families should stay semitransparent overlays and which should eventually be hidden or toggled separately
|
||||
- inspect broken pipe shapes and compare their metadata against ScummVM handling to determine why they currently render incorrectly
|
||||
- inspect force-field shapes and compare palette or translucency traits against expected in-game appearance
|
||||
|
||||
Open questions for phase 2:
|
||||
|
||||
- which editor-only families should become interactive overlays versus remain baked into the base render
|
||||
- what exact metadata fields are reliable enough to expose in the tooltip
|
||||
- which helper/editor families should stay as overlay sprites versus gain their own visibility toggles
|
||||
- what exact metadata fields are reliable enough to expose in the tooltip long-term
|
||||
- whether some editor-only entries should be clustered, filtered, or toggled by family to keep dense maps usable
|
||||
|
|
|
|||
|
|
@ -180,6 +180,98 @@ function overlayNotes(item, info) {
|
|||
return notes;
|
||||
}
|
||||
|
||||
function classifyBaseKind(info) {
|
||||
if (info.isRoof) {
|
||||
return "roof";
|
||||
}
|
||||
if (info.isOccl || info.isInvitem) {
|
||||
return "helper";
|
||||
}
|
||||
if (info.isLand) {
|
||||
return "terrain";
|
||||
}
|
||||
return "base";
|
||||
}
|
||||
|
||||
function inspectLabel(layer, kind) {
|
||||
if (layer === "overlay") {
|
||||
return overlayLabel(kind);
|
||||
}
|
||||
if (kind === "roof") {
|
||||
return "Roof Shape";
|
||||
}
|
||||
if (kind === "helper") {
|
||||
return "Occluding Helper";
|
||||
}
|
||||
if (kind === "terrain") {
|
||||
return "Terrain Shape";
|
||||
}
|
||||
return "Map Shape";
|
||||
}
|
||||
|
||||
function serializeInspectableItem(node, minLeft, minTop, id, layer, stackOrder) {
|
||||
const { item, info, frame } = node;
|
||||
const kind = layer === "overlay" ? classifyOverlayKind(item, info) : classifyBaseKind(info);
|
||||
const sceneLeft = node.left - minLeft;
|
||||
const sceneTop = node.top - minTop;
|
||||
|
||||
return {
|
||||
id,
|
||||
layer,
|
||||
stackOrder,
|
||||
kind,
|
||||
label: inspectLabel(layer, kind),
|
||||
family: info.family,
|
||||
source: item.source,
|
||||
world: {
|
||||
x: item.x,
|
||||
y: item.y,
|
||||
z: item.z
|
||||
},
|
||||
mapNum: item.mapNum,
|
||||
npcNum: item.npcNum,
|
||||
nextItem: item.nextItem,
|
||||
quality: item.quality,
|
||||
shape: item.shape,
|
||||
frame: item.frame,
|
||||
frameSize: {
|
||||
width: frame.width,
|
||||
height: frame.height,
|
||||
xoff: frame.xoff,
|
||||
yoff: frame.yoff
|
||||
},
|
||||
screen: {
|
||||
left: sceneLeft,
|
||||
top: sceneTop,
|
||||
right: node.right - minLeft,
|
||||
bottom: node.bottom - minTop,
|
||||
width: node.right - node.left,
|
||||
height: node.bottom - node.top,
|
||||
anchorX: Math.trunc(sceneLeft + (node.right - node.left) / 2),
|
||||
anchorY: node.bottom - minTop
|
||||
},
|
||||
flags: {
|
||||
raw: item.flags,
|
||||
hex: toHex(item.flags),
|
||||
invisible: Boolean(item.flags & FLAG_INVISIBLE),
|
||||
flipped: Boolean(item.flags & FLAG_FLIPPED)
|
||||
},
|
||||
traits: {
|
||||
editor: info.isEditor,
|
||||
roof: info.isRoof,
|
||||
occluding: info.isOccl,
|
||||
translucent: info.isTranslucent,
|
||||
solid: info.isSolid,
|
||||
fixed: info.isFixed,
|
||||
land: info.isLand,
|
||||
draw: info.isDraw,
|
||||
invitem: info.isInvitem,
|
||||
animType: info.animType
|
||||
},
|
||||
notes: overlayNotes(item, info)
|
||||
};
|
||||
}
|
||||
|
||||
function serializeOverlayItem(node, minLeft, minTop, index) {
|
||||
const { item, info, frame } = node;
|
||||
const kind = classifyOverlayKind(item, info);
|
||||
|
|
@ -417,6 +509,7 @@ export class BuildManager {
|
|||
overlays: [],
|
||||
overlayNodes: [],
|
||||
overlayNodeById: new Map(),
|
||||
inspectables: [],
|
||||
minLeft: 0,
|
||||
minTop: 0,
|
||||
width: TILE_SIZE,
|
||||
|
|
@ -471,6 +564,7 @@ export class BuildManager {
|
|||
overlays: [],
|
||||
overlayNodes: [],
|
||||
overlayNodeById: new Map(),
|
||||
inspectables: [],
|
||||
minLeft: 0,
|
||||
minTop: 0,
|
||||
width: TILE_SIZE,
|
||||
|
|
@ -516,6 +610,34 @@ export class BuildManager {
|
|||
for (let index = 0; index < overlays.length; index += 1) {
|
||||
overlayNodeById.set(overlays[index].id, overlayProjection.projected[index]);
|
||||
}
|
||||
const inspectables = [];
|
||||
for (let index = 0; index < sorted.prepared.length; index += 1) {
|
||||
const node = sorted.prepared[index];
|
||||
const stackOrder = node.order >= 0 ? node.order : index;
|
||||
inspectables.push(
|
||||
serializeInspectableItem(
|
||||
node,
|
||||
bounds.minLeft,
|
||||
bounds.minTop,
|
||||
`base:${stackOrder}:${node.item.source}:${node.item.shape}:${node.item.frame}:${node.item.x}:${node.item.y}:${node.item.z}`,
|
||||
"base",
|
||||
stackOrder
|
||||
)
|
||||
);
|
||||
}
|
||||
for (let index = 0; index < overlays.length; index += 1) {
|
||||
inspectables.push(
|
||||
serializeInspectableItem(
|
||||
overlayProjection.projected[index],
|
||||
bounds.minLeft,
|
||||
bounds.minTop,
|
||||
`overlay:${overlays[index].id}`,
|
||||
"overlay",
|
||||
sorted.prepared.length + index
|
||||
)
|
||||
);
|
||||
}
|
||||
inspectables.sort((left, right) => left.stackOrder - right.stackOrder);
|
||||
const invalidItemCount = sorted.invalidItemCount + overlayProjection.invalidItemCount;
|
||||
const invalidItems = [...sorted.invalidItems, ...overlayProjection.invalidItems].slice(0, 20);
|
||||
|
||||
|
|
@ -566,6 +688,7 @@ export class BuildManager {
|
|||
overlays,
|
||||
overlayNodes: overlayProjection.projected,
|
||||
overlayNodeById,
|
||||
inspectables,
|
||||
minLeft: bounds.minLeft,
|
||||
minTop: bounds.minTop,
|
||||
width: bounds.width,
|
||||
|
|
@ -638,6 +761,13 @@ export class BuildManager {
|
|||
};
|
||||
}
|
||||
|
||||
getInspectData(jobId, gameId, mapId) {
|
||||
const job = this.requireReadyJob(jobId, gameId, mapId);
|
||||
return {
|
||||
items: job.build.inspectables
|
||||
};
|
||||
}
|
||||
|
||||
async renderOverlaySprite(jobId, gameId, mapId, overlayId, format = "webp") {
|
||||
const job = this.requireReadyJob(jobId, gameId, mapId);
|
||||
const overlay = job.build.overlays.find((item) => item.id === overlayId);
|
||||
|
|
|
|||
|
|
@ -291,16 +291,15 @@ select:disabled,
|
|||
transition: transform 140ms ease, filter 140ms ease;
|
||||
}
|
||||
|
||||
.overlay-item:hover,
|
||||
.overlay-item:focus-visible {
|
||||
.viewport:not(.inspect-active) .overlay-item:hover,
|
||||
.viewport:not(.inspect-active) .overlay-item:focus-visible {
|
||||
transform: scale(1.05);
|
||||
filter: drop-shadow(0 10px 18px rgba(0, 0, 0, 0.34));
|
||||
}
|
||||
|
||||
.overlay-item--helper {
|
||||
outline: 1px solid var(--overlay-helper-stroke);
|
||||
outline-offset: 1px;
|
||||
background: var(--overlay-helper-fill);
|
||||
.viewport.inspect-active .overlay-item {
|
||||
pointer-events: none;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.overlay-sprite {
|
||||
|
|
@ -365,6 +364,15 @@ select:disabled,
|
|||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.inspect-highlight {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
border: 2px solid rgba(255, 229, 107, 0.95);
|
||||
background: rgba(255, 229, 107, 0.08);
|
||||
box-shadow: 0 0 0 1px rgba(7, 12, 18, 0.82), 0 0 18px rgba(255, 229, 107, 0.28);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
|
|
@ -379,6 +387,14 @@ select:disabled,
|
|||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.viewport.inspect-active {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.viewport.inspect-active.is-dragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.viewport-hint {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ const mapForm = document.querySelector("#map-form");
|
|||
const mapSelect = document.querySelector("#map-select");
|
||||
const includeEditorCheckbox = document.querySelector("#include-editor");
|
||||
const includeRoofsCheckbox = document.querySelector("#include-roofs");
|
||||
const inspectShapesCheckbox = document.querySelector("#inspect-shapes");
|
||||
const downloadButton = document.querySelector("#download-button");
|
||||
const spinner = document.querySelector("#spinner");
|
||||
const progressWrap = document.querySelector("#progress-wrap");
|
||||
|
|
@ -9,7 +10,9 @@ const progressFill = document.querySelector("#progress-fill");
|
|||
const statusBox = document.querySelector("#status");
|
||||
const metaBox = document.querySelector("#meta");
|
||||
const viewport = document.querySelector("#viewport");
|
||||
const viewportHint = document.querySelector("#viewport-hint");
|
||||
const scene = document.querySelector("#scene");
|
||||
const inspectHighlight = document.querySelector("#inspect-highlight");
|
||||
const overlayTooltip = document.querySelector("#overlay-tooltip");
|
||||
const emptyState = document.querySelector("#empty-state");
|
||||
const zoomLabel = document.querySelector("#zoom-label");
|
||||
|
|
@ -31,7 +34,8 @@ const state = {
|
|||
buildToken: 0,
|
||||
drag: null,
|
||||
pointers: new Map(),
|
||||
pinch: null
|
||||
pinch: null,
|
||||
inspectTargetId: null
|
||||
};
|
||||
|
||||
const ZOOM_FACTOR = 1.2;
|
||||
|
|
@ -63,6 +67,18 @@ function setStatus(message) {
|
|||
statusBox.textContent = message;
|
||||
}
|
||||
|
||||
function setInspectMode(active) {
|
||||
viewport.classList.toggle("inspect-active", active);
|
||||
viewportHint.textContent = active
|
||||
? "Inspect mode: move the cursor to identify shapes. Drag still pans."
|
||||
: "Drag to pan. Scroll or pinch to zoom.";
|
||||
if (!active) {
|
||||
state.inspectTargetId = null;
|
||||
hideInspectHighlight();
|
||||
hideOverlayTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replaceAll("&", "&")
|
||||
|
|
@ -273,19 +289,20 @@ function positionOverlayTooltip(clientX, clientY) {
|
|||
overlayTooltip.style.top = `${top}px`;
|
||||
}
|
||||
|
||||
function renderOverlayTooltip(overlay) {
|
||||
const notes = overlay.notes.length ? overlay.notes.map((note) => `<li>${escapeHtml(note)}</li>`).join("") : "";
|
||||
function renderShapeTooltip(shape) {
|
||||
const notes = shape.notes.length ? shape.notes.map((note) => `<li>${escapeHtml(note)}</li>`).join("") : "";
|
||||
return `
|
||||
<div class="tooltip-eyebrow">${escapeHtml(overlay.label)}</div>
|
||||
<div class="tooltip-title">Shape ${overlay.shape} frame ${overlay.frame}</div>
|
||||
<div class="tooltip-eyebrow">${escapeHtml(shape.label)}</div>
|
||||
<div class="tooltip-title">Shape ${shape.shape} frame ${shape.frame}</div>
|
||||
<dl class="tooltip-grid">
|
||||
<dt>Family</dt><dd>${overlay.family}</dd>
|
||||
<dt>World</dt><dd>${overlay.world.x}, ${overlay.world.y}, ${overlay.world.z}</dd>
|
||||
<dt>Source</dt><dd>${escapeHtml(overlay.source)}</dd>
|
||||
<dt>Flags</dt><dd>${escapeHtml(overlay.flags.hex)}</dd>
|
||||
<dt>NPC</dt><dd>${overlay.npcNum || "-"}</dd>
|
||||
<dt>Map</dt><dd>${overlay.mapNum || "-"}</dd>
|
||||
<dt>Quality</dt><dd>${overlay.quality || "-"}</dd>
|
||||
<dt>Layer</dt><dd>${escapeHtml(shape.layer)}</dd>
|
||||
<dt>Family</dt><dd>${shape.family}</dd>
|
||||
<dt>World</dt><dd>${shape.world.x}, ${shape.world.y}, ${shape.world.z}</dd>
|
||||
<dt>Source</dt><dd>${escapeHtml(shape.source)}</dd>
|
||||
<dt>Flags</dt><dd>${escapeHtml(shape.flags.hex)}</dd>
|
||||
<dt>NPC</dt><dd>${shape.npcNum || "-"}</dd>
|
||||
<dt>Map</dt><dd>${shape.mapNum || "-"}</dd>
|
||||
<dt>Quality</dt><dd>${shape.quality || "-"}</dd>
|
||||
</dl>
|
||||
${notes ? `<ul class="tooltip-notes">${notes}</ul>` : ""}
|
||||
`;
|
||||
|
|
@ -296,8 +313,20 @@ function hideOverlayTooltip() {
|
|||
overlayTooltip.innerHTML = "";
|
||||
}
|
||||
|
||||
function showOverlayTooltip(overlay, anchor) {
|
||||
overlayTooltip.innerHTML = renderOverlayTooltip(overlay);
|
||||
function hideInspectHighlight() {
|
||||
inspectHighlight.hidden = true;
|
||||
}
|
||||
|
||||
function showInspectHighlight(shape) {
|
||||
inspectHighlight.hidden = false;
|
||||
inspectHighlight.style.left = `${shape.screen.left}px`;
|
||||
inspectHighlight.style.top = `${shape.screen.top}px`;
|
||||
inspectHighlight.style.width = `${Math.max(1, shape.screen.width)}px`;
|
||||
inspectHighlight.style.height = `${Math.max(1, shape.screen.height)}px`;
|
||||
}
|
||||
|
||||
function showShapeTooltip(shape, anchor) {
|
||||
overlayTooltip.innerHTML = renderShapeTooltip(shape);
|
||||
overlayTooltip.hidden = false;
|
||||
|
||||
if (anchor instanceof Event) {
|
||||
|
|
@ -332,18 +361,35 @@ function createOverlayElement(overlay) {
|
|||
item.append(sprite);
|
||||
|
||||
item.addEventListener("pointerenter", (event) => {
|
||||
showOverlayTooltip(overlay, event);
|
||||
if (inspectShapesCheckbox.checked) {
|
||||
return;
|
||||
}
|
||||
showShapeTooltip(overlay, event);
|
||||
});
|
||||
item.addEventListener("pointermove", (event) => {
|
||||
if (!overlayTooltip.hidden) {
|
||||
positionOverlayTooltip(event.clientX, event.clientY);
|
||||
if (inspectShapesCheckbox.checked || overlayTooltip.hidden) {
|
||||
return;
|
||||
}
|
||||
positionOverlayTooltip(event.clientX, event.clientY);
|
||||
});
|
||||
item.addEventListener("pointerleave", () => {
|
||||
if (inspectShapesCheckbox.checked) {
|
||||
return;
|
||||
}
|
||||
hideOverlayTooltip();
|
||||
});
|
||||
item.addEventListener("pointerleave", hideOverlayTooltip);
|
||||
item.addEventListener("focus", () => {
|
||||
showOverlayTooltip(overlay, item);
|
||||
if (inspectShapesCheckbox.checked) {
|
||||
return;
|
||||
}
|
||||
showShapeTooltip(overlay, item);
|
||||
});
|
||||
item.addEventListener("blur", () => {
|
||||
if (inspectShapesCheckbox.checked) {
|
||||
return;
|
||||
}
|
||||
hideOverlayTooltip();
|
||||
});
|
||||
item.addEventListener("blur", hideOverlayTooltip);
|
||||
|
||||
return {
|
||||
item,
|
||||
|
|
@ -351,6 +397,51 @@ function createOverlayElement(overlay) {
|
|||
};
|
||||
}
|
||||
|
||||
function clientToScenePoint(clientX, clientY) {
|
||||
const rect = viewport.getBoundingClientRect();
|
||||
return {
|
||||
x: (clientX - rect.left - state.offsetX) / state.zoom,
|
||||
y: (clientY - rect.top - state.offsetY) / state.zoom
|
||||
};
|
||||
}
|
||||
|
||||
function findInspectableAtPoint(point) {
|
||||
const items = state.current?.inspect?.items ?? [];
|
||||
for (let index = items.length - 1; index >= 0; index -= 1) {
|
||||
const item = items[index];
|
||||
if (
|
||||
point.x >= item.screen.left &&
|
||||
point.x < item.screen.right &&
|
||||
point.y >= item.screen.top &&
|
||||
point.y < item.screen.bottom
|
||||
) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function updateInspectHover(event) {
|
||||
if (!inspectShapesCheckbox.checked || !state.current) {
|
||||
return;
|
||||
}
|
||||
const target = findInspectableAtPoint(clientToScenePoint(event.clientX, event.clientY));
|
||||
if (!target) {
|
||||
state.inspectTargetId = null;
|
||||
hideInspectHighlight();
|
||||
hideOverlayTooltip();
|
||||
return;
|
||||
}
|
||||
if (state.inspectTargetId !== target.id) {
|
||||
state.inspectTargetId = target.id;
|
||||
showInspectHighlight(target);
|
||||
showShapeTooltip(target, event);
|
||||
return;
|
||||
}
|
||||
showInspectHighlight(target);
|
||||
positionOverlayTooltip(event.clientX, event.clientY);
|
||||
}
|
||||
|
||||
function createTileElement(tileX, tileY, buildContext, metadata) {
|
||||
const tileSize = metadata.tileSize;
|
||||
const tile = document.createElement("img");
|
||||
|
|
@ -550,15 +641,16 @@ async function pollBuild(jobId, selected, token, preserveView) {
|
|||
return;
|
||||
}
|
||||
|
||||
const [metadata, overlays] = await Promise.all([
|
||||
const [metadata, overlays, inspect] = await Promise.all([
|
||||
fetchJson(`/api/maps/${selected.game}/${selected.mapId}/metadata?buildId=${encodeURIComponent(jobId)}`),
|
||||
fetchJson(`/api/maps/${selected.game}/${selected.mapId}/overlays?buildId=${encodeURIComponent(jobId)}`)
|
||||
fetchJson(`/api/maps/${selected.game}/${selected.mapId}/overlays?buildId=${encodeURIComponent(jobId)}`),
|
||||
fetchJson(`/api/maps/${selected.game}/${selected.mapId}/inspect?buildId=${encodeURIComponent(jobId)}`)
|
||||
]);
|
||||
if (token !== state.buildToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextContext = { selected, jobId, metadata, overlays };
|
||||
const nextContext = { selected, jobId, metadata, overlays, inspect };
|
||||
const nextLayerBuild = await buildLayer(nextContext);
|
||||
if (token !== state.buildToken) {
|
||||
return;
|
||||
|
|
@ -640,6 +732,12 @@ mapForm.addEventListener("submit", async (event) => {
|
|||
mapSelect.addEventListener("change", scheduleAutoBuild);
|
||||
includeEditorCheckbox.addEventListener("change", scheduleAutoBuild);
|
||||
includeRoofsCheckbox.addEventListener("change", scheduleAutoBuild);
|
||||
inspectShapesCheckbox.addEventListener("change", () => {
|
||||
setInspectMode(inspectShapesCheckbox.checked);
|
||||
if (!inspectShapesCheckbox.checked) {
|
||||
hideOverlayTooltip();
|
||||
}
|
||||
});
|
||||
|
||||
downloadButton.addEventListener("click", (event) => {
|
||||
if (downloadButton.classList.contains("is-disabled")) {
|
||||
|
|
@ -684,11 +782,24 @@ viewport.addEventListener(
|
|||
{ passive: false }
|
||||
);
|
||||
|
||||
viewport.addEventListener("pointerdown", (event) => {
|
||||
if (!state.current) {
|
||||
viewport.addEventListener("pointermove", (event) => {
|
||||
if (state.drag || state.pointers.size > 0) {
|
||||
return;
|
||||
}
|
||||
if (event.target.closest(".overlay-item")) {
|
||||
updateInspectHover(event);
|
||||
});
|
||||
|
||||
viewport.addEventListener("pointerleave", () => {
|
||||
if (!inspectShapesCheckbox.checked) {
|
||||
return;
|
||||
}
|
||||
state.inspectTargetId = null;
|
||||
hideInspectHighlight();
|
||||
hideOverlayTooltip();
|
||||
});
|
||||
|
||||
viewport.addEventListener("pointerdown", (event) => {
|
||||
if (!state.current) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
|
|
@ -773,6 +884,8 @@ setDownloadState(false);
|
|||
setLoadingState(false);
|
||||
setEmptyStateVisible(true);
|
||||
hideOverlayTooltip();
|
||||
setInspectMode(false);
|
||||
hideInspectHighlight();
|
||||
loadCatalog().catch((error) => {
|
||||
setStatus(error instanceof Error ? error.message : String(error));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
<div class="toggle-grid">
|
||||
<label class="toggle"><input id="include-editor" type="checkbox" checked> Show editor-only elements</label>
|
||||
<label class="toggle"><input id="include-roofs" type="checkbox"> Show roofs</label>
|
||||
<label class="toggle"><input id="inspect-shapes" type="checkbox"> Inspect shapes under cursor</label>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
|
@ -63,6 +64,7 @@
|
|||
<div id="viewport-hint" class="viewport-hint">Drag to pan. Scroll or pinch to zoom.</div>
|
||||
<div id="scene" class="scene">
|
||||
<div id="active-layer" class="layer"></div>
|
||||
<div id="inspect-highlight" class="inspect-highlight" hidden></div>
|
||||
</div>
|
||||
<div id="overlay-tooltip" class="overlay-tooltip" hidden></div>
|
||||
<div id="empty-state" class="empty-state">Choose a detected map to build and view it.</div>
|
||||
|
|
|
|||
|
|
@ -72,6 +72,17 @@ app.get("/api/maps/:game/:mapId/overlays", (request, response) => {
|
|||
}
|
||||
});
|
||||
|
||||
app.get("/api/maps/:game/:mapId/inspect", (request, response) => {
|
||||
try {
|
||||
const buildId = String(request.query.buildId ?? "");
|
||||
const mapId = Number.parseInt(request.params.mapId, 10);
|
||||
const inspect = builds.getInspectData(buildId, request.params.game, mapId);
|
||||
response.json(inspect);
|
||||
} catch (error) {
|
||||
response.status(400).json({ error: error instanceof Error ? error.message : String(error) });
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/maps/:game/:mapId/overlays/:overlayId.webp", async (request, response) => {
|
||||
try {
|
||||
const buildId = String(request.query.buildId ?? "");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue