Worldshaper/src/worldshaperHeightViewer/main.ts

706 lines
24 KiB
TypeScript

/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck
import type { WorldshaperStudioBootstrap } from "../worldshaperStudio/bootstrap";
import {
loadWorldshaperStudioBootstrap,
loadStandaloneWorldshaperBootstrap,
} from "../worldshaperStudio/bootstrap";
import { persistWorldshaperHeightViewerBounds } from "../worldshaperStudio/windowing";
import { createDebouncedCallback } from "../worldshaperStudio/debounce";
const VIEWER_STYLE_ID = "worldshaper-height-viewer-styles";
function ensureStyles(): void {
let styleEl = document.getElementById(VIEWER_STYLE_ID) as HTMLStyleElement | null;
if (!styleEl) {
styleEl = document.createElement("style");
styleEl.id = VIEWER_STYLE_ID;
document.head.appendChild(styleEl);
}
styleEl.textContent = `
:root { color-scheme: dark; }
* { box-sizing: border-box; }
html, body {
margin: 0;
width: 100%;
height: 100%;
background: #07111f;
color: #d8e8ff;
font-family: Segoe UI, Arial, sans-serif;
}
.viewer-shell {
display: grid;
grid-template-rows: 52px 1fr;
width: 100vw;
height: 100vh;
}
.viewer-bar {
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
align-items: center;
gap: 12px;
padding: 8px 12px;
border-bottom: 1px solid #274472;
background: linear-gradient(180deg, #152645 0%, #0d1b33 100%);
}
.viewer-title {
min-width: 0;
display: grid;
gap: 2px;
}
.viewer-title strong {
font-size: 14px;
color: #eef6ff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.viewer-title span {
font-size: 11px;
color: #9fb8e5;
}
.viewer-controls {
display: inline-flex;
align-items: center;
gap: 8px;
}
.viewer-btn {
height: 34px;
padding: 0 12px;
border: 1px solid #3c5e95;
border-radius: 8px;
background: #1a345e;
color: #d6e7ff;
font-size: 12px;
font-weight: 700;
cursor: pointer;
}
.viewer-btn:hover {
background: #214679;
}
.viewer-btn:disabled {
opacity: 0.55;
cursor: not-allowed;
background: #132643;
}
.viewer-height-pill {
min-width: 96px;
height: 34px;
padding: 0 12px;
border: 1px solid #3c5e95;
border-radius: 999px;
background: #10284b;
color: #d6e7ff;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
white-space: nowrap;
}
.viewer-hint {
font-size: 11px;
color: #9fb8e5;
white-space: nowrap;
}
.viewer-viewport {
position: relative;
min-width: 0;
min-height: 0;
overflow: auto;
background:
linear-gradient(180deg, rgba(20, 38, 69, 0.16), rgba(7, 17, 31, 0.04)),
#07111f;
}
.viewer-viewport-layer {
position: sticky;
top: 0;
left: 0;
width: 100%;
height: 0;
overflow: visible;
z-index: 1;
pointer-events: none;
}
.viewer-viewport-layer canvas {
display: block;
pointer-events: none;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
.viewer-viewport-spacer {
position: relative;
z-index: 0;
pointer-events: none;
}
`;
}
function renderMessage(title: string, message: string): void {
document.body.innerHTML = "";
document.body.style.margin = "0";
document.body.style.minHeight = "100vh";
document.body.style.display = "grid";
document.body.style.placeItems = "center";
document.body.style.background = "#07111f";
document.body.style.color = "#d8e8ff";
document.body.style.fontFamily = "Segoe UI, Arial, sans-serif";
const panel = document.createElement("div");
panel.style.maxWidth = "460px";
panel.style.padding = "24px";
panel.style.border = "1px solid #2e426c";
panel.style.borderRadius = "10px";
panel.style.background = "#0e1a33";
panel.style.boxShadow = "0 12px 36px rgba(3, 8, 18, 0.45)";
const heading = document.createElement("h1");
heading.textContent = title;
heading.style.margin = "0 0 8px";
heading.style.fontSize = "18px";
const text = document.createElement("p");
text.textContent = message;
text.style.margin = "0";
text.style.fontSize = "14px";
text.style.lineHeight = "1.5";
panel.appendChild(heading);
panel.appendChild(text);
document.body.appendChild(panel);
}
function renderLoading(message: string): void {
renderMessage("Loading Worldshaper Height Viewer", message);
}
function renderError(message: string): void {
renderMessage("Worldshaper Height Viewer unavailable", message);
}
function cloneValue<T>(value: T): T {
if (typeof structuredClone === "function") {
return structuredClone(value);
}
return value == null ? value : JSON.parse(JSON.stringify(value));
}
function buildViewerMarkup(): string {
return `
<div class="viewer-shell">
<div class="viewer-bar">
<div class="viewer-title">
<strong id="viewerTitle">Worldshaper Height Viewer</strong>
<span id="viewerMeta">Previewing current world snapshot.</span>
</div>
<div class="viewer-controls">
<button class="viewer-btn" id="heightDownBtn" type="button">Height -</button>
<div class="viewer-height-pill" id="heightLabel">Height 0</div>
<button class="viewer-btn" id="heightUpBtn" type="button">Height +</button>
</div>
<div class="viewer-hint">Use Up / Down arrows to change height.</div>
</div>
<div class="viewer-viewport" id="viewerViewport">
<div class="viewer-viewport-layer">
<canvas id="viewerCanvas"></canvas>
</div>
<div class="viewer-viewport-spacer" id="viewerViewportSpacer" aria-hidden="true"></div>
</div>
</div>
`;
}
function startViewer(bootstrap: WorldshaperStudioBootstrap): void {
document.body.removeAttribute("style");
document.body.innerHTML = buildViewerMarkup();
document.title = "Worldshaper Height Viewer - " + (bootstrap.mapName || bootstrap.mapId || "Untitled");
const titleEl = document.getElementById("viewerTitle");
const metaEl = document.getElementById("viewerMeta");
const heightLabelEl = document.getElementById("heightLabel");
const heightDownBtn = document.getElementById("heightDownBtn") as HTMLButtonElement | null;
const heightUpBtn = document.getElementById("heightUpBtn") as HTMLButtonElement | null;
const viewportEl = document.getElementById("viewerViewport") as HTMLDivElement | null;
const viewportSpacerEl = document.getElementById("viewerViewportSpacer") as HTMLDivElement | null;
const canvasEl = document.getElementById("viewerCanvas") as HTMLCanvasElement | null;
const ctx = canvasEl?.getContext("2d") || null;
if (!viewportEl || !viewportSpacerEl || !canvasEl || !ctx) {
renderError("The height viewer could not initialize its canvas.");
return;
}
const mapWidth = Math.max(1, Number(bootstrap.width) || 1);
const mapHeight = Math.max(1, Number(bootstrap.height) || 1);
const tileSize = Math.max(8, Number(bootstrap.tileSize) || 32);
const worldPixelWidth = mapWidth * tileSize;
const worldPixelHeight = mapHeight * tileSize;
const backgroundColor = /^#[0-9a-fA-F]{6}$/.test(String(bootstrap.backgroundColor || "").trim())
? String(bootstrap.backgroundColor).trim().toUpperCase()
: "#060A14";
const layers = Array.isArray(bootstrap.roomLayers)
? cloneValue(bootstrap.roomLayers).map((layer) => ({
layer: Number(layer.layer) || 0,
name: typeof layer.name === "string" ? layer.name.trim() : "",
rows: Array.isArray(layer.rows) ? layer.rows.map((row) => String(row || "")) : [],
})).sort((a, b) => a.layer - b.layer)
: [];
const heightLayers = Array.isArray(bootstrap.heightLayers)
? cloneValue(bootstrap.heightLayers).map((entry, index) => {
const rows = Array.isArray(entry?.rows) ? entry.rows.map((row) => String(row || "").replace(/\./g, " ")) : [];
const width = rows.reduce((max, row) => Math.max(max, row.length), 0);
const height = rows.length;
const x = Math.max(0, Number(entry?.x) || 0);
const y = Math.max(0, Number(entry?.y) || 0);
return {
id: String(entry?.id || ("height_" + String(index + 1))).trim() || ("height_" + String(index + 1)),
name: typeof entry?.name === "string" ? entry.name.trim() : "",
z: Math.max(1, Math.floor(Number(entry?.z) || 1)),
x,
y,
rows,
width,
height,
pixelX: x * tileSize,
pixelY: y * tileSize,
pixelWidth: width * tileSize,
pixelHeight: height * tileSize,
};
})
: [];
const heightLayersByZ = new Map<number, Array<Record<string, unknown>>>();
heightLayers.forEach((entry) => {
const z = Math.max(1, Number(entry.z) || 1);
if (!heightLayersByZ.has(z)) {
heightLayersByZ.set(z, []);
}
heightLayersByZ.get(z)?.push(entry);
});
const maxHeight = heightLayers.reduce((max, entry) => Math.max(max, Math.max(1, Number(entry?.z) || 1)), 0);
const tileCatalogBySymbol: Record<string, {
symbol: string;
color: string;
dataUrl: string | null;
}> = {};
Object.entries(bootstrap.tileColors || {}).forEach(([symbol, color]) => {
tileCatalogBySymbol[symbol] = {
symbol,
color: String(color || "#7AA7FF"),
dataUrl: null,
};
});
Object.values(bootstrap.tileCatalogById || {}).forEach((entry) => {
const symbol = String(entry?.symbol || "").charAt(0);
if (!symbol) {
return;
}
tileCatalogBySymbol[symbol] = {
symbol,
color: String(entry?.color || tileCatalogBySymbol[symbol]?.color || "#7AA7FF"),
dataUrl: entry?.dataUrl || null,
};
});
const backgroundTileId = String(bootstrap.backgroundTileId || "").trim();
const backgroundSymbol = backgroundTileId
? String(bootstrap.tileCatalogById?.[backgroundTileId]?.symbol || ".").charAt(0) || "."
: "";
const imageCache: Record<string, HTMLImageElement> = {};
const patchSurfaceCache = new Map<string, HTMLCanvasElement>();
const baseSurfaceCanvas = document.createElement("canvas");
const baseSurfaceCtx = baseSurfaceCanvas.getContext("2d");
const baseSurfaceState = {
dirty: true,
width: 0,
height: 0,
viewportLeft: -1,
viewportTop: -1,
tileSize: 0,
};
const state = {
currentHeight: 0,
pendingDrawFrame: 0,
};
const heightBlurStep = Math.max(0, Math.min(1, Number(bootstrap.heightBlurStep ?? bootstrap.heightDetailStep) || 0.1));
function clampViewerHeight(value: unknown): number {
return Math.max(0, Math.min(maxHeight, Number(value) || 0));
}
function getHeightBlurStrength(height: number): number {
const normalizedHeight = Math.max(0, Number(height) || 0);
return Math.min(8, normalizedHeight * heightBlurStep * (tileSize / 4));
}
function syncViewportDimensions(): void {
const nextCanvasWidth = Math.max(1, Math.ceil(Number(viewportEl.clientWidth) || 0));
const nextCanvasHeight = Math.max(1, Math.ceil(Number(viewportEl.clientHeight) || 0));
if (canvasEl.width !== nextCanvasWidth || canvasEl.height !== nextCanvasHeight) {
canvasEl.width = nextCanvasWidth;
canvasEl.height = nextCanvasHeight;
}
canvasEl.style.width = nextCanvasWidth + "px";
canvasEl.style.height = nextCanvasHeight + "px";
viewportSpacerEl.style.width = Math.max(nextCanvasWidth, worldPixelWidth) + "px";
viewportSpacerEl.style.height = Math.max(nextCanvasHeight, worldPixelHeight) + "px";
}
function getViewportRenderRect() {
const viewportWidth = Math.max(1, Math.ceil(Number(viewportEl.clientWidth) || 0));
const viewportHeight = Math.max(1, Math.ceil(Number(viewportEl.clientHeight) || 0));
const left = Math.max(0, Math.min(worldPixelWidth, Math.floor(Number(viewportEl.scrollLeft) || 0)));
const top = Math.max(0, Math.min(worldPixelHeight, Math.floor(Number(viewportEl.scrollTop) || 0)));
const right = Math.max(left + 1, Math.min(worldPixelWidth, Math.ceil((Number(viewportEl.scrollLeft) || 0) + viewportWidth)));
const bottom = Math.max(top + 1, Math.min(worldPixelHeight, Math.ceil((Number(viewportEl.scrollTop) || 0) + viewportHeight)));
return {
left,
top,
right,
bottom,
width: right - left,
height: bottom - top,
};
}
function rectIntersects(rect, x, y, width, height): boolean {
return x + width > rect.left && x < rect.right && y + height > rect.top && y < rect.bottom;
}
function updateHeightLabel(): void {
if (heightLabelEl) {
heightLabelEl.textContent = "Height " + state.currentHeight;
}
if (heightDownBtn) {
heightDownBtn.disabled = state.currentHeight <= 0;
}
if (heightUpBtn) {
heightUpBtn.disabled = state.currentHeight >= maxHeight;
}
}
function invalidateBaseSurface(): void {
baseSurfaceState.dirty = true;
baseSurfaceState.viewportLeft = -1;
baseSurfaceState.viewportTop = -1;
baseSurfaceState.tileSize = 0;
}
function drawSymbolAtPixel(targetCtx: CanvasRenderingContext2D, symbol: string, drawX: number, drawY: number): void {
const tileEntry = tileCatalogBySymbol[symbol] || tileCatalogBySymbol["."] || { color: "#7AA7FF", dataUrl: null };
const img = getTileImage(symbol);
if (img && img.complete && img.naturalWidth > 0) {
targetCtx.drawImage(img, drawX, drawY, tileSize, tileSize);
return;
}
targetCtx.fillStyle = tileEntry.color || "#7AA7FF";
targetCtx.fillRect(drawX, drawY, tileSize, tileSize);
}
function getTileImage(symbol: string): HTMLImageElement | null {
const tileEntry = tileCatalogBySymbol[symbol];
if (!tileEntry?.dataUrl) {
return null;
}
if (imageCache[symbol]) {
return imageCache[symbol];
}
const img = new Image();
img.src = tileEntry.dataUrl;
img.onload = () => {
patchSurfaceCache.clear();
invalidateBaseSurface();
draw();
};
imageCache[symbol] = img;
return img;
}
function drawVisibleBaseTiles(targetCtx: CanvasRenderingContext2D, viewportRect): void {
const startTileX = Math.max(0, Math.floor(viewportRect.left / tileSize));
const endTileX = Math.min(mapWidth - 1, Math.ceil(viewportRect.right / tileSize));
const startTileY = Math.max(0, Math.floor(viewportRect.top / tileSize));
const endTileY = Math.min(mapHeight - 1, Math.ceil(viewportRect.bottom / tileSize));
targetCtx.save();
targetCtx.setTransform(1, 0, 0, 1, -viewportRect.left, -viewportRect.top);
targetCtx.imageSmoothingEnabled = false;
layers.forEach((layer) => {
const isBackgroundLayer = (Number(layer.layer) || 0) === 0;
const fillChar = isBackgroundLayer ? "." : " ";
const rows = Array.isArray(layer.rows) ? layer.rows : [];
for (let tileY = startTileY; tileY <= endTileY; tileY += 1) {
const row = String(rows[tileY] || "");
for (let tileX = startTileX; tileX <= endTileX; tileX += 1) {
let ch = row.charAt(tileX) || fillChar;
if (isBackgroundLayer && ch === " ") {
continue;
}
if (isBackgroundLayer && ch === "." && backgroundSymbol) {
ch = backgroundSymbol;
}
if (!isBackgroundLayer && ch === " ") {
continue;
}
if (ch === ".") {
continue;
}
drawSymbolAtPixel(targetCtx, ch, tileX * tileSize, tileY * tileSize);
}
}
});
targetCtx.restore();
}
function baseSurfaceNeedsRefresh(viewportRect, canvasWidth: number, canvasHeight: number): boolean {
if (baseSurfaceState.dirty || baseSurfaceState.width !== canvasWidth || baseSurfaceState.height !== canvasHeight) {
return true;
}
if (baseSurfaceState.viewportLeft !== viewportRect.left || baseSurfaceState.viewportTop !== viewportRect.top) {
return true;
}
if (baseSurfaceState.tileSize !== tileSize) {
return true;
}
return false;
}
function refreshBaseSurface(viewportRect, canvasWidth: number, canvasHeight: number): void {
if (!baseSurfaceCtx) {
return;
}
if (baseSurfaceCanvas.width !== canvasWidth || baseSurfaceCanvas.height !== canvasHeight) {
baseSurfaceCanvas.width = canvasWidth;
baseSurfaceCanvas.height = canvasHeight;
}
baseSurfaceCtx.setTransform(1, 0, 0, 1, 0, 0);
baseSurfaceCtx.clearRect(0, 0, canvasWidth, canvasHeight);
drawVisibleBaseTiles(baseSurfaceCtx, viewportRect);
baseSurfaceState.width = canvasWidth;
baseSurfaceState.height = canvasHeight;
baseSurfaceState.viewportLeft = viewportRect.left;
baseSurfaceState.viewportTop = viewportRect.top;
baseSurfaceState.tileSize = tileSize;
baseSurfaceState.dirty = false;
}
function getOrBuildPatchSurface(entry): HTMLCanvasElement | null {
if (!entry || entry.pixelWidth <= 0 || entry.pixelHeight <= 0) {
return null;
}
const entryId = String(entry.id || "").trim();
if (!entryId) {
return null;
}
const cached = patchSurfaceCache.get(entryId);
if (cached) {
return cached;
}
const surface = document.createElement("canvas");
surface.width = Math.max(1, Number(entry.pixelWidth) || 1);
surface.height = Math.max(1, Number(entry.pixelHeight) || 1);
const surfaceCtx = surface.getContext("2d");
if (!surfaceCtx) {
return null;
}
surfaceCtx.imageSmoothingEnabled = false;
const rows = Array.isArray(entry.rows) ? entry.rows : [];
rows.forEach((rawRow, localY) => {
const row = String(rawRow || "");
for (let localX = 0; localX < row.length; localX += 1) {
const symbol = String(row.charAt(localX) || " ").charAt(0) || " ";
if (symbol === " " || symbol === ".") {
continue;
}
drawSymbolAtPixel(surfaceCtx, symbol, localX * tileSize, localY * tileSize);
}
});
patchSurfaceCache.set(entryId, surface);
return surface;
}
function drawVisibleHeightPatches(viewportRect): void {
const visibleEntries = heightLayersByZ.get(state.currentHeight) || [];
if (visibleEntries.length <= 0) {
return;
}
ctx.save();
ctx.imageSmoothingEnabled = false;
visibleEntries.forEach((entry) => {
const patchPixelX = Math.max(0, Number(entry.pixelX) || 0);
const patchPixelY = Math.max(0, Number(entry.pixelY) || 0);
const patchPixelWidth = Math.max(0, Number(entry.pixelWidth) || 0);
const patchPixelHeight = Math.max(0, Number(entry.pixelHeight) || 0);
if (patchPixelWidth <= 0 || patchPixelHeight <= 0) {
return;
}
if (!rectIntersects(viewportRect, patchPixelX, patchPixelY, patchPixelWidth, patchPixelHeight)) {
return;
}
const surface = getOrBuildPatchSurface(entry);
if (!surface) {
return;
}
const cropLeft = Math.max(viewportRect.left, patchPixelX);
const cropTop = Math.max(viewportRect.top, patchPixelY);
const cropRight = Math.min(viewportRect.right, patchPixelX + patchPixelWidth);
const cropBottom = Math.min(viewportRect.bottom, patchPixelY + patchPixelHeight);
const sourceX = cropLeft - patchPixelX;
const sourceY = cropTop - patchPixelY;
const drawWidth = cropRight - cropLeft;
const drawHeight = cropBottom - cropTop;
if (drawWidth <= 0 || drawHeight <= 0) {
return;
}
ctx.drawImage(
surface,
sourceX,
sourceY,
drawWidth,
drawHeight,
cropLeft - viewportRect.left,
cropTop - viewportRect.top,
drawWidth,
drawHeight,
);
});
ctx.restore();
}
function performDraw(): void {
syncViewportDimensions();
const canvasWidth = Math.max(1, canvasEl.width || Math.ceil(Number(viewportEl.clientWidth) || 0));
const canvasHeight = Math.max(1, canvasEl.height || Math.ceil(Number(viewportEl.clientHeight) || 0));
const viewportRect = getViewportRenderRect();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
if (baseSurfaceNeedsRefresh(viewportRect, canvasWidth, canvasHeight)) {
refreshBaseSurface(viewportRect, canvasWidth, canvasHeight);
}
if (baseSurfaceCanvas.width > 0 && baseSurfaceCanvas.height > 0) {
ctx.save();
if (state.currentHeight > 0) {
const blurStrength = getHeightBlurStrength(state.currentHeight);
ctx.globalAlpha = Math.max(0.95, 1 - (state.currentHeight * 0.01));
ctx.filter = blurStrength > 0 ? `blur(${blurStrength}px)` : "none";
ctx.imageSmoothingEnabled = blurStrength > 0;
ctx.drawImage(baseSurfaceCanvas, 0, 0);
} else {
ctx.globalAlpha = 1;
ctx.filter = "none";
ctx.imageSmoothingEnabled = false;
ctx.drawImage(baseSurfaceCanvas, 0, 0);
}
ctx.restore();
if (state.currentHeight > 0) {
ctx.save();
ctx.fillStyle = "rgba(7, 12, 20, 0.06)";
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
ctx.restore();
}
}
drawVisibleHeightPatches(viewportRect);
updateHeightLabel();
}
function drawNow(): void {
if (state.pendingDrawFrame) {
window.cancelAnimationFrame(state.pendingDrawFrame);
state.pendingDrawFrame = 0;
}
performDraw();
}
function draw(): void {
if (state.pendingDrawFrame) {
return;
}
state.pendingDrawFrame = window.requestAnimationFrame(() => {
state.pendingDrawFrame = 0;
performDraw();
});
}
function setHeight(nextHeight: number): void {
const normalizedHeight = clampViewerHeight(nextHeight);
if (normalizedHeight === state.currentHeight) {
updateHeightLabel();
return;
}
state.currentHeight = normalizedHeight;
draw();
}
function changeHeight(delta: number): void {
setHeight(state.currentHeight + (Number(delta) || 0));
}
function handleWindowKeydown(event: KeyboardEvent): void {
if (event.defaultPrevented) {
return;
}
if (event.key === "ArrowUp") {
event.preventDefault();
changeHeight(1);
return;
}
if (event.key === "ArrowDown") {
event.preventDefault();
changeHeight(-1);
}
}
titleEl.textContent = bootstrap.mapName || bootstrap.mapId || "Worldshaper Height Viewer";
metaEl.textContent = bootstrap.mapId + " | " + mapWidth + "x" + mapHeight + " | tile " + tileSize + "px | " + heightLayers.length + " height patch" + (heightLayers.length === 1 ? "" : "es");
const persistBounds = () => {
persistWorldshaperHeightViewerBounds(window);
};
const persistBoundsDeferred = createDebouncedCallback(() => {
persistBounds();
}, 160);
heightDownBtn?.addEventListener("click", () => changeHeight(-1));
heightUpBtn?.addEventListener("click", () => changeHeight(1));
window.addEventListener("keydown", handleWindowKeydown);
window.addEventListener("resize", () => {
invalidateBaseSurface();
draw();
persistBoundsDeferred();
});
viewportEl.addEventListener("scroll", () => {
draw();
}, { passive: true });
drawNow();
window.addEventListener("beforeunload", persistBounds);
}
async function initHeightViewer(): Promise<void> {
ensureStyles();
renderLoading("Preparing world snapshot...");
const params = new URLSearchParams(window.location.search);
const token = params.get("token")?.trim() || "";
const requestedWorldId = params.get("worldId")?.trim() || params.get("mapId")?.trim() || "";
let bootstrap = loadWorldshaperStudioBootstrap(token);
if (!bootstrap) {
try {
bootstrap = await loadStandaloneWorldshaperBootstrap(requestedWorldId, window.location.origin);
} catch (error) {
renderError(String(error || "Failed to load the height viewer."));
return;
}
}
if (!bootstrap) {
renderError("No world data was available for the height viewer.");
return;
}
startViewer(bootstrap);
}
void initHeightViewer();