<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->

<template>
<div :class="$style.root">
	<div :class="[$style.screen, { [$style.zen]: false }]">
		<canvas ref="canvas" :class="$style.canvas" tabindex="-1" :style="{ visibility: controller.isReady.value ? 'visible' : 'hidden' }"></canvas>

		<Transition
			:enterActiveClass="$style.transition_fade_enterActive"
			:leaveActiveClass="$style.transition_fade_leaveActive"
			:enterFromClass="$style.transition_fade_enterFrom"
			:leaveToClass="$style.transition_fade_leaveTo"
		>
			<div v-if="!controller.isReady.value" :class="$style.loading">
				<MkProgressBar :class="$style.progressBar" :progress="controller.initializeProgress.value" :waiting="controller.initializeProgress.value === 1"/>
			</div>
		</Transition>

		<div :class="$style.overlayTop">
			<div :class="$style.topMain">
				<div :class="$style.topMenu" class="_panel _shadow">
					<template v-if="controller.isReady.value">
						<button v-tooltip="'照明切り替え'" :class="$style.topMenuButton" class="_button" @click="toggleLight"><i class="ti ti-bulb"></i></button>
						<button v-if="controller.isEditMode.value" :class="$style.topMenuButton" class="_button" style="color: var(--MI_THEME-accent)" @click="exitEditMode"><i class="ti ti-paint"></i></button>
						<button v-if="!controller.isEditMode.value" :class="$style.topMenuButton" class="_button" @click="enterEditMode"><i class="ti ti-paint"></i></button>
						<button v-if="controller.isEditMode.value" :class="$style.topMenuButton" class="_button" @click="addObject"><i class="ti ti-plus"></i></button>
						<button v-if="controller.isEditMode.value" :class="$style.topMenuButton" class="_button" @click="showSnappingMenu"><i class="ti ti-grid-4x4"></i></button>
						<button v-if="controller.isEditMode.value && !isRoomSettingsOpen" :class="$style.topMenuButton" class="_button" @click="() => isRoomSettingsOpen = true"><i class="ti ti-home-cog"></i></button>
						<button v-if="controller.isEditMode.value && isRoomSettingsOpen" :class="$style.topMenuButton" class="_button" style="color: var(--MI_THEME-accent)" @click="() => isRoomSettingsOpen = false"><i class="ti ti-home-cog"></i></button>
					</template>
					<button :class="$style.topMenuButton" class="_button" @click="showOtherMenu"><i class="ti ti-dots"></i></button>
				</div>
				<div v-if="isModified" :class="$style.modified" class="_panel _shadow">
					<span :class="$style.modifiedText">{{ i18n.ts._room.thereAreUnsavedChanges }}</span>
					<button class="_button" style="color: var(--MI_THEME-error)" @click="revert">戻す</button>
					<button class="_button" style="color: var(--MI_THEME-accent)" @click="save">保存</button>
				</div>
			</div>
		</div>

		<div :class="$style.overlayBottom">
			<div v-if="controller.isReady.value" class="_buttonsCenter" :class="$style.overlayControls">
				<template v-if="controller.isEditMode.value">
					<MkButton v-if="controller.grabbing.value" @click="cancelGrabbing"><i class="ti ti-x"></i> cancel</MkButton>
					<MkButton v-if="controller.grabbing.value && !controller.grabbing.value.forInstall" @click="endGrabbing"><i class="ti ti-check"></i> (E)</MkButton>
					<MkButton v-else-if="controller.grabbing.value && controller.grabbing.value.forInstall" @click="endGrabbing"><i class="ti ti-check"></i> (E)</MkButton>
					<MkButton v-else-if="controller.selected.value != null" @click="beginSelectedInstalledObjectGrabbing"><i class="ti ti-hand-grab"></i> (E)</MkButton>

					<MkButton v-if="controller.grabbing.value" @click="rotate"><i class="ti ti-view-360-arrow"></i> (R)</MkButton>

					<MkButton v-if="!controller.grabbing.value && controller.selected.value != null" @click="removeSelectedObject"><i class="ti ti-trash"></i> (X)</MkButton>
				</template>
				<MkButton v-if="controller.isSitting.value" @click="controller.standUp()">降りる (Q)</MkButton>
				<template v-for="interaction in interacions" :key="interaction.id">
					<MkButton inline @click="interaction.fn()">{{ interaction.label }}{{ interaction.isPrimary ? ' (E)' : '' }}</MkButton>
				</template>
			</div>

			<div v-if="useVirtualJoystick" ref="joyStickEl" :class="$style.joyStick" :style="{ '--startXPx': (joyStickStartPos?.x ?? 0) + 'px', '--startYPx': (joyStickStartPos?.y ?? 0) + 'px', '--rPx': joyStickRadiusPx + 'px' }">
				<div v-show="joyStickStartPos != null" :class="$style.joyStickRangeCircle"></div>
				<div v-show="joyStickVec.x !== 0 || joyStickVec.y !== 0" :class="$style.joyStickPuck" :style="{ '--x': joyStickVec.x, '--y': joyStickVec.y }"></div>
			</div>
		</div>

		<div v-if="controller.isReady.value && controller.isEditMode.value && controller.selected.value != null && selectedObjectDef != null && !controller.grabbing.value" :key="controller.selected.value.objectId" class="_panel" :class="$style.overlayObjectInfoPanel">
			{{ selectedObjectDef.name }}

			<XObjectCustomizeForm :schema="selectedObjectDef.options.schema" :options="controller.selected.value.objectState.options" @update="(k, v) => controller.updateObjectOption(controller.selected.value.objectId, k, v)"></XObjectCustomizeForm>
		</div>

		<div v-if="isRoomSettingsOpen && controller.isEditMode.value" class="_panel" :class="$style.overlayObjectInfoPanel">
			<XEnvOptions :controller="controller"/>
		</div>
	</div>

	<div v-if="controller.isReady.value" class="_buttons" :class="$style.controls">
		<!--<MkButton v-for="action in actions" :key="action.key" @click="action.fn">{{ action.label }}{{ hotkeyToLabel(action.hotkey) }}</MkButton>-->
	</div>
</div>
</template>

<script lang="ts" setup>
import { computed, defineAsyncComponent, markRaw, nextTick, onMounted, onUnmounted, ref, shallowRef, useTemplateRef, watch } from 'vue';
import * as BABYLON from '@babylonjs/core';
import XObjectCustomizeForm from './room.object-customize-form.vue';
import XEnvOptions from './room.env-options.vue';
import type { RoomControllerOptions } from '@/world/room/controller.js';
import { definePage } from '@/page.js';
import { i18n } from '@/i18n.js';
import { ensureSignin } from '@/i';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { RoomController } from '@/world/room/controller.js';
import { cm, getHex, getRgb, WORLD_SCALE } from '@/world/utility.js';
import { deepClone } from '@/utility/clone.js';
import { deviceKind } from '@/utility/device-kind.js';
import MkProgressBar from '@/components/MkProgressBar.vue';
import { Joystick } from '@/world/joystick.js';
import { isTouchUsing } from '@/utility/touch.js';
import { prefer } from '@/preferences.js';
import { getObjectDef } from '@/world/room/object-defs.js';
import { GRAPHICS_QUALITY } from '@/world/room/utility.js';

const canvas = useTemplateRef('canvas');

const interacions = shallowRef<{
	id: string;
	label: string;
	isPrimary: boolean;
	fn: () => void;
}[]>([]);

function resize() {
	controller.resize();
}

const isRoomSettingsOpen = ref(false);
const isModified = ref(false);

const graphicsQualityRaw = prefer.model('world.graphicsQuality');
const graphicsQualityAutoValue = computed<number>(() => deviceKind !== 'desktop' ? GRAPHICS_QUALITY.LOW : GRAPHICS_QUALITY.MEDIUM);
const graphicsQuality = computed<number>(() => graphicsQualityRaw.value ?? graphicsQualityAutoValue.value);

const fpsRaw = prefer.model('world.fps');
const fpsAutoValue = computed<number | null>(() => deviceKind !== 'desktop' ? 30 : 60);
const fps = computed<number | null>(() =>
	fpsRaw.value == null ? fpsAutoValue.value :
	fpsRaw.value === 'max' ? null :
	fpsRaw.value === '120' ? 120 :
	fpsRaw.value === '60' ? 60 :
	30);

const resolutionRaw = prefer.model('world.resolution');
const resolutionAutoValue = computed<number>(() => deviceKind !== 'desktop' ? 0.5 : 1);
const resolution = computed<number>(() => resolutionRaw.value ?? resolutionAutoValue.value);

const antialias = prefer.model('world.antialias');

const useVirtualJoystick = isTouchUsing && (deviceKind === 'smartphone' || deviceKind === 'tablet');
//const useVirtualJoystick = true;

let isWPressing = false;
let isSPressing = false;
let isAPressing = false;
let isDPressing = false;

function calcWasdVec() {
	const vec = { x: 0, y: 0 };
	if (isWPressing) vec.y -= 1;
	if (isSPressing) vec.y += 1;
	if (isAPressing) vec.x -= 1;
	if (isDPressing) vec.x += 1;
	return vec;
}

let isDashing = false;

const joyStickRadiusPx = 100;
const joyStickEl = useTemplateRef('joyStickEl');
const joyStickVec = ref({ x: 0, y: 0 });
const joyStickStartPos = ref<{ x: number; y: number } | null>(null);

const data = localStorage.getItem('roomData') != null ? JSON.parse(localStorage.getItem('roomData')!) : {
	env: {
		type: 'simple',
		options: {
			dimension: [300, 300],
			window: 'demado',
			walls: {
				n: {
					material: null,
					color: [0.9, 0.9, 0.9],
					withBeam: false,
					beamMaterial: null,
					beamColor: [0.8, 0.8, 0.8],
					withBaseboard: true,
				},
				e: {
					material: null,
					color: [0.9, 0.9, 0.9],
					withBeam: false,
					beamMaterial: null,
					beamColor: [0.8, 0.8, 0.8],
					withBaseboard: true,
				},
				s: {
					material: null,
					color: [0.9, 0.9, 0.9],
					withBeam: false,
					beamMaterial: null,
					beamColor: [0.8, 0.8, 0.8],
					withBaseboard: true,
				},
				w: {
					material: null,
					color: [0.9, 0.9, 0.9],
					withBeam: false,
					beamMaterial: null,
					beamColor: [0.8, 0.8, 0.8],
					withBaseboard: true,
				},
			},
			pillars: {
				nw: {
					material: null,
					color: [0.9, 0.9, 0.9],
					show: false,
				},
				ne: {
					material: null,
					color: [0.9, 0.9, 0.9],
					show: false,
				},
				sw: {
					material: null,
					color: [0.9, 0.9, 0.9],
					show: false,
				},
				se: {
					material: null,
					color: [0.9, 0.9, 0.9],
					show: false,
				},
			},
			flooring: {
				material: 'wood',
				color: [0.9, 0.9, 0.9],
			},
			ceiling: {
				material: null,
				color: [0.9, 0.9, 0.9],
			},
		},
	},
	roomLightColor: [1.0, 0.9, 0.8],
	installedObjects: [],
	worldScale: WORLD_SCALE,
};

// 後方互換性のため
if (data.worldScale == null) {
	data.worldScale = 1;
}
if (data.heya != null) {
	data.env = data.heya;
	delete data.heya;
}

data.env.type = 'japanese';

console.log('installedObjects:', data.installedObjects.length);

let latestData = deepClone(data);

const roomControllerOptions = computed<RoomControllerOptions>(() => ({
	graphicsQuality: graphicsQuality.value,
	fps: fps.value,
	resolution: resolution.value,
	antialias: antialias.value,
	useVirtualJoystick,
	workerMode: true,
}));

const controller = markRaw(new RoomController(data, roomControllerOptions.value));

const selectedObjectDef = computed(() => controller.selected.value == null ? null : getObjectDef(controller.selected.value.objectState.type));

onMounted(async () => {
	if (!await BABYLON.WebGPUEngine.IsSupportedAsync) {
		os.alert({
			type: 'warning',
			title: i18n.ts._room.yourDeviceNotSupported_title,
			text: i18n.ts._room.yourDeviceNotSupported_description,
		});
		return;
	}

	try {
		await controller.init(canvas.value!);
	} catch (err) {
		console.error(err);
		os.alert({
			type: 'error',
			title: i18n.ts._room.failedToInitialize,
			text: (err instanceof Error ? err.message : String(err)),
		});
		return;
	}

	canvas.value!.focus();

	window.addEventListener('resize', resize);

	watch(controller.roomState, () => {
		controller.roomState.value.worldScale = WORLD_SCALE;
		isModified.value = true;
	});

	//watch(controller.selected, (v) => {
	//	if (v == null) {
	//		interacions.value = [];
	//	} else {
	//		interacions.value = Object.entries(v.objectEntity.instance.interactions).map(([interactionId, interactionInfo]) => ({
	//			id: interactionId,
	//			label: interactionInfo.label,
	//			isPrimary: v.objectEntity.instance.primaryInteraction === interactionId,
	//			fn: interactionInfo.fn,
	//		}));
	//	}
	//});

	if (joyStickEl.value != null) {
		const joyStick = new Joystick(joyStickEl.value!, { radiusPx: joyStickRadiusPx });
		joyStick.on('start', (vector) => {
			joyStickStartPos.value = vector;
		});
		joyStick.on('end', () => {
			joyStickStartPos.value = null;
		});
		joyStick.on('updateVector', (vector) => {
			joyStickVec.value = vector;
			controller.setCameraJoystickMoveVector(vector);
		});
	}

	canvas.value!.addEventListener('keydown', (ev) => {
		if (ev.repeat) return;

		switch (ev.code) {
			case 'KeyW':
				isWPressing = true;
				controller.setCameraMoveVector(calcWasdVec(), isDashing);
				break;
			case 'KeyS':
				isSPressing = true;
				controller.setCameraMoveVector(calcWasdVec(), isDashing);
				break;
			case 'KeyA':
				isAPressing = true;
				controller.setCameraMoveVector(calcWasdVec(), isDashing);
				break;
			case 'KeyD':
				isDPressing = true;
				controller.setCameraMoveVector(calcWasdVec(), isDashing);
				break;
			case 'ShiftLeft':
			case 'ShiftRight':
				isDashing = true;
				controller.setCameraMoveVector(calcWasdVec(), isDashing);
				break;
		}
	});

	canvas.value!.addEventListener('keyup', (ev) => {
		switch (ev.code) {
			case 'KeyW':
				isWPressing = false;
				controller.setCameraMoveVector(calcWasdVec(), isDashing);
				break;
			case 'KeyS':
				isSPressing = false;
				controller.setCameraMoveVector(calcWasdVec(), isDashing);
				break;
			case 'KeyA':
				isAPressing = false;
				controller.setCameraMoveVector(calcWasdVec(), isDashing);
				break;
			case 'KeyD':
				isDPressing = false;
				controller.setCameraMoveVector(calcWasdVec(), isDashing);
				break;
			case 'ShiftLeft':
			case 'ShiftRight':
				isDashing = false;
				controller.setCameraMoveVector(calcWasdVec(), isDashing);
				break;
		}
	});

	watch([graphicsQuality, fps, resolution, antialias], () => {
		refresh();
	});
});

onUnmounted(() => {
	controller.destroy();

	window.removeEventListener('resize', resize);
});

function beginSelectedInstalledObjectGrabbing() {
	controller.beginSelectedInstalledObjectGrabbing();
	canvas.value!.focus();
}

function endGrabbing() {
	controller.endGrabbing();
	canvas.value!.focus();
}

function cancelGrabbing() {
	controller.cancelGrabbing();
	canvas.value!.focus();
}

function toggleLight() {
	controller.toggleRoomLight();
	canvas.value!.focus();
}

function showSnappingMenu(ev: PointerEvent) {
	os.popupMenu([{
		type: 'switch',
		text: i18n.ts._room.snapToGrid,
		ref: computed({
			get: () => controller.gridSnapping.value.enabled,
			set: v => controller.setGridSnapping({ ...controller.gridSnapping.value, enabled: v }),
		}),
	}, {
		type: 'radioOption',
		text: '1cm',
		active: computed(() => controller.gridSnapping.value.scale === cm(1)),
		action: () => controller.setGridSnapping({ ...controller.gridSnapping.value, scale: cm(1) }),
	}, {
		type: 'radioOption',
		text: '2cm',
		active: computed(() => controller.gridSnapping.value.scale === cm(2)),
		action: () => controller.setGridSnapping({ ...controller.gridSnapping.value, scale: cm(2) }),
	}, {
		type: 'radioOption',
		text: '4cm',
		active: computed(() => controller.gridSnapping.value.scale === cm(4)),
		action: () => controller.setGridSnapping({ ...controller.gridSnapping.value, scale: cm(4) }),
	}, {
		type: 'radioOption',
		text: '8cm',
		active: computed(() => controller.gridSnapping.value.scale === cm(8)),
		action: () => controller.setGridSnapping({ ...controller.gridSnapping.value, scale: cm(8) }),
	}, {
		type: 'radioOption',
		text: '16cm',
		active: computed(() => controller.gridSnapping.value.scale === cm(16)),
		action: () => controller.setGridSnapping({ ...controller.gridSnapping.value, scale: cm(16) }),
	}], ev.currentTarget ?? ev.target);
}

function rotate() {
	controller.changeGrabbingRotation(Math.PI / 8);
	canvas.value!.focus();
}

async function addObject(ev: PointerEvent) {
	// 重いので止める
	controller.pauseRender();
	const { dispose } = await os.popupAsyncWithDialog(import('./room.add-object-dialog.vue').then(x => x.default), {
	}, {
		ok: async (res) => {
			controller.addObject(res.id, res.options);
			canvas.value!.focus();
		},
		closed: () => {
			controller.resumeRender();
			dispose();
		},
	});
}

function removeSelectedObject() {
	controller.removeSelectedObject();
	canvas.value!.focus();
}

function enterEditMode() {
	controller.enterEditMode();
}

function exitEditMode() {
	controller.exitEditMode();
}

function save() {
	latestData = deepClone(controller.roomState.value);
	localStorage.setItem('roomData', JSON.stringify(latestData));
	isModified.value = false;
}

async function revert() {
	const { canceled } = await os.confirm({
		type: 'warning',
		title: i18n.ts.areYouSure,
		text: i18n.ts._room.revertAllChangesConfirmation,
	});
	if (canceled) return;

	await controller.reset(latestData);
	isModified.value = false;
}

async function refresh() {
	await controller.reset(null, roomControllerOptions.value);
}

function expor() {
	const dataStr = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(controller.roomState.value));
	const dlAnchorElem = window.document.createElement('a');
	dlAnchorElem.setAttribute('href', dataStr);
	dlAnchorElem.setAttribute('download', 'room.json');
	dlAnchorElem.click();
}

function impor() {
	const inputElem = window.document.createElement('input');
	inputElem.setAttribute('type', 'file');
	inputElem.setAttribute('accept', 'application/json');
	inputElem.addEventListener('change', () => {
		const file = inputElem.files?.[0];
		if (file == null) return;
		const reader = new FileReader();
		reader.onload = () => {
			try {
				localStorage.setItem('roomData', reader.result as string);
				window.location.reload();
			} catch (e) {
				alert('Failed to load room data: ' + e);
			}
		};
		reader.readAsText(file);
	});
	inputElem.click();
}

function showOtherMenu(ev: PointerEvent) {
	os.popupMenu([{
		type: 'radio',
		text: i18n.ts._room.graphicsQuality,
		caption: graphicsQualityRaw.value == null ? i18n.ts.auto : graphicsQualityRaw.value === GRAPHICS_QUALITY.HIGH ? 'High' : graphicsQualityRaw.value === GRAPHICS_QUALITY.MEDIUM ? 'Medium' : 'Low',
		options: [{
			label: `${i18n.ts.auto} (${graphicsQualityAutoValue.value === GRAPHICS_QUALITY.HIGH ? 'High' : graphicsQualityAutoValue.value === GRAPHICS_QUALITY.MEDIUM ? 'Medium' : 'Low'})`,
			value: null,
		}, { type: 'divider' }, {
			label: 'High',
			value: GRAPHICS_QUALITY.HIGH,
		}, {
			label: 'Medium',
			value: GRAPHICS_QUALITY.MEDIUM,
		}, {
			label: 'Low',
			value: GRAPHICS_QUALITY.LOW,
		}],
		ref: graphicsQualityRaw,
	}, {
		type: 'radio',
		text: i18n.ts._room.frameRate,
		caption: fpsRaw.value == null ? i18n.ts.auto : fpsRaw.value === 'max' ? 'Max' : `~${fpsRaw.value}fps`,
		options: [{
			label: `${i18n.ts.auto} (${fpsAutoValue.value}fps)`,
			value: null,
		}, { type: 'divider' }, {
			label: 'Max',
			value: 'max',
		}, {
			label: '~120fps',
			value: '120',
		}, {
			label: '~60fps',
			value: '60',
		}, {
			label: '~30fps',
			value: '30',
		}],
		ref: fpsRaw,
	}, {
		type: 'radio',
		text: i18n.ts._room.resolution,
		caption: resolutionRaw.value == null ? i18n.ts.auto : resolutionRaw.value + 'x',
		options: [{
			label: `${i18n.ts.auto} (${resolutionAutoValue.value}x)`,
			value: null,
		}, { type: 'divider' }, {
			label: '2x',
			value: 2,
		}, {
			label: '1x',
			value: 1,
		}, {
			label: '0.5x',
			value: 0.5,
		}],
		ref: resolutionRaw,
	}, {
		type: 'switch',
		text: i18n.ts._room.antialiasing,
		ref: antialias,
	}, {
		type: 'divider',
	}, {
		text: 'Export',
		action: expor,
	}, {
		text: 'Import',
		action: impor,
	}, {
		text: 'Delete',
		danger: true,
		action: () => {
			os.confirm({
				type: 'warning',
				title: i18n.ts.areYouSure,
			}).then(({ canceled }) => {
				if (canceled) return;
				localStorage.removeItem('roomData');
				window.location.reload();
			});
		},
	}, {
		type: 'divider',
	}, {
		text: 'Refresh',
		action: refresh,
	}], ev.currentTarget ?? ev.target);
}

definePage(() => ({
	title: 'Room',
	icon: 'ti ti-door',
	needWideArea: true,
}));
</script>

<style lang="scss" module>
.root {
	height: 100%;
	overflow: clip;
	background: var(--MI_THEME-bg);
}

.screen {
	position: relative;
	width: 100%;
	height: 100cqh;
	overflow: clip;
}

.canvas {
	width: 100%;
	height: 100%;
	display: block;
	touch-action: none;
	background: #000;

	&:focus {
		outline: none;
	}
}

.joyStick {
	position: relative;
	width: 50%;
	height: 100px;
	box-sizing: border-box;
	padding: 8px;
	touch-action: none;
}

.joyStick::before {
	content: '';
	display: block;
	width: 100%;
	height: 100%;
	border: solid 2px #fff;
	border-radius: 16px;
	pointer-events: none;
}

.joyStickRangeCircle {
	position: absolute;
	top: var(--startYPx);
	left: var(--startXPx);
	width: calc(var(--rPx) * 2);
	height: calc(var(--rPx) * 2);
	border: solid 2px rgba(255, 255, 255, 0.5);
	border-radius: 100%;
	transform: translate(-50%, -50%);
	pointer-events: none;
}

.joyStickPuck {
	position: absolute;
	top: calc(var(--startYPx) + (var(--y) * var(--rPx)));
	left: calc(var(--startXPx) + (var(--x) * var(--rPx)));
	width: 30px;
	height: 30px;
	background: #fff;
	border-radius: 100%;
	transform: translate(-50%, -50%);
	pointer-events: none;
}

.overlayTop {
	position: absolute;
	top: 0;
	left: 0;
	z-index: 1;
	width: 100%;
}

.overlayBottom {
	position: absolute;
	bottom: 0;
	left: 0;
	z-index: 1;
	width: 100%;
}

.topMain {
	display: flex;
	align-items: center;
	gap: 16px;
}

.topMenu {
	margin: 16px;
	display: flex;
	box-sizing: border-box;
	width: max-content;
}

.topMenuButton {
	padding: 8px;
}
.topMenuButton:first-child {
	padding-left: 16px;
}
.topMenuButton:last-child {
	padding-right: 16px;
}

.modified {
	display: flex;
	align-items: center;
	font-size: 90%;
	gap: 1em;
	padding: 8px 16px;
}

.modifiedText {
	color: var(--MI_THEME-warn);
	animation: modified-blink 2s infinite;
}

@keyframes modified-blink {
	0% { opacity: 1; }
	50% { opacity: 0.5; }
	100% { opacity: 1; }
}

.overlayControls {

}

.overlayObjectInfoPanel {
	position: absolute;
	top: 16px;
	right: 16px;
	z-index: 1;
	padding: 16px;
	box-sizing: border-box;
	width: 300px;
	max-height: 100%;
	box-sizing: border-box;
	overflow: auto;
}

.loading {
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	display: grid;
	place-items: center;
	background: var(--MI_THEME-bg);
}

.progressBar {
	width: 75%;
}

.transition_fade_enterActive,
.transition_fade_leaveActive {
	transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
.transition_fade_enterFrom,
.transition_fade_leaveTo {
	opacity: 0;
}
</style>
