feat(map): adopt DC Default scene
This commit is contained in:
@@ -0,0 +1,681 @@
|
||||
import {
|
||||
Checker,
|
||||
ColorField,
|
||||
ControlRow,
|
||||
Inspector,
|
||||
RangeControl,
|
||||
StatusBadge,
|
||||
} from "@nodedc/ui-react";
|
||||
|
||||
import type {
|
||||
MapInspectorSection,
|
||||
MapViewDocument,
|
||||
} from "../../core/map/mapView";
|
||||
import type { useMapGatewayHealth } from "../../core/map/useMapGatewayHealth";
|
||||
|
||||
export const editableMapInspectorSections = new Set<MapInspectorSection>([
|
||||
"base-terrain",
|
||||
"atmosphere-light",
|
||||
"buildings",
|
||||
"grid-lod",
|
||||
"camera",
|
||||
"tile-cache",
|
||||
]);
|
||||
|
||||
export function MapSettingsInspector({
|
||||
draft,
|
||||
updateDraft,
|
||||
gatewayHealth,
|
||||
}: {
|
||||
draft: MapViewDocument;
|
||||
updateDraft: (
|
||||
update: (current: MapViewDocument) => MapViewDocument,
|
||||
) => void;
|
||||
gatewayHealth: ReturnType<typeof useMapGatewayHealth>;
|
||||
}) {
|
||||
return (
|
||||
<Inspector
|
||||
singleOpen
|
||||
sections={createInspectorSections(draft, updateDraft, gatewayHealth)}
|
||||
openSections={draft.view.inspectorOpenSections.filter(
|
||||
(section) => editableMapInspectorSections.has(section),
|
||||
)}
|
||||
activeId={draft.view.inspectorOpenSections[0]}
|
||||
onOpenSectionsChange={(sections) => {
|
||||
updateDraft((current) => {
|
||||
current.view.inspectorOpenSections = sections.filter(
|
||||
(section): section is MapInspectorSection =>
|
||||
editableMapInspectorSections.has(section as MapInspectorSection),
|
||||
).slice(0, 1);
|
||||
return current;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function createInspectorSections(
|
||||
draft: MapViewDocument,
|
||||
updateDraft: (
|
||||
update: (current: MapViewDocument) => MapViewDocument,
|
||||
) => void,
|
||||
gatewayHealth: ReturnType<typeof useMapGatewayHealth>,
|
||||
) {
|
||||
const view = draft.view;
|
||||
const settings = view.visualSettings;
|
||||
return [
|
||||
{
|
||||
id: "base-terrain",
|
||||
label: "Подложка и terrain",
|
||||
description: "provider-neutral surface",
|
||||
content: (
|
||||
<div className="mission-map__inspector-controls">
|
||||
<ControlRow label="Подложка">
|
||||
<strong>Cesium World Imagery</strong>
|
||||
</ControlRow>
|
||||
<Checker
|
||||
label="Спутниковая подложка"
|
||||
checked={view.layerVisibility.imagery}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.layerVisibility.imagery = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<Checker
|
||||
label="Terrain"
|
||||
checked={view.layerVisibility.terrain}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.layerVisibility.terrain = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Вертикальное преувеличение рельефа"
|
||||
value={settings.terrainExaggeration * 100}
|
||||
min={25}
|
||||
max={300}
|
||||
step={1}
|
||||
formatValue={(value) => `${(value / 100).toFixed(2)}×`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.terrainExaggeration = value / 100;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<Checker
|
||||
label="Монохромная поверхность"
|
||||
checked={settings.monochromeEnabled}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.visualSettings.monochromeEnabled = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<ControlRow label="Цвет монохрома">
|
||||
<ColorField
|
||||
label="Цвет монохромной поверхности"
|
||||
value={settings.monochromeColor}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.monochromeColor = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
</ControlRow>
|
||||
<RangeControl
|
||||
label="Яркость"
|
||||
value={settings.imageryBrightness}
|
||||
min={0}
|
||||
max={200}
|
||||
formatValue={(value) => `${value}%`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.imageryBrightness = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Контраст"
|
||||
value={settings.imageryContrast}
|
||||
min={0}
|
||||
max={200}
|
||||
formatValue={(value) => `${value}%`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.imageryContrast = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Насыщенность"
|
||||
value={settings.imagerySaturation}
|
||||
min={0}
|
||||
max={200}
|
||||
formatValue={(value) => `${value}%`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.imagerySaturation = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Гамма"
|
||||
value={settings.imageryGamma}
|
||||
min={0}
|
||||
max={300}
|
||||
formatValue={(value) => `${value}%`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.imageryGamma = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Оттенок"
|
||||
value={settings.imageryHue}
|
||||
min={-180}
|
||||
max={180}
|
||||
formatValue={(value) => `${value}°`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.imageryHue = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Прозрачность imagery"
|
||||
value={settings.imageryAlpha}
|
||||
min={0}
|
||||
max={100}
|
||||
formatValue={(value) => `${value}%`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.imageryAlpha = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<ControlRow label="Цвет планеты">
|
||||
<ColorField
|
||||
label="Цвет terrain без imagery"
|
||||
value={settings.globeColor}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.globeColor = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
</ControlRow>
|
||||
<ControlRow label="Фон сцены">
|
||||
<ColorField
|
||||
label="Цвет фона сцены"
|
||||
value={settings.backgroundColor}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.backgroundColor = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
</ControlRow>
|
||||
<RangeControl
|
||||
label="Высота карты"
|
||||
value={view.mapHeight}
|
||||
min={420}
|
||||
max={1080}
|
||||
step={1}
|
||||
formatValue={(value) => `${value} px`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.mapHeight = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "atmosphere-light",
|
||||
label: "Атмосфера и освещение",
|
||||
description: "scene / color correction",
|
||||
content: (
|
||||
<div className="mission-map__inspector-controls">
|
||||
<Checker
|
||||
label="Показывать атмосферу"
|
||||
checked={settings.atmosphereEnabled}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.visualSettings.atmosphereEnabled = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Атмосфера: оттенок"
|
||||
value={settings.atmosphereHue}
|
||||
min={-100}
|
||||
max={100}
|
||||
formatValue={(value) => `${value}%`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.atmosphereHue = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Атмосфера: насыщенность"
|
||||
value={settings.atmosphereSaturation}
|
||||
min={-100}
|
||||
max={100}
|
||||
formatValue={(value) => `${value}%`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.atmosphereSaturation = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Атмосфера: яркость"
|
||||
value={settings.atmosphereBrightness}
|
||||
min={-100}
|
||||
max={100}
|
||||
formatValue={(value) => `${value}%`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.atmosphereBrightness = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<Checker
|
||||
label="Туман"
|
||||
checked={settings.fogEnabled}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.visualSettings.fogEnabled = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Плотность тумана"
|
||||
value={settings.fogDensity}
|
||||
min={0}
|
||||
max={100}
|
||||
formatValue={(value) => `${(value / 10_000).toFixed(4)}`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.fogDensity = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<Checker
|
||||
label="Солнечное освещение"
|
||||
checked={settings.sunEnabled}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.visualSettings.sunEnabled = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Час солнца"
|
||||
value={settings.sunHour}
|
||||
min={0}
|
||||
max={24}
|
||||
formatValue={(value) => `${value}:00 UTC`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.sunHour = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Интенсивность света"
|
||||
value={settings.sunIntensity}
|
||||
min={0}
|
||||
max={200}
|
||||
formatValue={(value) => `${value}%`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.sunIntensity = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<Checker
|
||||
label="Тени"
|
||||
checked={settings.shadowsEnabled}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.visualSettings.shadowsEnabled = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "buildings",
|
||||
label: "3D здания",
|
||||
description: "3D Tiles / detail",
|
||||
content: (
|
||||
<div className="mission-map__inspector-controls">
|
||||
<Checker
|
||||
label="Показывать 3D здания"
|
||||
checked={view.layerVisibility.buildings}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.layerVisibility.buildings = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<ControlRow label="Цвет">
|
||||
<ColorField
|
||||
label="Цвет зданий"
|
||||
value={settings.buildingsColor}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.buildingsColor = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
</ControlRow>
|
||||
<RangeControl
|
||||
label="Прозрачность"
|
||||
value={Math.round(settings.buildingsOpacity * 100)}
|
||||
min={0}
|
||||
max={100}
|
||||
formatValue={(value) => `${value}%`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.buildingsOpacity = value / 100;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Детализация"
|
||||
value={settings.buildingsMaximumScreenSpaceError}
|
||||
min={4}
|
||||
max={32}
|
||||
formatValue={(value) => `SSE ${value}`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.buildingsMaximumScreenSpaceError = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "grid-lod",
|
||||
label: "Сетка и LOD",
|
||||
description: "first adapter control",
|
||||
content: (
|
||||
<div className="mission-map__inspector-controls">
|
||||
<Checker
|
||||
label="3D-сетка"
|
||||
checked={view.layerVisibility.grid}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.layerVisibility.grid = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<Checker
|
||||
label="LOD по высоте камеры"
|
||||
checked={settings.gridLodEnabled}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridLodEnabled = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Высота над поверхностью"
|
||||
value={settings.gridHeightMeters}
|
||||
min={0}
|
||||
max={1000}
|
||||
formatValue={(value) => `${value} м`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridHeightMeters = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="LOD 1: до высоты"
|
||||
value={settings.gridLod1MaxHeightKm}
|
||||
min={1}
|
||||
max={50}
|
||||
formatValue={(value) => `${value} км`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridLod1MaxHeightKm = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="LOD 1: шаг"
|
||||
value={settings.gridLod1StepKm}
|
||||
min={1}
|
||||
max={10}
|
||||
formatValue={(value) => `${value} км`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridLod1StepKm = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="LOD 2: до высоты"
|
||||
value={settings.gridLod2MaxHeightKm}
|
||||
min={10}
|
||||
max={200}
|
||||
formatValue={(value) => `${value} км`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridLod2MaxHeightKm = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="LOD 2: шаг"
|
||||
value={settings.gridLod2StepKm}
|
||||
min={1}
|
||||
max={25}
|
||||
formatValue={(value) => `${value} км`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridLod2StepKm = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="LOD 3: шаг"
|
||||
value={settings.gridLod3StepKm}
|
||||
min={5}
|
||||
max={100}
|
||||
formatValue={(value) => `${value} км`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridLod3StepKm = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Радиус сетки"
|
||||
value={settings.gridRadiusKm}
|
||||
min={5}
|
||||
max={150}
|
||||
formatValue={(value) => `${value} км`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridRadiusKm = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<ControlRow label="Цвет линий">
|
||||
<ColorField
|
||||
label="Цвет линий сетки"
|
||||
value={settings.gridColor}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridColor = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
</ControlRow>
|
||||
<RangeControl
|
||||
label="Толщина линий"
|
||||
value={settings.gridLineWidth}
|
||||
min={1}
|
||||
max={8}
|
||||
formatValue={(value) => `${value} px`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridLineWidth = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Прозрачность сетки"
|
||||
value={settings.gridOpacity}
|
||||
min={0}
|
||||
max={100}
|
||||
formatValue={(value) => `${value}%`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridOpacity = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<Checker
|
||||
label="Точки в пересечениях"
|
||||
checked={settings.gridDotsEnabled}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridDotsEnabled = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Размер точки"
|
||||
value={settings.gridDotsSize}
|
||||
min={2}
|
||||
max={28}
|
||||
formatValue={(value) => `${value} px`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridDotsSize = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<ControlRow label="Цвет точек">
|
||||
<ColorField
|
||||
label="Цвет точек сетки"
|
||||
value={settings.gridDotsColor}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridDotsColor = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
</ControlRow>
|
||||
<RangeControl
|
||||
label="Прозрачность точек"
|
||||
value={settings.gridDotsOpacity}
|
||||
min={0}
|
||||
max={100}
|
||||
formatValue={(value) => `${value}%`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridDotsOpacity = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "camera",
|
||||
label: "Анимация камеры",
|
||||
description: "geodesic spiral survey",
|
||||
content: (
|
||||
<div className="mission-map__inspector-controls">
|
||||
<Checker
|
||||
label="Разрешить анимацию камеры"
|
||||
checked={settings.cameraAnimationEnabled}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.visualSettings.cameraAnimationEnabled = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<ControlRow label="Сохранённая позиция">
|
||||
<StatusBadge tone={view.camera ? "success" : "neutral"}>
|
||||
{view.camera ? "Есть" : "Не задана"}
|
||||
</StatusBadge>
|
||||
</ControlRow>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "tile-cache",
|
||||
label: "TileCache",
|
||||
description: "Platform Map Gateway",
|
||||
content: (
|
||||
<div
|
||||
className="mission-map__inspector-controls"
|
||||
data-health-code={gatewayHealth.code ?? undefined}
|
||||
>
|
||||
<Checker
|
||||
label="Использовать общий TileCache"
|
||||
checked={view.cacheIntent.enabled}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.cacheIntent.enabled = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<Checker
|
||||
label="Не перезаписывать существующие тайлы"
|
||||
checked={view.cacheIntent.noOverwrite}
|
||||
disabled={!view.cacheIntent.enabled}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.cacheIntent.noOverwrite = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<CacheHealth health={gatewayHealth} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function CacheHealth({
|
||||
health,
|
||||
}: {
|
||||
health: ReturnType<typeof useMapGatewayHealth>;
|
||||
}) {
|
||||
if (health.state === "loading" || health.state === "idle") {
|
||||
return (
|
||||
<ControlRow label="Состояние">
|
||||
<StatusBadge tone="neutral">Проверяем</StatusBadge>
|
||||
</ControlRow>
|
||||
);
|
||||
}
|
||||
if (!health.snapshot) {
|
||||
return (
|
||||
<ControlRow label="Состояние">
|
||||
<StatusBadge tone="danger">Недоступен</StatusBadge>
|
||||
</ControlRow>
|
||||
);
|
||||
}
|
||||
const { cache } = health.snapshot;
|
||||
return (
|
||||
<dl className="mission-map__cache-health">
|
||||
<div>
|
||||
<dt>Хранилище</dt>
|
||||
<dd>
|
||||
<StatusBadge tone={cache.persistent ? "success" : "warning"}>
|
||||
{cache.persistent ? "Постоянное" : "Временное"}
|
||||
</StatusBadge>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Объекты</dt>
|
||||
<dd>{cache.entries.toLocaleString("ru-RU")}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Объём</dt>
|
||||
<dd>
|
||||
{formatBytes(cache.bytes)}
|
||||
{cache.maxBytes === null ? "" : ` / ${formatBytes(cache.maxBytes)}`}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Заполнение</dt>
|
||||
<dd>
|
||||
<StatusBadge
|
||||
tone={
|
||||
cache.atCapacity || health.state === "stale"
|
||||
? "warning"
|
||||
: "success"
|
||||
}
|
||||
>
|
||||
{cache.atCapacity
|
||||
? "Лимит"
|
||||
: health.state === "stale"
|
||||
? "Устарело"
|
||||
: "Норма"}
|
||||
</StatusBadge>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
);
|
||||
}
|
||||
|
||||
function formatBytes(value: number): string {
|
||||
if (value < 1024) return `${value} Б`;
|
||||
const units = ["КБ", "МБ", "ГБ", "ТБ"];
|
||||
let amount = value / 1024;
|
||||
let unit = units[0];
|
||||
for (let index = 1; index < units.length && amount >= 1024; index += 1) {
|
||||
amount /= 1024;
|
||||
unit = units[index];
|
||||
}
|
||||
return `${amount.toFixed(amount >= 10 ? 1 : 2)} ${unit}`;
|
||||
}
|
||||
Reference in New Issue
Block a user