Files
Aislo/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts
T
eomsangdonandClaude Opus 5.5 c45814f910 feat(구조물표): 323-4 형상 얹기 한 벌(TS · 파이썬 짝) — 저장분 · 초안 · 미리 뜬 횡단 같은 길(저장 전 그림 = 저장 뒤 그림) · 빈칸 경고 한 꼴(카드 칩 「종류 칸 빈칸」) · 3D 지문 = 그림 자료 통째 · C군 벽 3D 한 번 · 소단 원천 = 표 형상 berm
- 짝 `common_util_structure_attach.ts` attachShapes ↔ `B06_Section_Engine_Attach.py` attach_shapes — 열쇠 걷고 관 세트 → 벽 → 포장 → 맹암거 → 빈칸 경고(측점 계획 자리 · 「옹벽 높이」 · 「배관 관경」 · 「맹암거 암거 높이」 · 「물넘이포장 월류 높이」)
- 서버 `_read_section_detail` · 재생성 응답 = attach_saved_shapes 하나 · 화면 `withDraftShapes`(현황 = 초안 ?? 저장분 · 초안 없어지면 저장분으로 한 번) — decorate 관 세트 · withDraftPipeKinds · 맹암거 · 포장 · 벽 블록 걷음 · 구조물 초안 없을 때 미리 뜬 횡단에 포장 · 벽 · 맹암거 빠지던 것 · 반폭 넓힘 뒤 날 상세(초안 빠짐) 고침
- 파이썬 얹기 = 목록 함수(culvert_sets_from · attach_culvert_specs · wall_specs_from · attach_wall_specs · underdrains_from · attach_underdrain_specs) + 옛 파일 함수는 감쌈
- 3D 지문 = SHAPE_KEYS(`common_util_structure_shape_keys.ts`) + 조작값 통째(손 해시 걷음 · 벽 기초 · 단 · 포장 · 맹암거 빠짐) · C군 벽 = 기준 측점과 가장 가까운 측점에서만 시작 ~ 끝 스윕(`wallSweepAt`)
- 소단 = 표 형상 berm(BERM_TYPE_ID 걷음 · 화면 isBermType · 서버 catalog_row)
- 시험: test_323_4_attach_mirror(42종 + 빈칸 · 서버 = 화면 · 옛 자리 걷힘 · 벽 한 번 · 지문) · 전수 표 ★ 둘(물넘이포장 걸친 격자 100 = 서버와 같음 · STAGE 4) · 옛 도움 파일 = 측점 → 형상 차례

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
2026-10-14 17:26:53 +09:00

185 lines
8.5 KiB
TypeScript

/* =============================================================================
* B06_Section_UI_Cross_Card_Chrome.ts
* 횡단 카드의 **껍데기**(제목행·하단 정보행)만 떼어 낸 조립기 (2026-09-03 · 700줄 제한).
*
* 카드 본체(`_UI_Cross_View`)는 SVG 도면과 조작을 맡고, 도면과 무관한 머리·꼬리는 여기서
* 만든다. 여기 있는 것은 전부 **읽어서 붙이는 값**이라 카드 상태(줌·선택)를 건드리지 않는다.
* ========================================================================== */
import type { CrossSection } from "./B06_Section_Api_Fetch";
import type { RockBoundaryControl } from "./B06_Section_UI_Cross_Design";
import { buildCutSlopeControl, type CutSlopeControl } from "./B06_Section_UI_Cross_CutSlope";
import {
buildDesignControls,
buildRockBoundaryControl,
sectionModeLabel,
} from "./B06_Section_UI_Cross_Design";
import { type FillSlopeLength, fillSlopeLengths } from "./B06_Section_UI_Cross_Fit";
import {
type DesignChangeHandler,
L,
stationLabel,
structureDisplayName,
} from "./B06_Section_UI_Section_Common";
/**
* 성토사면 경사길이 표기 칸 — 성토측이 없으면 null.
*
* 값은 **구조물이 끊기 전 본래 사면**(정본 설계선 기준)이라, 기슭막이·집수정이 선 측점도
* 표준 횡단의 사면 길이를 그대로 보여 준다(2026-09-03 사용자 지시).
*/
function fillSlopeInfo(section: CrossSection): HTMLElement | null {
const lengths = fillSlopeLengths(section);
const sides = (["left", "right"] as const).filter((side) => lengths[side] !== null);
if (!sides.length) return null;
const both = sides.length > 1;
const info = document.createElement("span");
info.className = "b06-cross-card__fillslope";
info.textContent = `${L("B06_Cross_FillSlope")} ${sides
.map((side) => {
const length = lengths[side] as FillSlopeLength;
// 계산 반폭 안에서 원지반을 못 만난 사면은 거기까지만 잰 하한값이라 「≥」로 구분한다.
const value = `${length.open ? "≥" : ""}${length.lengthM.toFixed(2)}m`;
if (!both) return value;
const label = L(side === "left" ? "B06_Design_Ditch_Left" : "B06_Design_Ditch_Right");
return `${label} ${value}`;
})
.join(" · ")}`;
info.title = L("B06_Cross_FillSlope_Tip");
return info;
}
/**
* 시설을 **기본값으로 그렸거나 못 그린 까닭** `[보이는 글, 툴팁]` (2026-09-14 브레인 판정 ②③).
* 저장만 안 하는 것으로는 모자람 — 그림이 설계값처럼 보이면 기본값이 몰래 확정으로 굳던 병의 잔재.
*/
function facilityNotes(section: CrossSection): Array<[string, string]> {
const notes: Array<[string, string]> = [];
const sets: Array<[string, string[] | undefined]> = [
["BOX암거", section.box?.defaulted],
["세월교", section.ford?.defaulted],
["물넘이포장", section.ford_pavement?.defaulted],
];
for (const [name, items] of sets) {
if (!items?.length) continue;
notes.push([
`⚠ ${name} 기본값(미확정)`,
`B05 시설 칸에 안 적어 기본값으로 그림 — ${items.join(" · ")} · 적으면 그 값으로 그림`,
]);
}
// 빈칸 경고 한 꼴(323-4) — 형상 필수 칸이 빈 관 · 구조물(관경 · 벽 · 기슭막이 높이 · 맹암거 높이 · 월류
// 높이 · 형상 얹기가 측점 계획 자리에 얹음). 세월교 · 박스 흙막이 높이 빈 칸 = 본체 끝 높이(79-7)라 안 셈
for (const item of section.blank ?? [])
notes.push([
`⚠ ${item} 빈칸`,
`${item}을 안 적어 그 그림을 안 그림(수치를 지어내지 않음 · 수량도 미확정) — 시설 칸에 적으면 그림`,
]);
return notes;
}
/**
* 카드 제목행을 만들어 카드에 붙인다(설계 조작이 있으면 조작 바까지).
*
* 1행 구조(E-2/E-3): 측점 위치표기 → 지반유형 → 단면유형 pill → 구조물 → 측점정보.
*/
export function appendCardHeader(
card: HTMLElement,
section: CrossSection,
stationInterval: number,
onDesignChange?: DesignChangeHandler,
): void {
const header = document.createElement("header");
const title = document.createElement("div");
title.className = "b06-cross-card__title";
const label = document.createElement("strong");
label.textContent = stationLabel(section.chainage_m, stationInterval);
const chainage = document.createElement("span");
chainage.textContent = `${section.chainage_m.toFixed(1)}m`;
title.append(label, chainage);
// 단면유형 pill(자동 판정, 읽기 전용) — 구조물 pill과 동일 표기, 좌측 배치(E-3).
const modePill = document.createElement("span");
modePill.className = "b06-cross-card__mode";
modePill.textContent = sectionModeLabel(section.design?.section_mode);
modePill.title = L("B06_Design_Mode_Legend");
const meta = document.createElement("div");
meta.className = "b06-cross-card__meta";
// 사면이 계산 반폭 끝까지 원지반을 못 만난 측점 — 면적이 거기서 잘려 유토곡선까지
// 그 값을 쌓는다. 계산은 손대지 않고 사실만 알린다(2026-09-03 사용자 확정).
if (section.design?.slope_unclosed) {
const openSlope = document.createElement("span");
openSlope.className = "b06-cross-card__warning";
openSlope.textContent = `⚠ ${L("B06_Cross_SlopeUnclosed")}`;
openSlope.title = L("B06_Cross_SlopeUnclosed_Tip");
meta.append(openSlope);
}
for (const [text, tip] of facilityNotes(section)) {
const note = document.createElement("span");
note.className = "b06-cross-card__warning";
note.textContent = text;
note.title = tip;
meta.append(note);
}
const structureName = section.structure;
if (structureName) {
const structure = document.createElement("span");
structure.className = "b06-cross-card__structure";
// 배수관은 관종·관경(`파형강관 D1200`)이 아니라 표시 이름 하나로 적는다
// (2026-09-03 사용자 지시). 관종·관경은 툴팁에 남긴다.
structure.textContent = structureDisplayName(structureName);
structure.title = `구조물: ${structureName}`;
meta.append(structure);
}
// 측점 종류는 시·종점(BP/EP)만 적는다 — "일반측점"은 대다수라 정보가 없다(2026-08-02 사용자 지시).
if (section.kind === "bp" || section.kind === "ep") {
const kind = document.createElement("span");
kind.textContent = L(
section.kind === "ep" ? "B06_Profile_View_Kind_EP" : "B06_Profile_View_Kind_BP",
);
meta.append(kind);
}
// 성토사면 경사길이는 **행 우측 끝**(2026-09-03 사용자 지시) — meta 가 제목행의 오른쪽
// 끝이므로 그 마지막 칸에 붙인다.
const fillSlope = fillSlopeInfo(section);
if (fillSlope) meta.append(fillSlope);
// 단면유형 pill은 지반유형 우측·우측 맞춤으로 배치(1번). 좌측 구분선은 지반유형 세그먼트에 준다(3번).
modePill.classList.add("b06-cross-card__mode--right");
header.append(title);
if (onDesignChange) {
const controls = buildDesignControls(section, onDesignChange);
controls.groundSegment.classList.add("b06-cross-card__ground");
header.append(controls.groundSegment, modePill, meta);
card.append(header, controls.bar);
return;
}
header.append(modePill, meta);
card.append(header);
}
/** 하단 정보행 — 중심고와 (암 지반일 때) 암 경계선·암 절토 경사 제어. */
export function appendCardFooter(
card: HTMLElement,
section: CrossSection,
rockBoundary?: RockBoundaryControl,
cutSlope?: CutSlopeControl,
): void {
// 중심고는 그래프 Y축 라벨 상자로 옮김(318-2 · `_Cross_Axes`) — 하단 띠는 암 제어만 · 비어도 띠 높이 그대로
const footer = document.createElement("footer");
// 암 경계선 제어는 하단 정보 행 왼쪽(318-2) — 암 지반만.
if (rockBoundary && section.design?.geometry_preset === "rock") {
const rockControl = buildRockBoundaryControl(section, rockBoundary);
rockControl.classList.add("b06-cross-card__rockb");
footer.append(rockControl);
}
// 암 절토 경사(각도)는 암 측점에만 — 토사 측점은 암반이 없어 쓰이지 않는다
// (2026-09-07 사용자 확정). 전체를 바꾸는 자리는 좌측 [표준 횡단면 설정]이다.
if (cutSlope && section.design?.geometry_preset === "rock") {
const slopeControl = buildCutSlopeControl(section, cutSlope);
slopeControl.classList.add("b06-cross-card__cutslope");
footer.append(slopeControl);
}
card.append(footer);
}