Light report (Section1ParcelInfo) only rendered the dead top-level zoning.data_available=false (closed-PKK6 path) and never showed the resolved nspd_zoning, so the resolved regulation (synth #1891) was invisible to the user («мы выгружали НСПД — где они?»). - Add «Градрегламент (НСПД)»-card to Раздел 1 (right column, под ЕГРН): тер.зона, КСИТ/max_far, пред. высота, этажность, коэф. застройки, КСИТ-ёмкость/пятно (от площади ЕГРН), источник + список разрешённых ВРИ зоны. Empty-state когда регламент не определён (gap-зоны / не-ЕКБ). КСИТ-microcopy расшифровывает жаргон на первой странице. - New pure mapper adaptNspdRegulation (nspd-regulation.ts) reused by ptica-adapt so light report и /ptica остаются consistent. - coerceFloat: max_far/max_building_pct приходят с провода СТРОКАМИ ("2.4"/"80") — фикс латентного бага cockpit'а (typeof===number глушил реальный строковый КСИТ в massing/insolation). - Widen NspdZoning.max_far/max_building_pct to string|number, add main_vri; expose nspd_zoning on ParcelAnalyzeResponse. - Unit test для field-mapping + coercion + empty-state.
57 lines
2.1 KiB
TypeScript
57 lines
2.1 KiB
TypeScript
"use client";
|
||
|
||
/**
|
||
* InsolationCard — lower-grid «Инсоляционная матрица» preview card. Renders a
|
||
* LIVE compact autorotating 3D massing model (a hands-off mini version of the
|
||
* «Открыть 3D» drawer) instead of the old abstract SVG cube, fed by the REAL
|
||
* parcel area (geometry) + НСПД КСИТ (max_far). «Открыть 3D» opens the
|
||
* insolation drawer (3D massing + shadow module).
|
||
*/
|
||
|
||
import styles from "@/app/site-finder/analysis/[cad]/ptica/ptica.module.css";
|
||
import type { ParcelAnalysis } from "@/types/site-finder";
|
||
import { coerceFloat } from "@/components/site-finder/analysis/nspd-regulation";
|
||
import { parcelAreaM2 } from "@/components/site-finder/ptica/ptica-adapt";
|
||
import { MassingViewer } from "@/components/site-finder/ptica/massing/MassingViewer";
|
||
import type { DrawerKey } from "@/components/site-finder/ptica/drawers/drawer-keys";
|
||
|
||
interface Props {
|
||
analysis: ParcelAnalysis;
|
||
onOpenDrawer: (key: DrawerKey) => void;
|
||
}
|
||
|
||
export function InsolationCard({ analysis, onOpenDrawer }: Props) {
|
||
const areaM2 = parcelAreaM2(analysis) ?? undefined;
|
||
// max_far arrives as a string on the wire — coerce before passing to the 3D viewer.
|
||
const maxFar = coerceFloat(analysis.nspd_zoning?.max_far) ?? undefined;
|
||
const farIsReal = maxFar != null;
|
||
|
||
return (
|
||
<div className={styles.card}>
|
||
<div className={styles.cardHead}>
|
||
<h3 className={styles.cardTitle}>Инсоляционная матрица</h3>
|
||
<span className={styles.openTag}>превью</span>
|
||
</div>
|
||
|
||
<MassingViewer
|
||
preview
|
||
areaM2={areaM2}
|
||
maxFar={maxFar}
|
||
farIsReal={farIsReal}
|
||
/>
|
||
|
||
<div className={styles.placeholder}>
|
||
<div className={styles.placeholderSoon}>ПРЕВЬЮ</div>
|
||
<p>Тени по времени суток — в 3D-модуле массы застройки.</p>
|
||
<button
|
||
type="button"
|
||
className={styles.detailBtn}
|
||
onClick={() => onOpenDrawer("insolation")}
|
||
title="Открыть 3D-модуль"
|
||
>
|
||
Открыть 3D
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|