Merge pull request 'feat(ptica): wire real КСИТ/height/zone/density into cockpit (frontend of #1843)' (#1849) from feat/ptica-real-ksit-frontend into main
This commit is contained in:
commit
7866479653
4 changed files with 267 additions and 43 deletions
|
|
@ -51,7 +51,7 @@ import {
|
|||
DrawerActions,
|
||||
} from "@/components/site-finder/ptica/drawers/DrawerPrimitives";
|
||||
|
||||
/** КСИТ-цель (FAR) — regulation default, see caption in the 3D sandbox. */
|
||||
/** КСИТ-цель (FAR) — regulation default when НСПД max_far is absent. */
|
||||
const MASSING_MAX_FAR = 3.5;
|
||||
/** Fallback parcel area (m²) when geometry_suitability.area_ha is absent. */
|
||||
const MASSING_FALLBACK_AREA = 6800;
|
||||
|
|
@ -65,6 +65,21 @@ function massingAreaM2(analysis: ParcelAnalysis): number {
|
|||
return MASSING_FALLBACK_AREA;
|
||||
}
|
||||
|
||||
/** Real КСИТ from НСПД-зонирование (PR #1847), else the regulation default. */
|
||||
function massingMaxFar(analysis: ParcelAnalysis): number {
|
||||
const maxFar = analysis.nspd_zoning?.max_far;
|
||||
if (typeof maxFar === "number" && Number.isFinite(maxFar) && maxFar > 0) {
|
||||
return maxFar;
|
||||
}
|
||||
return MASSING_MAX_FAR;
|
||||
}
|
||||
|
||||
/** True when the КСИТ used by the massing sandbox is the real НСПД value. */
|
||||
function massingFarIsReal(analysis: ParcelAnalysis): boolean {
|
||||
const maxFar = analysis.nspd_zoning?.max_far;
|
||||
return typeof maxFar === "number" && Number.isFinite(maxFar) && maxFar > 0;
|
||||
}
|
||||
|
||||
const SRC_UPDATED = "источник: /analyze";
|
||||
|
||||
// ── shared helpers ────────────────────────────────────────────────────────────
|
||||
|
|
@ -807,22 +822,31 @@ export function OksContent() {
|
|||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export function Future3dContent({ analysis }: { analysis: ParcelAnalysis }) {
|
||||
const maxFar = massingMaxFar(analysis);
|
||||
const farReal = massingFarIsReal(analysis);
|
||||
return (
|
||||
<>
|
||||
<DrawerSection>
|
||||
<SummaryBox>
|
||||
Генеративная масса по регламенту: КСИТ-цель {MASSING_MAX_FAR} · пятно
|
||||
и высота подбираются под целевую ёмкость, GFA и КСИТ-факт считаются от
|
||||
вводных. Орбита, зум и солнце — интерактивные.
|
||||
Генеративная масса по регламенту: КСИТ-цель {maxFar.toFixed(1)} ·
|
||||
пятно и высота подбираются под целевую ёмкость, GFA и КСИТ-факт
|
||||
считаются от вводных. Орбита, зум и солнце — интерактивные.
|
||||
</SummaryBox>
|
||||
</DrawerSection>
|
||||
<DrawerSection>
|
||||
<MassingViewer
|
||||
areaM2={massingAreaM2(analysis)}
|
||||
maxFar={MASSING_MAX_FAR}
|
||||
maxFar={maxFar}
|
||||
farIsReal={farReal}
|
||||
/>
|
||||
</DrawerSection>
|
||||
<SourceLine source="Песочница массинга · КСИТ-цель регламент-дефолт (НСПД)" />
|
||||
<SourceLine
|
||||
source={
|
||||
farReal
|
||||
? "Песочница массинга · КСИТ-цель регламент НСПД (max_far)"
|
||||
: "Песочница массинга · КСИТ-цель регламент-дефолт (НСПД)"
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -840,7 +864,8 @@ export function InsolationContent({ analysis }: { analysis: ParcelAnalysis }) {
|
|||
<DrawerSection>
|
||||
<MassingViewer
|
||||
areaM2={massingAreaM2(analysis)}
|
||||
maxFar={MASSING_MAX_FAR}
|
||||
maxFar={massingMaxFar(analysis)}
|
||||
farIsReal={massingFarIsReal(analysis)}
|
||||
/>
|
||||
</DrawerSection>
|
||||
<DrawerSection heading="ЧТО ДОБАВИТСЯ В МОДУЛЕ">
|
||||
|
|
|
|||
|
|
@ -26,8 +26,10 @@ import styles from "@/app/site-finder/analysis/[cad]/ptica/ptica.module.css";
|
|||
export interface MassingSceneProps {
|
||||
/** Real parcel area in m² when available (fallback ≈ 6800). */
|
||||
areaM2?: number;
|
||||
/** КСИТ-цель (FAR target) — regulation default, not parcel-specific. */
|
||||
/** КСИТ-цель (FAR target) — real НСПД-регламент when present, else default. */
|
||||
maxFar?: number;
|
||||
/** True when `maxFar` is the real НСПД ПЗЗ value (drives the caption). */
|
||||
farIsReal?: boolean;
|
||||
}
|
||||
|
||||
// ── constants (scene units = metres) ──────────────────────────────────────────
|
||||
|
|
@ -298,6 +300,7 @@ function modelMetrics(model: MassingModel): Metrics {
|
|||
export default function MassingScene({
|
||||
areaM2,
|
||||
maxFar,
|
||||
farIsReal = false,
|
||||
}: MassingSceneProps): React.JSX.Element {
|
||||
const parcelArea =
|
||||
areaM2 && Number.isFinite(areaM2) && areaM2 > 0 ? areaM2 : DEFAULT_AREA;
|
||||
|
|
@ -705,7 +708,8 @@ export default function MassingScene({
|
|||
</div>
|
||||
|
||||
<div className={styles.massingCaption}>
|
||||
КСИТ-цель {farTarget.toFixed(1)} · регламент-дефолт, источник НСПД ·
|
||||
КСИТ-цель {farTarget.toFixed(1)} ·{" "}
|
||||
{farIsReal ? "регламент НСПД" : "регламент-дефолт, источник НСПД"} ·
|
||||
масса генеративная (этажи/секции/время — исследовательские вводные)
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import type {
|
|||
ParcelAnalysis,
|
||||
ParcelAnalysisCompetitor,
|
||||
} from "@/types/site-finder";
|
||||
import type { NspdZoning } from "@/types/nspd";
|
||||
import type { ForecastReport, ProductMixEntry } from "@/types/forecast";
|
||||
|
||||
// ── View-model ──────────────────────────────────────────────────────────────
|
||||
|
|
@ -90,6 +91,29 @@ function formatRubPerM2(n: number): string {
|
|||
return `${formatInt(n)} ₽/м²`;
|
||||
}
|
||||
|
||||
// ── НСПД-регламент (ПЗЗ) helpers ──────────────────────────────────────────────
|
||||
|
||||
/** КСИТ (max_far) as a plain RU number — e.g. "3,5". */
|
||||
function formatFar(far: number): string {
|
||||
return fmtNum(far, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Real parcel area in m² when geometry is known (area_ha × 10000); null
|
||||
* otherwise. The single point at which we turn area_ha into m² for ёмкость.
|
||||
*/
|
||||
function parcelAreaM2(a: ParcelAnalysis): number | null {
|
||||
const areaHa = a.geometry_suitability?.area_ha;
|
||||
return areaHa != null ? areaHa * 10000 : null;
|
||||
}
|
||||
|
||||
/** Human regulation zone — prefer the readable index ("Ж-2") over reg-number. */
|
||||
function regulationZone(z: NspdZoning | null | undefined): string | null {
|
||||
return z?.regulation_zone_index ?? z?.zone_code ?? null;
|
||||
}
|
||||
|
||||
const SRC_NSPD_ZONING = "источник НСПД-зонирование";
|
||||
|
||||
// ── Passport ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface PticaPassport {
|
||||
|
|
@ -296,25 +320,49 @@ function utilityLabel(subtype: string): string {
|
|||
return UTILITY_LABELS[subtype] ?? subtype;
|
||||
}
|
||||
|
||||
/** Градостроительство — mostly placeholder (НСПД-зонирование / источники). */
|
||||
/**
|
||||
* Градостроительство — REAL ПЗЗ-регламент when nspd_zoning carries it
|
||||
* (PR #1847), honest placeholder otherwise. Зона prefers the human index
|
||||
* ("Ж-2"); КСИТ / высота / плотность light up once the backend field flows.
|
||||
*/
|
||||
export function adaptUrbanCard(a: ParcelAnalysis): PticaScanCard {
|
||||
const zoneCode = a.nspd_zoning?.zone_code;
|
||||
const z = a.nspd_zoning;
|
||||
const zone = regulationZone(z);
|
||||
const maxFar = z?.max_far;
|
||||
const maxHeight = z?.max_height_m;
|
||||
const areaM2 = parcelAreaM2(a);
|
||||
|
||||
// Плотность (КСИТ) ёмкость = площадь × КСИТ (м²), real only with both inputs.
|
||||
const densityField =
|
||||
maxFar != null
|
||||
? areaM2 != null
|
||||
? {
|
||||
value: `${formatInt(areaM2 * maxFar)} м²`,
|
||||
isReal: true,
|
||||
caption: `КСИТ ${formatFar(maxFar)} · НСПД`,
|
||||
}
|
||||
: { value: formatFar(maxFar), isReal: true, caption: "КСИТ · НСПД" }
|
||||
: placeholder(SRC_NSPD_ZONING);
|
||||
|
||||
return {
|
||||
title: "Градостроительство",
|
||||
rows: [
|
||||
{
|
||||
key: "Зона",
|
||||
field: zoneCode
|
||||
? { value: zoneCode, isReal: true, caption: "НСПД-зонирование" }
|
||||
: placeholder("источник НСПД-зонирование"),
|
||||
field: zone
|
||||
? { value: zone, isReal: true, caption: "НСПД-зонирование" }
|
||||
: placeholder(SRC_NSPD_ZONING),
|
||||
},
|
||||
{
|
||||
key: "Пред. высота",
|
||||
field: placeholder("источник НСПД-зонирование"),
|
||||
field:
|
||||
maxHeight != null
|
||||
? real(`${formatInt(maxHeight)} м`)
|
||||
: placeholder(SRC_NSPD_ZONING),
|
||||
},
|
||||
{
|
||||
key: "Плотность (КСИТ)",
|
||||
field: placeholder("источник НСПД-зонирование"),
|
||||
field: densityField,
|
||||
},
|
||||
{
|
||||
key: "Пятно застройки",
|
||||
|
|
@ -592,6 +640,40 @@ export function adaptBuildabilityDrawer(
|
|||
},
|
||||
];
|
||||
|
||||
// ── Пятно / продаваемая площадь — REAL when ПЗЗ max_far flows (PR #1847) ──
|
||||
// КСИТ ёмкость (надземная GFA) = площадь × max_far. Footprint (пятно) needs
|
||||
// the building-coverage % (max_building_pct); sellable still depends on
|
||||
// МОП/parking, so it stays an estimate placeholder.
|
||||
const z = a.nspd_zoning;
|
||||
const areaM2 = parcelAreaM2(a);
|
||||
const maxFar = z?.max_far;
|
||||
const farReal = maxFar != null;
|
||||
const buildingPct = z?.max_building_pct;
|
||||
|
||||
const spotRow: DrawerKvRow =
|
||||
areaM2 != null && buildingPct != null
|
||||
? {
|
||||
k: "Пятно застройки",
|
||||
field: {
|
||||
value: `${formatInt(areaM2 * (buildingPct / 100))} м²`,
|
||||
isReal: true,
|
||||
caption: `${formatInt(buildingPct)} % площади · НСПД`,
|
||||
},
|
||||
}
|
||||
: { k: "Пятно застройки", field: notReal(SRC_NSPD_ZONING) };
|
||||
|
||||
const sellableRow: DrawerKvRow =
|
||||
farReal && areaM2 != null
|
||||
? {
|
||||
k: "Продаваемая площадь",
|
||||
field: {
|
||||
value: `≈ ${formatInt(areaM2 * maxFar)} м²`,
|
||||
isReal: true,
|
||||
caption: "оценка по КСИТ-ёмкости · без вычета МОП/паркинга",
|
||||
},
|
||||
}
|
||||
: { k: "Продаваемая площадь", field: notReal("нужен max_far (НСПД)") };
|
||||
|
||||
const geometry: DrawerKvRow[] = [
|
||||
{
|
||||
k: "Площадь",
|
||||
|
|
@ -618,8 +700,8 @@ export function adaptBuildabilityDrawer(
|
|||
? real(`${fmtNum(geom.suitability_score * 10, 1)} / 10`)
|
||||
: notReal("нет геометрии"),
|
||||
},
|
||||
{ k: "Пятно застройки", field: notReal("нужен max_far (НСПД)") },
|
||||
{ k: "Продаваемая площадь", field: notReal("нужен max_far (НСПД)") },
|
||||
spotRow,
|
||||
sellableRow,
|
||||
];
|
||||
|
||||
const penalties = (geom?.penalties ?? []).map((p) => ({
|
||||
|
|
@ -627,10 +709,13 @@ export function adaptBuildabilityDrawer(
|
|||
impact: "штраф",
|
||||
}));
|
||||
|
||||
const summary =
|
||||
"Индекс застраиваемости — предварительная оценка по гейт-вердикту, геометрии " +
|
||||
"участка и близости инженерии. Это эвристика, а не итоговый индекс: точное " +
|
||||
"значение появится после расчёта потенциала (max_far НСПД) и финмодели.";
|
||||
const summary = farReal
|
||||
? "Индекс застраиваемости — предварительная оценка по гейт-вердикту, геометрии " +
|
||||
"участка и близости инженерии. Это эвристика, а не итоговый индекс: точное " +
|
||||
"значение появится после финмодели. КСИТ-ёмкость рассчитана по регламенту НСПД."
|
||||
: "Индекс застраиваемости — предварительная оценка по гейт-вердикту, геометрии " +
|
||||
"участка и близости инженерии. Это эвристика, а не итоговый индекс: точное " +
|
||||
"значение появится после расчёта потенциала (max_far НСПД) и финмодели.";
|
||||
|
||||
return {
|
||||
gauge,
|
||||
|
|
@ -653,13 +738,34 @@ export interface PticaUrbanDrawer {
|
|||
|
||||
export function adaptUrbanDrawer(a: ParcelAnalysis): PticaUrbanDrawer {
|
||||
const zoning = a.nspd_zoning;
|
||||
const zoneCode = zoning?.zone_code
|
||||
? real(zoning.zone_code)
|
||||
: notReal("источник НСПД-зонирование");
|
||||
// Территориальная зона: prefer human index ("Ж-2"), else reg-number code.
|
||||
const zone = regulationZone(zoning);
|
||||
const zoneCode = zone ? real(zone) : notReal(SRC_NSPD_ZONING);
|
||||
const zoneName = zoning?.zone_name
|
||||
? real(zoning.zone_name)
|
||||
: notReal("источник НСПД-зонирование");
|
||||
: notReal(SRC_NSPD_ZONING);
|
||||
|
||||
const maxFar = zoning?.max_far;
|
||||
const maxHeight = zoning?.max_height_m;
|
||||
const buildingPct = zoning?.max_building_pct;
|
||||
const areaM2 = parcelAreaM2(a);
|
||||
const regSource = zoning?.regulation_source;
|
||||
|
||||
// КСИТ ёмкость (надземная GFA) = площадь × max_far.
|
||||
const densityField: PticaField =
|
||||
maxFar != null && areaM2 != null
|
||||
? {
|
||||
value: `${formatInt(areaM2 * maxFar)} м²`,
|
||||
isReal: true,
|
||||
caption: regSource
|
||||
? `КСИТ ${formatFar(maxFar)} · ${regSource}`
|
||||
: `КСИТ ${formatFar(maxFar)} · НСПД`,
|
||||
}
|
||||
: maxFar != null
|
||||
? notReal("нужна геометрия участка")
|
||||
: notReal("после расчёта потенциала");
|
||||
|
||||
const farReal = maxFar != null;
|
||||
const regulation: DrawerKvRow[] = [
|
||||
{ k: "Территориальная зона", field: zoneCode },
|
||||
{ k: "Наименование зоны", field: zoneName },
|
||||
|
|
@ -669,19 +775,44 @@ export function adaptUrbanDrawer(a: ParcelAnalysis): PticaUrbanDrawer {
|
|||
? real(a.egrn.permitted_use_text)
|
||||
: notReal("нет данных ЕГРН"),
|
||||
},
|
||||
{ k: "КСИТ (max FAR)", field: notReal("нужен max_far (НСПД)") },
|
||||
{ k: "Предельная высота", field: notReal("источник НСПД-зонирование") },
|
||||
{
|
||||
k: "КСИТ (max FAR)",
|
||||
field: farReal
|
||||
? real(formatFar(maxFar))
|
||||
: notReal("нужен max_far (НСПД)"),
|
||||
},
|
||||
{
|
||||
k: "Предельная высота",
|
||||
field:
|
||||
maxHeight != null
|
||||
? real(`${formatInt(maxHeight)} м`)
|
||||
: notReal(SRC_NSPD_ZONING),
|
||||
},
|
||||
{
|
||||
k: "Этажность (предельная)",
|
||||
field:
|
||||
zoning?.max_floors != null
|
||||
? real(`${formatInt(zoning.max_floors)} эт.`)
|
||||
: notReal(SRC_NSPD_ZONING),
|
||||
},
|
||||
{
|
||||
k: "Коэфф. застройки (пятно)",
|
||||
field: notReal("источник НСПД-зонирование"),
|
||||
field:
|
||||
buildingPct != null
|
||||
? real(`${formatInt(buildingPct)} %`)
|
||||
: notReal(SRC_NSPD_ZONING),
|
||||
},
|
||||
{ k: "Плотность (КСИТ)", field: notReal("после расчёта потенциала") },
|
||||
{ k: "Плотность (КСИТ)", field: densityField },
|
||||
];
|
||||
|
||||
const summary =
|
||||
"Градостроительный регламент участка. Территориальная зона и ВРИ берутся из " +
|
||||
"НСПД-зонирования / ЕГРН; КСИТ (max_far), предельная высота и плотность пока " +
|
||||
"недоступны в выгрузке — отмечены «—» с источником.";
|
||||
const summary = farReal
|
||||
? "Градостроительный регламент участка из НСПД-зонирования (ПЗЗ). " +
|
||||
"Территориальная зона, КСИТ (max_far), предельная высота и коэффициент " +
|
||||
"застройки — реальные регламентные значения; плотность рассчитана от " +
|
||||
"площади участка."
|
||||
: "Градостроительный регламент участка. Территориальная зона и ВРИ берутся из " +
|
||||
"НСПД-зонирования / ЕГРН; КСИТ (max_far), предельная высота и плотность пока " +
|
||||
"недоступны в выгрузке — отмечены «—» с источником.";
|
||||
|
||||
return { summary, regulation, zoneCode, zoneName };
|
||||
}
|
||||
|
|
@ -1298,21 +1429,76 @@ export function adaptPotentialDrawer(a: ParcelAnalysis): PticaPotentialDrawer {
|
|||
? real(`${areaM2FromHa(areaHa)} (${formatHa(areaHa)})`)
|
||||
: notReal("нет геометрии");
|
||||
|
||||
const z = a.nspd_zoning;
|
||||
const areaM2 = parcelAreaM2(a);
|
||||
const maxFar = z?.max_far;
|
||||
const farReal = maxFar != null;
|
||||
const buildingPct = z?.max_building_pct;
|
||||
const maxHeight = z?.max_height_m;
|
||||
const maxFloors = z?.max_floors;
|
||||
|
||||
// КСИТ ёмкость (надземная GFA) = площадь × max_far — real with both inputs.
|
||||
const capacityField: PticaField =
|
||||
farReal && areaM2 != null
|
||||
? {
|
||||
value: `${formatInt(areaM2 * maxFar)} м²`,
|
||||
isReal: true,
|
||||
caption: `КСИТ ${formatFar(maxFar)} · площадь × КСИТ`,
|
||||
}
|
||||
: notReal("нужен max_far (НСПД)");
|
||||
|
||||
// Пятно застройки = площадь × коэф. застройки (max_building_pct).
|
||||
const spotField: PticaField =
|
||||
areaM2 != null && buildingPct != null
|
||||
? {
|
||||
value: `${formatInt(areaM2 * (buildingPct / 100))} м²`,
|
||||
isReal: true,
|
||||
caption: `${formatInt(buildingPct)} % площади · НСПД`,
|
||||
}
|
||||
: notReal("нужен % застройки (НСПД)");
|
||||
|
||||
// Расчётная этажность — прямо из регламента, либо высота / 3 м (этаж).
|
||||
const floorsField: PticaField =
|
||||
maxFloors != null
|
||||
? real(`${formatInt(maxFloors)} эт.`)
|
||||
: maxHeight != null
|
||||
? {
|
||||
value: `≈ ${formatInt(maxHeight / 3)} эт.`,
|
||||
isReal: true,
|
||||
caption: `от предельной высоты ${formatInt(maxHeight)} м`,
|
||||
}
|
||||
: notReal("нужна предельная высота (НСПД)");
|
||||
|
||||
const rows: DrawerKvRow[] = [
|
||||
{ k: "Площадь участка", field: areaField },
|
||||
{ k: "КСИТ (max_far)", field: notReal("нужен max_far (НСПД)") },
|
||||
{ k: "Ёмкость по КСИТ", field: notReal("нужен max_far (НСПД)") },
|
||||
{ k: "Пятно застройки", field: notReal("нужен max_far (НСПД)") },
|
||||
{ k: "Продаваемая площадь", field: notReal("после расчёта потенциала") },
|
||||
{
|
||||
k: "Расчётная этажность",
|
||||
field: notReal("нужна предельная высота (НСПД)"),
|
||||
k: "КСИТ (max_far)",
|
||||
field: farReal
|
||||
? real(formatFar(maxFar))
|
||||
: notReal("нужен max_far (НСПД)"),
|
||||
},
|
||||
{ k: "Ёмкость по КСИТ", field: capacityField },
|
||||
{ k: "Пятно застройки", field: spotField },
|
||||
{
|
||||
k: "Продаваемая площадь",
|
||||
field:
|
||||
farReal && areaM2 != null
|
||||
? {
|
||||
value: `≈ ${formatInt(areaM2 * maxFar)} м²`,
|
||||
isReal: true,
|
||||
caption: "оценка по КСИТ-ёмкости · без вычета МОП/паркинга",
|
||||
}
|
||||
: notReal("после расчёта потенциала"),
|
||||
},
|
||||
{ k: "Расчётная этажность", field: floorsField },
|
||||
];
|
||||
|
||||
const summary =
|
||||
"Расчёт ёмкости (плотность, пятно, продаваемая площадь) требует предельного " +
|
||||
"КСИТ (max_far) и высоты из НСПД-зонирования. Доступна только площадь участка.";
|
||||
const summary = farReal
|
||||
? "Расчёт ёмкости по регламенту НСПД-зонирования: ёмкость по КСИТ = площадь × " +
|
||||
"max_far, пятно — по коэффициенту застройки. Продаваемая площадь — оценка " +
|
||||
"(без вычета МОП и паркинга), уточняется финмоделью."
|
||||
: "Расчёт ёмкости (плотность, пятно, продаваемая площадь) требует предельного " +
|
||||
"КСИТ (max_far) и высоты из НСПД-зонирования. Доступна только площадь участка.";
|
||||
|
||||
return { summary, rows };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,15 @@ export interface NspdZoning {
|
|||
zone_name: string | null;
|
||||
source?: string;
|
||||
raw_props?: Record<string, unknown>;
|
||||
// Real ПЗЗ regulation (PR #1847) — optional + nullable so the cockpit stays
|
||||
// backward-compatible until #1847 deploys, then auto-lights-up.
|
||||
max_far?: number | null; // КСИТ (предельный коэф. плотности застройки)
|
||||
max_height_m?: number | null; // предельная высота, м
|
||||
max_floors?: number | null; // предельная этажность
|
||||
max_building_pct?: number | null; // коэф. застройки (процент пятна)
|
||||
min_parcel_area_m2?: number | null; // мин. площадь участка, м²
|
||||
regulation_zone_index?: string | null; // human zone index, e.g. "Ж-2"
|
||||
regulation_source?: string | null; // источник регламента
|
||||
}
|
||||
|
||||
export interface NspdZouitOverlap {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue