260725_2
This commit is contained in:
@@ -328,6 +328,8 @@ export function createRouteMarkers(scene: THREE.Scene, getBounds: () => ModelBou
|
||||
selectionListener?.(null);
|
||||
return;
|
||||
}
|
||||
// 빈 공간·경로점 클릭: 측점 선택을 해제해 하이라이트·하단 값 열 오버레이를 초기화한다(D-4).
|
||||
if (selectedStationId !== null) selectStation(null);
|
||||
selectedId =
|
||||
typeof object?.userData.routePointId === "string" ? object.userData.routePointId : null;
|
||||
renderMarkers();
|
||||
|
||||
@@ -204,16 +204,23 @@ export interface CrossDesign {
|
||||
| { type: "standard"; top_width_m: number; bottom_width_m: number; depth_m: number }
|
||||
| { type: "l_type"; width_m: number; depth_m: number }
|
||||
| { type: "none" };
|
||||
/** 측구 생성 여부(엔진이 자동/override 반영해 실제 적용한 결과). */
|
||||
ditch_enabled?: boolean;
|
||||
/** 포장 중첩 여부와 포장층 두께(포장 시). */
|
||||
paved: boolean;
|
||||
pavement_thickness_m?: number;
|
||||
/** B05 법정 경사 분석의 포장 제안 여부(사용자 토글과 무관하게 유지). */
|
||||
pavement_suggested?: boolean;
|
||||
/** 노면 양 끝점(포장층 박스·노면 렌더링 기준). */
|
||||
/** 노면(노견 포함) 양 끝점 — 노면 렌더링 기준. */
|
||||
road_edges: {
|
||||
left: { offset_m: number; elevation_m: number };
|
||||
right: { offset_m: number; elevation_m: number };
|
||||
};
|
||||
/** 차도(노견 제외) 양 끝점 — 포장 범위 기준. */
|
||||
carriageway_edges?: {
|
||||
left: { offset_m: number; elevation_m: number };
|
||||
right: { offset_m: number; elevation_m: number };
|
||||
};
|
||||
design_elevation_m: number;
|
||||
cut_area_m2: number;
|
||||
fill_area_m2: number;
|
||||
@@ -242,6 +249,8 @@ export interface CrossDesignRequest {
|
||||
rock_boundary_offset_m?: number | null;
|
||||
/** 암 지반 2단계 경사 적용 여부(기본 true, 토글로 해제). */
|
||||
two_stage_slope?: boolean;
|
||||
/** 측구 생성 여부. null/미지정=자동 판정, true/false=수동 override. */
|
||||
ditch_enabled?: boolean | null;
|
||||
/** 설정 패널 편집값. 요청값 → config 기본값 순으로 우선한다. */
|
||||
standard_cross_section?: StandardCrossSection;
|
||||
}
|
||||
|
||||
@@ -175,8 +175,10 @@ class _SectionGeometry:
|
||||
soil_cut_ratio: float | None = None,
|
||||
rock_boundary_offset_m: float | None = None,
|
||||
two_stage_slope: bool = False,
|
||||
ditch_enabled: bool | None = None,
|
||||
) -> None:
|
||||
half_road = group["road_width_m"] / 2.0
|
||||
self.half_road = half_road # 차도 반폭(노견 제외) — 포장 범위 기준
|
||||
self.left_extent = half_road + group["shoulder_left_m"] # 좌(+) 노면 끝
|
||||
self.right_extent = half_road + group["shoulder_right_m"] # 우(-) 노면 끝
|
||||
self.z_center = design_elevation_m
|
||||
@@ -193,25 +195,37 @@ class _SectionGeometry:
|
||||
self._ground_at = ground_at
|
||||
self._rock_offset = rock_boundary_offset_m or 0.0
|
||||
self._rock_knee: dict[str, tuple[float, float] | None] = {}
|
||||
# 성토만(양성)이면 측구 없음(합의). 절토가 있는 단면만 측구를 판다.
|
||||
self.has_ditch = section_mode != "both_fill"
|
||||
self.ditch_type = ditch_type
|
||||
# 횡단경사: 측구 방향으로 내려가는 단일 사면 (좌=+offset 규약).
|
||||
slope = cross_slope_pct / 100.0
|
||||
self.slope_per_offset = -slope if ditch_side == "left" else slope
|
||||
|
||||
# 편절편성에서 계획고가 지반보다 많이 낮으면 성토측 지반이 노면 끝보다 높아
|
||||
# 실제로는 양측 절토가 된다. 그 측 역할을 절토로 자동 전환한다(측구 위치는
|
||||
# ditch_side 그대로 유지, 전환된 측도 2단 경사 대상). 양절·양성(사용자 명시)은
|
||||
# 손대지 않는다.
|
||||
if section_mode in ("left_cut", "right_cut") and ground_at is not None:
|
||||
for side, edge in (("left", self.left_extent), ("right", -self.right_extent)):
|
||||
role = self.left_role if side == "left" else self.right_role
|
||||
if role == "fill" and ground_at(edge) > self.road_z(edge) + 1e-3:
|
||||
if side == "left":
|
||||
self.left_role = "cut"
|
||||
else:
|
||||
self.right_role = "cut"
|
||||
# 단면 유형 자동 판정(D-2): 각 측 절/성토 역할을 노면 끝 지반이 설계면보다
|
||||
# 높은지(절토)/낮은지(성토)로 결정한다. 좌절/우절/양절/양성이 모두 지형에서
|
||||
# 자연 도출된다. 사용자 입력 section_mode는 측구 방향(ditch_side) 기본값에만 쓰고
|
||||
# 절/성토 역할은 손대지 않는다. ground_at이 없으면 section_mode 기반 역할을 쓴다.
|
||||
if ground_at is not None:
|
||||
self.left_role = (
|
||||
"cut" if ground_at(self.left_extent) > self.road_z(self.left_extent) + 1e-3
|
||||
else "fill"
|
||||
)
|
||||
self.right_role = (
|
||||
"cut" if ground_at(-self.right_extent) > self.road_z(-self.right_extent) + 1e-3
|
||||
else "fill"
|
||||
)
|
||||
|
||||
# 측구 생성 여부(D-1): 양성은 항상 미생성. ditch_enabled가 오면 그 값을 따르고(수동
|
||||
# override), None이면 자동 판정 — 측구측 노면 끝에서 지반이 설계면보다 높으면(절토
|
||||
# 상황) 생성, 낮으면(성토 상황, 자연 배수) 미생성. ground_at 없으면 보수적으로 생성.
|
||||
if section_mode == "both_fill":
|
||||
self.has_ditch = False
|
||||
elif ditch_enabled is not None:
|
||||
self.has_ditch = ditch_enabled
|
||||
elif ground_at is not None:
|
||||
ditch_edge = self.left_extent if ditch_side == "left" else -self.right_extent
|
||||
self.has_ditch = ground_at(ditch_edge) > self.road_z(ditch_edge) + 1e-3
|
||||
else:
|
||||
self.has_ditch = True
|
||||
|
||||
# 측구 꼭짓점(측구측 노면 끝 기준, 바깥 방향 부호 적용).
|
||||
self.ditch_points: list[tuple[float, float]] = []
|
||||
@@ -363,6 +377,7 @@ def compute_cross_design(
|
||||
standard: dict[str, Any] | None = None,
|
||||
rock_boundary_offset_m: float | None = None,
|
||||
two_stage_slope: bool = True,
|
||||
ditch_enabled: bool | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""측점 하나의 표준횡단 설계선과 절·성토 단면적을 계산한다.
|
||||
|
||||
@@ -422,6 +437,7 @@ def compute_cross_design(
|
||||
soil_cut_ratio=soil_cut_ratio,
|
||||
rock_boundary_offset_m=rock_boundary_offset_m,
|
||||
two_stage_slope=enable_two_stage,
|
||||
ditch_enabled=ditch_enabled,
|
||||
)
|
||||
|
||||
# 적분 오프셋 = 지반 샘플 ∪ 설계 꼭짓점(샘플 범위 안쪽만). 꼭짓점을 넣어야
|
||||
@@ -468,10 +484,20 @@ def compute_cross_design(
|
||||
"depth_m": group["ditch_depth_m"],
|
||||
}
|
||||
|
||||
# 자동 판정된 절/성토 역할에서 실제 단면 유형을 도출해 echo한다(D-2, 표시·저장용).
|
||||
if geometry.left_role == "cut" and geometry.right_role == "cut":
|
||||
resolved_mode = "both_cut"
|
||||
elif geometry.left_role == "fill" and geometry.right_role == "fill":
|
||||
resolved_mode = "both_fill"
|
||||
elif geometry.left_role == "cut":
|
||||
resolved_mode = "left_cut"
|
||||
else:
|
||||
resolved_mode = "right_cut"
|
||||
|
||||
result: dict[str, Any] = {
|
||||
"ground_type": ground_type,
|
||||
"geometry_preset": preset_key,
|
||||
"section_mode": section_mode,
|
||||
"section_mode": resolved_mode,
|
||||
"ditch_side": resolved_ditch_side,
|
||||
"ditch_type": ditch_type if geometry.has_ditch else None,
|
||||
"cut_slope_ratio": round(geometry.cut_ratio, 4),
|
||||
@@ -482,8 +508,9 @@ def compute_cross_design(
|
||||
"carriageway_width_m": round(group["road_width_m"], 4),
|
||||
"cross_slope_pct": round(cross_slope_pct, 4),
|
||||
"ditch": ditch_spec,
|
||||
"ditch_enabled": bool(geometry.has_ditch),
|
||||
"paved": bool(paved),
|
||||
# 노면 양 끝점(프론트 포장층 박스·노면 렌더링 기준).
|
||||
# 노면(노견 포함) 양 끝점 — 노면 렌더링 기준.
|
||||
"road_edges": {
|
||||
"left": {
|
||||
"offset_m": round(geometry.left_extent, 4),
|
||||
@@ -494,6 +521,17 @@ def compute_cross_design(
|
||||
"elevation_m": round(geometry.road_z(-geometry.right_extent), 4),
|
||||
},
|
||||
},
|
||||
# 차도(노견 제외) 양 끝점 — 포장 범위 기준(D-5).
|
||||
"carriageway_edges": {
|
||||
"left": {
|
||||
"offset_m": round(geometry.half_road, 4),
|
||||
"elevation_m": round(geometry.road_z(geometry.half_road), 4),
|
||||
},
|
||||
"right": {
|
||||
"offset_m": round(-geometry.half_road, 4),
|
||||
"elevation_m": round(geometry.road_z(-geometry.half_road), 4),
|
||||
},
|
||||
},
|
||||
"design_elevation_m": round(float(design_elevation_m), 4),
|
||||
"cut_area_m2": round(cut_area, 4),
|
||||
"fill_area_m2": round(fill_area, 4),
|
||||
|
||||
@@ -18,6 +18,7 @@ from B05_wf2_Route.B05_wf2_Route_Engine_Sections import (
|
||||
run_section_generation,
|
||||
)
|
||||
from B05_wf2_Route.B05_wf2_Route_Engine_Sections_Core import SectionGenerationOptions
|
||||
from B05_wf2_Route.B05_wf2_Route_Router_Confirm import _merge_uphill_overrides_into_longitudinal
|
||||
from B06_wf3_ProfileCross.B06_wf3_ProfileCross_Engine_Design import (
|
||||
compute_cross_design,
|
||||
design_elevation_from_longitudinal,
|
||||
@@ -534,6 +535,7 @@ async def compute_cross_section_design(
|
||||
standard=request.standard_cross_section,
|
||||
rock_boundary_offset_m=request.rock_boundary_offset_m,
|
||||
two_stage_slope=request.two_stage_slope,
|
||||
ditch_enabled=request.ditch_enabled,
|
||||
)
|
||||
# B06 지정 시점은 잠정치. B07 도면 확정 시 동일 엔진으로 재계산해 confirmed로 승격한다.
|
||||
design["status"] = "provisional"
|
||||
@@ -645,6 +647,31 @@ async def confirm_sections(
|
||||
except Exception:
|
||||
await connection.rollback()
|
||||
raise
|
||||
|
||||
# 측구 방향(design.ditch_side) 변경을 B05 종단 정본 stations.uphill_side에 역반영한다(E-7).
|
||||
# 파일 기반·비치명적: 실패해도 확정은 유지한다.
|
||||
try:
|
||||
async with pool.acquire() as connection:
|
||||
designs = await get_cross_section_designs(connection, route_id)
|
||||
overrides = [
|
||||
{"chainage_m": record["chainage_m"], "side": record["design"]["ditch_side"]}
|
||||
for record in designs
|
||||
if isinstance(record.get("design"), dict)
|
||||
and record["design"].get("ditch_side") in ("left", "right")
|
||||
]
|
||||
if overrides:
|
||||
await asyncio.to_thread(
|
||||
_merge_uphill_overrides_into_longitudinal,
|
||||
Path(resolve_stored_project_path(stored_path)),
|
||||
str(existing["longitudinal_file_path"]),
|
||||
overrides,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"B06 측구 방향 B05 역반영 실패 (확정은 유지): project_id=%s route_id=%s",
|
||||
project_id,
|
||||
route_id,
|
||||
)
|
||||
return SectionConfirmResponse(project_id=str(project_id), route_id=route_id)
|
||||
except Exception:
|
||||
logger.exception("B06 종횡단 확정 실패: project_id=%s", project_id)
|
||||
|
||||
@@ -52,6 +52,8 @@ class CrossDesignRequest(BaseModel):
|
||||
rock_boundary_offset_m: float | None = None
|
||||
# 암 지반 2단계 경사(암반 경계 아래=암 경사, 위=토사 경사) 적용 여부. 토글로 해제 가능.
|
||||
two_stage_slope: bool = True
|
||||
# 측구 생성 여부. None=자동 판정(측구측 절토면만 생성), True/False=수동 override.
|
||||
ditch_enabled: bool | None = None
|
||||
# B06 설정 패널 편집값(STANDARD_CROSS_SECTION 형태). 요청값 → config 기본값 순.
|
||||
standard_cross_section: dict[str, Any] | None = None
|
||||
|
||||
|
||||
@@ -41,6 +41,12 @@ const MODE_OPTIONS: Array<[SectionMode, keyof typeof ui_locales]> = [
|
||||
["both_cut", "B06_Design_Mode_BothCut"],
|
||||
["both_fill", "B06_Design_Mode_BothFill"],
|
||||
];
|
||||
|
||||
/** 자동 판정된 단면 유형의 지역화 라벨(제목행 pill용, D-2/E-3). */
|
||||
export function sectionModeLabel(mode: SectionMode | undefined): string {
|
||||
const key = MODE_OPTIONS.find(([value]) => value === mode)?.[1];
|
||||
return key ? ui_locales[key][currentLanguageIndex] : "-";
|
||||
}
|
||||
const DITCH_OPTIONS: Array<[DitchSide, keyof typeof ui_locales]> = [
|
||||
["left", "B06_Design_Ditch_Left"],
|
||||
["right", "B06_Design_Ditch_Right"],
|
||||
@@ -58,6 +64,8 @@ export interface CrossDesignChange {
|
||||
paved: boolean;
|
||||
/** 암 지반 2단계 경사(암반 경계 아래=암, 위=토사) 적용 여부. 기본 true, 토글로 해제. */
|
||||
two_stage_slope: boolean;
|
||||
/** 측구 생성 여부. null=자동 판정, true/false=수동 override. */
|
||||
ditch_enabled: boolean | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,13 +90,18 @@ function segment<T extends string>(
|
||||
options: Array<[T, keyof typeof ui_locales]>,
|
||||
selected: T | null,
|
||||
onPick: (value: T) => void,
|
||||
disabled = false,
|
||||
reason = "",
|
||||
): HTMLElement {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "b06-design__seg";
|
||||
const legendEl = document.createElement("span");
|
||||
legendEl.className = "b06-design__seg-legend";
|
||||
legendEl.textContent = legend;
|
||||
wrap.append(legendEl);
|
||||
wrap.className = `b06-design__seg${disabled ? " b06-design__seg--disabled" : ""}`;
|
||||
// 라벨(legend)이 빈 문자열이면 생략한다(E-7: 일부 옵션 라벨 삭제).
|
||||
if (legend) {
|
||||
const legendEl = document.createElement("span");
|
||||
legendEl.className = "b06-design__seg-legend";
|
||||
legendEl.textContent = legend;
|
||||
wrap.append(legendEl);
|
||||
}
|
||||
const group = document.createElement("div");
|
||||
group.className = "b06-design__seg-buttons";
|
||||
for (const [value, labelKey] of options) {
|
||||
@@ -97,7 +110,10 @@ function segment<T extends string>(
|
||||
button.className = `b06-design__btn${value === selected ? " b06-design__btn--active" : ""}`;
|
||||
button.textContent = L(labelKey);
|
||||
button.setAttribute("aria-pressed", value === selected ? "true" : "false");
|
||||
button.addEventListener("click", () => onPick(value));
|
||||
// 선행 조건 미충족 시 비활성(상시 노출하되 클릭 불가·사유 툴팁) — E-6.
|
||||
button.disabled = disabled;
|
||||
if (disabled && reason) button.title = reason;
|
||||
else button.addEventListener("click", () => onPick(value));
|
||||
group.append(button);
|
||||
}
|
||||
wrap.append(group);
|
||||
@@ -111,12 +127,17 @@ function toggle(
|
||||
onLabel: string,
|
||||
offLabel: string,
|
||||
onToggle: () => void,
|
||||
disabled = false,
|
||||
reason = "",
|
||||
): { wrap: HTMLElement; button: HTMLButtonElement } {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "b06-design__seg";
|
||||
const legendEl = document.createElement("span");
|
||||
legendEl.className = "b06-design__seg-legend";
|
||||
legendEl.textContent = legend;
|
||||
wrap.className = `b06-design__seg${disabled ? " b06-design__seg--disabled" : ""}`;
|
||||
if (legend) {
|
||||
const legendEl = document.createElement("span");
|
||||
legendEl.className = "b06-design__seg-legend";
|
||||
legendEl.textContent = legend;
|
||||
wrap.append(legendEl);
|
||||
}
|
||||
const buttons = document.createElement("div");
|
||||
buttons.className = "b06-design__seg-buttons";
|
||||
const button = document.createElement("button");
|
||||
@@ -124,14 +145,19 @@ function toggle(
|
||||
button.className = `b06-design__btn${on ? " b06-design__btn--active" : ""}`;
|
||||
button.textContent = on ? onLabel : offLabel;
|
||||
button.setAttribute("aria-pressed", on ? "true" : "false");
|
||||
button.addEventListener("click", onToggle);
|
||||
button.disabled = disabled;
|
||||
if (disabled && reason) button.title = reason;
|
||||
else button.addEventListener("click", onToggle);
|
||||
buttons.append(button);
|
||||
wrap.append(legendEl, buttons);
|
||||
wrap.append(buttons);
|
||||
return { wrap, button };
|
||||
}
|
||||
|
||||
/** 암 경계선 상/하/리셋 컨트롤(B05 측점 선 제어 ▲/▼/↺ 버튼 패턴 재활용). */
|
||||
function rockBoundaryRow(section: CrossSection, control: RockBoundaryControl): HTMLElement {
|
||||
/** 암 경계선 상/하/리셋 컨트롤(B05 측점 선 제어 ▲/▼/↺ 버튼 패턴 재활용). E-7: X축 행 배치용 export. */
|
||||
export function buildRockBoundaryControl(
|
||||
section: CrossSection,
|
||||
control: RockBoundaryControl,
|
||||
): HTMLElement {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "b06-design__seg b06-design__rockb";
|
||||
const legendEl = document.createElement("span");
|
||||
@@ -177,12 +203,14 @@ function rockBoundaryRow(section: CrossSection, control: RockBoundaryControl): H
|
||||
return wrap;
|
||||
}
|
||||
|
||||
/** 카드 헤더용 설계 지정 컨트롤 바를 만든다. */
|
||||
/**
|
||||
* 카드 설계 지정 컨트롤을 만든다. 지반유형 세그먼트는 제목행 배치용으로 분리 반환하고(D-6),
|
||||
* 나머지(단면유형·측구·2단·포장·암경계)는 본문 `bar`에 담는다.
|
||||
*/
|
||||
export function buildDesignControls(
|
||||
section: CrossSection,
|
||||
onChange: (chainageM: number, change: CrossDesignChange) => void,
|
||||
rockBoundary?: RockBoundaryControl,
|
||||
): HTMLElement {
|
||||
): { bar: HTMLElement; groundSegment: HTMLElement } {
|
||||
const design = section.design;
|
||||
// 기본값: 토사(soil) + 상단측 절토(uphill_side, 미상이면 좌절토) + 일반측구 + 비포장.
|
||||
const state: {
|
||||
@@ -192,20 +220,24 @@ export function buildDesignControls(
|
||||
ditchType: DitchType;
|
||||
paved: boolean;
|
||||
twoStage: boolean;
|
||||
ditchEnabled: boolean | null;
|
||||
} = {
|
||||
ground: design?.ground_type ?? "soil",
|
||||
mode: design?.section_mode ?? (section.uphill_side === "right" ? "right_cut" : "left_cut"),
|
||||
ditch: design?.ditch_side ?? null,
|
||||
ditchType: design?.ditch_type ?? "standard",
|
||||
paved: design?.paved ?? false,
|
||||
twoStage: design?.two_stage_slope ?? true,
|
||||
// 암 design일 때만 저장값을 신뢰(토사는 two_stage=false echo가 무의미) — 암 전환 시 기본 복합경사(4번).
|
||||
twoStage: design && isRock(design.ground_type) ? (design.two_stage_slope ?? true) : true,
|
||||
ditchEnabled: design?.ditch_enabled ?? null,
|
||||
};
|
||||
const bar = document.createElement("div");
|
||||
bar.className = "b06-design";
|
||||
// 컨트롤 상호작용이 카드 선택 클릭으로 전파되지 않게 한다.
|
||||
bar.addEventListener("click", (event) => event.stopPropagation());
|
||||
|
||||
const needsDitch = (): boolean => state.mode === "both_cut" || state.mode === "both_fill";
|
||||
// 측구 방향 선택이 필요한 경우 = 양절(both_cut). 편절은 절토측 자동, 양성은 측구 없음.
|
||||
const needsDitch = (): boolean => state.mode === "both_cut";
|
||||
const emit = (): void => {
|
||||
if (!state.ground || !state.mode) return;
|
||||
// L형 측구는 암 전용 — 토사로 되돌리면 일반측구로 강등해 서버 거부를 예방한다.
|
||||
@@ -217,51 +249,87 @@ export function buildDesignControls(
|
||||
ditch_type: state.ditchType,
|
||||
paved: state.paved,
|
||||
two_stage_slope: state.twoStage,
|
||||
ditch_enabled: state.ditchEnabled,
|
||||
});
|
||||
};
|
||||
|
||||
bar.append(
|
||||
segment(L("B06_Design_Ground_Legend"), GROUND_OPTIONS, state.ground, (value) => {
|
||||
// 지반유형: 제목행 배치용으로 분리 반환(D-6). 라벨 삭제(3번) — 버튼만.
|
||||
const groundSegment = segment(
|
||||
"",
|
||||
GROUND_OPTIONS,
|
||||
state.ground,
|
||||
(value) => {
|
||||
state.ground = value;
|
||||
emit();
|
||||
}),
|
||||
segment(L("B06_Design_Mode_Legend"), MODE_OPTIONS, state.mode, (value) => {
|
||||
state.mode = value;
|
||||
emit();
|
||||
}),
|
||||
},
|
||||
);
|
||||
if (needsDitch()) {
|
||||
bar.append(
|
||||
segment(L("B06_Design_Ditch_Legend"), DITCH_OPTIONS, state.ditch, (value) => {
|
||||
groundSegment.classList.add("b06-design__seg--header");
|
||||
// 단면 유형은 지형에서 자동 판정되며(D-2), 제목행 pill로 표시한다(E-3, Cross_View에서 생성).
|
||||
// 아래 옵션은 상시 노출하되 선행 조건 미충족 시 비활성 처리한다(E-6).
|
||||
const rockCut = isRock(state.ground) && state.mode !== "both_fill";
|
||||
const hasDitch = state.mode !== "both_fill";
|
||||
// 측구 생성 여부(자동 판정 or 사용자 override) — 측구형식은 측구가 있을 때만 의미 있다.
|
||||
const ditchOn = state.ditchEnabled ?? design?.ditch_enabled ?? true;
|
||||
|
||||
// 측구 방향(좌/우): 양절(both_cut)에서만 활성. 편절은 절토측 자동, 양성은 측구 없음. 라벨 삭제(E-7).
|
||||
bar.append(
|
||||
segment(
|
||||
"",
|
||||
DITCH_OPTIONS,
|
||||
state.ditch,
|
||||
(value) => {
|
||||
state.ditch = value;
|
||||
emit();
|
||||
}),
|
||||
);
|
||||
}
|
||||
// 측구형식(일반/L형): 암 지반 + 측구가 존재하는 단면(양성 제외)에서만 노출.
|
||||
if (isRock(state.ground) && state.mode !== "both_fill") {
|
||||
bar.append(
|
||||
segment(L("B06_Design_DitchType_Legend"), DITCH_TYPE_OPTIONS, state.ditchType, (value) => {
|
||||
},
|
||||
state.mode !== "both_cut",
|
||||
L("B06_Design_Disabled_BothCutOnly"),
|
||||
),
|
||||
);
|
||||
// 측구 생성 토글(D-1): 측구 있는 단면에서만 활성. 버튼명 "측구" 고정, 컬러로 상태 표시(E-7).
|
||||
const ditchToggle = toggle(
|
||||
"",
|
||||
ditchOn,
|
||||
L("B06_Design_Ditch_On"),
|
||||
L("B06_Design_Ditch_Off"),
|
||||
() => {
|
||||
state.ditchEnabled = !ditchOn;
|
||||
emit();
|
||||
},
|
||||
!hasDitch,
|
||||
L("B06_Design_Disabled_NoDitch"),
|
||||
);
|
||||
bar.append(ditchToggle.wrap);
|
||||
// 측구형식(일반/L형): 암 지반 + 절토 단면 + 측구가 실제 있을 때만 활성.
|
||||
bar.append(
|
||||
segment(
|
||||
L("B06_Design_DitchType_Legend"),
|
||||
DITCH_TYPE_OPTIONS,
|
||||
state.ditchType,
|
||||
(value) => {
|
||||
state.ditchType = value;
|
||||
emit();
|
||||
}),
|
||||
);
|
||||
// 2단계 경사 토글: 암 지반 + 절토가 있는 단면(양성 제외)에서만. 기본 활성, 해제 시 단일 암 경사.
|
||||
const twoStage = toggle(
|
||||
L("B06_Design_TwoStage_Legend"),
|
||||
state.twoStage,
|
||||
L("B06_Design_TwoStage_On"),
|
||||
L("B06_Design_TwoStage_Off"),
|
||||
() => {
|
||||
state.twoStage = !state.twoStage;
|
||||
emit();
|
||||
},
|
||||
);
|
||||
bar.append(twoStage.wrap);
|
||||
}
|
||||
// 포장 토글: 지반유형과 중첩 적용(횡단경사·포장층만 변경).
|
||||
!rockCut || !ditchOn,
|
||||
!ditchOn ? L("B06_Design_Disabled_NoDitchType") : L("B06_Design_Disabled_RockCut"),
|
||||
),
|
||||
);
|
||||
// 2단계 경사 토글: 암 지반 + 절토 단면에서만 활성. 활성="복합경사"/비활성="단경사"(E-7).
|
||||
const twoStage = toggle(
|
||||
"",
|
||||
state.twoStage,
|
||||
L("B06_Design_TwoStage_On"),
|
||||
L("B06_Design_TwoStage_Off"),
|
||||
() => {
|
||||
state.twoStage = !state.twoStage;
|
||||
emit();
|
||||
},
|
||||
!rockCut,
|
||||
L("B06_Design_Disabled_RockCut"),
|
||||
);
|
||||
bar.append(twoStage.wrap);
|
||||
// 포장 토글: 라벨 삭제, 버튼명 활성="포장"/비활성="비포장"(컬러 유지) — E-7.
|
||||
const paved = toggle(
|
||||
L("B06_Design_Paved_Legend"),
|
||||
"",
|
||||
state.paved,
|
||||
L("B06_Design_Paved_On"),
|
||||
L("B06_Design_Paved_Off"),
|
||||
@@ -281,13 +349,16 @@ export function buildDesignControls(
|
||||
}
|
||||
bar.append(paved.wrap);
|
||||
|
||||
// 암 경계선 제어: 암 지반에서만 노출(서버 재계산 없이 세션 보관, 확정 시 DB 병합).
|
||||
if (rockBoundary && isRock(state.ground)) {
|
||||
bar.append(rockBoundaryRow(section, rockBoundary));
|
||||
}
|
||||
// 암 경계선 제어는 그래프 X축 제목 행으로 이동(E-7, Cross_View에서 배치).
|
||||
|
||||
// 절·성토 면적 readout은 그래프 중상단 오버레이로 이동(E-4, Cross_View에서 배치).
|
||||
return { bar, groundSegment };
|
||||
}
|
||||
|
||||
/** 절·성토 면적값 오버레이(E-4) — 그래프 중상단에 배경색과 함께 표시한다. */
|
||||
export function buildAreaReadout(design: CrossDesign | undefined): HTMLElement {
|
||||
const readout = document.createElement("div");
|
||||
readout.className = "b06-design__areas";
|
||||
readout.className = "b06-cross-card__areas";
|
||||
if (design) {
|
||||
const cut = document.createElement("span");
|
||||
cut.className = "b06-design__area b06-design__area--cut";
|
||||
@@ -302,8 +373,7 @@ export function buildDesignControls(
|
||||
unset.textContent = L("B06_Design_Unset");
|
||||
readout.append(unset);
|
||||
}
|
||||
bar.append(readout);
|
||||
return bar;
|
||||
return readout;
|
||||
}
|
||||
|
||||
/** 횡단 SVG에 표준단면 설계선을 겹쳐 그린다. */
|
||||
@@ -362,9 +432,11 @@ export function appendPavementOverlay(
|
||||
x: (offset: number) => number,
|
||||
toDisplayY: (elevation: number) => number,
|
||||
): void {
|
||||
if (!design.paved || !design.road_edges) return;
|
||||
// 포장은 차도(노견 제외)만 덮는다(D-5). 구 데이터 폴백으로 road_edges를 쓴다.
|
||||
const edges = design.carriageway_edges ?? design.road_edges;
|
||||
if (!design.paved || !edges) return;
|
||||
const thickness = design.pavement_thickness_m ?? 0.2;
|
||||
const { left, right } = design.road_edges;
|
||||
const { left, right } = edges;
|
||||
const points = [
|
||||
`${x(left.offset_m)},${toDisplayY(left.elevation_m)}`,
|
||||
`${x(right.offset_m)},${toDisplayY(right.elevation_m)}`,
|
||||
|
||||
@@ -12,7 +12,10 @@ import {
|
||||
appendCrossDesignOverlay,
|
||||
appendPavementOverlay,
|
||||
appendRockBoundaryOverlay,
|
||||
buildAreaReadout,
|
||||
buildDesignControls,
|
||||
buildRockBoundaryControl,
|
||||
sectionModeLabel,
|
||||
type RockBoundaryControl,
|
||||
} from "./B06_wf3_ProfileCross_UI_Cross_Design";
|
||||
import {
|
||||
@@ -109,6 +112,87 @@ export function crossCardNaturalHeight(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 SVG에 마우스 휠 줌(커서 중심) + 드래그 팬 + 더블클릭 원복을 붙인다(E-5).
|
||||
* viewBox만 조작하고 선은 `vector-effect: non-scaling-stroke`(CSS)로 굵기를 유지해
|
||||
* 확대해도 선·글자가 선명하다. 최대 8배까지, 축소는 원본까지만 허용한다.
|
||||
*/
|
||||
function attachZoomPan(svg: SVGSVGElement, widthPx: number, heightPx: number): void {
|
||||
const base = { x: 0, y: 0, w: widthPx, h: heightPx };
|
||||
const vb = { ...base };
|
||||
const applyVB = (): void => svg.setAttribute("viewBox", `${vb.x} ${vb.y} ${vb.w} ${vb.h}`);
|
||||
const clampPan = (): void => {
|
||||
vb.x = Math.min(Math.max(vb.x, base.x), base.x + base.w - vb.w);
|
||||
vb.y = Math.min(Math.max(vb.y, base.y), base.y + base.h - vb.h);
|
||||
};
|
||||
|
||||
svg.addEventListener(
|
||||
"wheel",
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
const rect = svg.getBoundingClientRect();
|
||||
const mx = vb.x + ((event.clientX - rect.left) / rect.width) * vb.w;
|
||||
const my = vb.y + ((event.clientY - rect.top) / rect.height) * vb.h;
|
||||
const factor = event.deltaY < 0 ? 0.85 : 1 / 0.85;
|
||||
const nw = Math.min(base.w, Math.max(base.w / 8, vb.w * factor));
|
||||
const nh = Math.min(base.h, Math.max(base.h / 8, vb.h * factor));
|
||||
vb.x = mx - (mx - vb.x) * (nw / vb.w);
|
||||
vb.y = my - (my - vb.y) * (nh / vb.h);
|
||||
vb.w = nw;
|
||||
vb.h = nh;
|
||||
clampPan();
|
||||
applyVB();
|
||||
},
|
||||
{ passive: false },
|
||||
);
|
||||
|
||||
let panning = false;
|
||||
let moved = false;
|
||||
let lastX = 0;
|
||||
let lastY = 0;
|
||||
svg.addEventListener("pointerdown", (event) => {
|
||||
panning = true;
|
||||
moved = false;
|
||||
lastX = event.clientX;
|
||||
lastY = event.clientY;
|
||||
svg.setPointerCapture(event.pointerId);
|
||||
});
|
||||
svg.addEventListener("pointermove", (event) => {
|
||||
if (!panning) return;
|
||||
if (Math.abs(event.clientX - lastX) + Math.abs(event.clientY - lastY) > 2) moved = true;
|
||||
const rect = svg.getBoundingClientRect();
|
||||
vb.x -= ((event.clientX - lastX) / rect.width) * vb.w;
|
||||
vb.y -= ((event.clientY - lastY) / rect.height) * vb.h;
|
||||
lastX = event.clientX;
|
||||
lastY = event.clientY;
|
||||
clampPan();
|
||||
applyVB();
|
||||
});
|
||||
const endPan = (event: PointerEvent): void => {
|
||||
panning = false;
|
||||
try {
|
||||
svg.releasePointerCapture(event.pointerId);
|
||||
} catch {
|
||||
/* 이미 해제됨 */
|
||||
}
|
||||
};
|
||||
svg.addEventListener("pointerup", endPan);
|
||||
svg.addEventListener("pointercancel", endPan);
|
||||
// 드래그(팬)로 끝난 클릭은 카드 선택으로 전파하지 않는다.
|
||||
svg.addEventListener("click", (event) => {
|
||||
if (moved) event.stopPropagation();
|
||||
});
|
||||
// 더블클릭 원복.
|
||||
svg.addEventListener("dblclick", (event) => {
|
||||
event.stopPropagation();
|
||||
vb.x = base.x;
|
||||
vb.y = base.y;
|
||||
vb.w = base.w;
|
||||
vb.h = base.h;
|
||||
applyVB();
|
||||
});
|
||||
}
|
||||
|
||||
export function createCrossSectionCard(
|
||||
section: CrossSection,
|
||||
selected: boolean,
|
||||
@@ -131,13 +215,22 @@ export function createCrossSectionCard(
|
||||
if (event.key === "Enter" || event.key === " ") onSelect(section.station_id);
|
||||
});
|
||||
|
||||
// 제목행 1행 구조(E-2/E-3): 측점 위치표기 → 단면유형 pill → 지반유형 → 구조물 → 측점정보.
|
||||
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 kind = document.createElement("span");
|
||||
kind.textContent =
|
||||
section.kind === "ep"
|
||||
@@ -145,7 +238,6 @@ export function createCrossSectionCard(
|
||||
: section.kind === "bp"
|
||||
? L("B06_Profile_View_Kind_BP")
|
||||
: L("B06_Profile_View_Kind_Station");
|
||||
// kind 라벨(일반측점/BP/EP) 좌측에 구조물 정보를 표기(비정규 측점이 확정 시 실어온 값).
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "b06-cross-card__meta";
|
||||
if (section.structure) {
|
||||
@@ -156,9 +248,21 @@ export function createCrossSectionCard(
|
||||
meta.append(structure);
|
||||
}
|
||||
meta.append(kind);
|
||||
header.append(title, meta);
|
||||
card.append(header);
|
||||
if (onDesignChange) card.append(buildDesignControls(section, onDesignChange, rockBoundary));
|
||||
|
||||
// 단면유형 pill은 지반유형 우측·우측 맞춤으로 배치(1번). 좌측 구분선은 지반유형 세그먼트에 준다(3번).
|
||||
modePill.classList.add("b06-cross-card__mode--right");
|
||||
header.append(title);
|
||||
if (onDesignChange) {
|
||||
// 제목행: 측점 라벨 → (구분선) 지반유형 → 단면유형 pill(우측) → 구조물·kind (D-6/E-2/1·3번).
|
||||
const controls = buildDesignControls(section, onDesignChange);
|
||||
controls.groundSegment.classList.add("b06-cross-card__ground");
|
||||
header.append(controls.groundSegment, modePill, meta);
|
||||
card.append(header);
|
||||
card.append(controls.bar);
|
||||
} else {
|
||||
header.append(modePill, meta);
|
||||
card.append(header);
|
||||
}
|
||||
|
||||
const metrics = crossPlotMetrics(
|
||||
section,
|
||||
@@ -332,7 +436,19 @@ export function createCrossSectionCard(
|
||||
class: "b06-chart__axis-label",
|
||||
}),
|
||||
);
|
||||
card.append(svg);
|
||||
// 마우스 휠 줌·드래그 팬·더블클릭 원복(E-5). viewBox 조작 + non-scaling-stroke로 선명도 유지.
|
||||
const chartWrap = document.createElement("div");
|
||||
chartWrap.className = "b06-cross-card__chart-wrap";
|
||||
attachZoomPan(svg, widthPx, heightPx);
|
||||
// 절·성토 면적값은 그래프 중상단 오버레이로 표시(E-4).
|
||||
chartWrap.append(svg, buildAreaReadout(section.design));
|
||||
// 암 경계선 제어는 그래프 X축 제목 행 우측에 배치(E-7). 암 지반에서만.
|
||||
if (rockBoundary && section.design?.geometry_preset === "rock") {
|
||||
const rockControl = buildRockBoundaryControl(section, rockBoundary);
|
||||
rockControl.classList.add("b06-cross-card__rockb");
|
||||
chartWrap.append(rockControl);
|
||||
}
|
||||
card.append(chartWrap);
|
||||
}
|
||||
|
||||
const footer = document.createElement("footer");
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
type StandardPanelController,
|
||||
} from "./B06_wf3_ProfileCross_UI_Standard_Panel";
|
||||
import "./B06_wf3_ProfileCross_UI_Style.css";
|
||||
import "./B06_wf3_ProfileCross_UI_Style_Cross.css";
|
||||
|
||||
function L(key: keyof typeof ui_locales): string {
|
||||
return ui_locales[key][currentLanguageIndex];
|
||||
@@ -162,6 +163,7 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
ditch_type: design.ditch_type ?? "standard",
|
||||
paved: design.paved,
|
||||
two_stage_slope: design.two_stage_slope ?? true,
|
||||
ditch_enabled: design.ditch_enabled ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -171,6 +173,23 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
if (change && change.ground_type !== "soil") void handleDesignChange(chainageM, change);
|
||||
}
|
||||
|
||||
/**
|
||||
* 로드 시 2단계 경사 필드(`two_stage_slope`)가 없는 옛 암 측점 design을 최신 엔진으로
|
||||
* 자동 재계산한다(E-1). 엔진 업데이트 전 저장된 암 design은 단일 경사로 남아 있어,
|
||||
* 지반유형을 다시 고르기 전엔 2단계가 반영되지 않던 문제를 해소한다. 세션 암경계
|
||||
* 오프셋을 실어 재계산하므로 지반유형 변경과 동일한 결과가 나온다.
|
||||
*/
|
||||
function reconcileStaleRockDesigns(): void {
|
||||
if (!sectionDetail) return;
|
||||
for (const section of sectionDetail.cross_sections) {
|
||||
const design = section.design;
|
||||
if (design?.geometry_preset === "rock" && design.two_stage_slope === undefined) {
|
||||
const change = changeFromDesign(section.chainage_m);
|
||||
if (change) void handleDesignChange(section.chainage_m, change);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 암 경계선 오프셋(측점별) 세션 저장소 ─────────────────────────────
|
||||
* 서버 재계산 없이 프론트 세션(sessionStorage)에 보관하고, 종횡단 확정 시
|
||||
* cross_patches로 DB(data.design.rock_boundary_offset_m)에 병합한다.
|
||||
@@ -420,6 +439,7 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
stationInterval = storedOptions.station_interval_m;
|
||||
appliedHalfWidth = crossHalfWidth();
|
||||
renderSectionDetail();
|
||||
reconcileStaleRockDesigns(); // 옛 암 design 2단계 자동 재계산(E-1)
|
||||
updateActionState();
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? ` ${error.message}` : "";
|
||||
|
||||
@@ -23,10 +23,11 @@ export const LONG_WIDTH = 1200;
|
||||
export const LONG_HEIGHT = 220;
|
||||
export const CROSS_WIDTH = 560;
|
||||
export const CROSS_HEIGHT = 250;
|
||||
export const CROSS_GRID_MIN_WIDTH = 480;
|
||||
// 한 행 맞춤 기준 최소 카드 폭 — 조금 더 넓은 화면 필요(480→560, 약 +17%).
|
||||
export const CROSS_GRID_MIN_WIDTH = 560;
|
||||
export const CROSS_GRID_GAP = 16;
|
||||
export const LONG_PAD = { left: 62, right: 24, top: 30, bottom: 52 };
|
||||
export const CROSS_PAD = { left: 58, right: 20, top: 20, bottom: 52 };
|
||||
export const CROSS_PAD = { left: 58, right: 20, top: 10, bottom: 52 };
|
||||
|
||||
export interface YScaleOptions {
|
||||
pixelsPerMeter: number;
|
||||
|
||||
@@ -59,32 +59,23 @@ export function buildStandardDiagram(): HTMLElement {
|
||||
),
|
||||
);
|
||||
|
||||
// 노면(노견 포함): 좌 절토측이 측구 방향으로 살짝 낮게 기운다(횡단경사).
|
||||
// 노면(노견 포함): 위치 안내 모식도라 횡단경사는 반영하지 않고 수평으로 그린다(D-7).
|
||||
const roadLeft = 95;
|
||||
const roadRight = 205;
|
||||
const shoulderL = 108; // 노견 좌 경계
|
||||
const shoulderR = 192; // 노견 우 경계
|
||||
const roadYL = 128;
|
||||
const roadYR = 122;
|
||||
const roadY = 128;
|
||||
svg.append(
|
||||
node(
|
||||
"polyline",
|
||||
{ points: `${roadLeft},${roadYL} ${roadRight},${roadYR}`, fill: "none" },
|
||||
{ points: `${roadLeft},${roadY} ${roadRight},${roadY}`, fill: "none" },
|
||||
"b06-diag__road",
|
||||
),
|
||||
);
|
||||
// 노견 경계 눈금(좌/우)
|
||||
svg.append(
|
||||
node(
|
||||
"line",
|
||||
{ x1: shoulderL, y1: roadYL - 5, x2: shoulderL, y2: roadYL + 5 },
|
||||
"b06-diag__tick",
|
||||
),
|
||||
node(
|
||||
"line",
|
||||
{ x1: shoulderR, y1: roadYR - 5, x2: shoulderR, y2: roadYR + 5 },
|
||||
"b06-diag__tick",
|
||||
),
|
||||
node("line", { x1: shoulderL, y1: roadY - 5, x2: shoulderL, y2: roadY + 5 }, "b06-diag__tick"),
|
||||
node("line", { x1: shoulderR, y1: roadY - 5, x2: shoulderR, y2: roadY + 5 }, "b06-diag__tick"),
|
||||
);
|
||||
|
||||
// 중심선(계획고): 노면 중앙 수직 파선.
|
||||
@@ -96,26 +87,20 @@ export function buildStandardDiagram(): HTMLElement {
|
||||
node(
|
||||
"polygon",
|
||||
{
|
||||
points: `${roadLeft},${roadYL} ${roadLeft - 6},${roadYL + 16} ${roadLeft - 12},${roadYL + 16} ${roadLeft - 15},${roadYL}`,
|
||||
points: `${roadLeft},${roadY} ${roadLeft - 6},${roadY + 16} ${roadLeft - 12},${roadY + 16} ${roadLeft - 15},${roadY}`,
|
||||
},
|
||||
"b06-diag__ditch",
|
||||
),
|
||||
);
|
||||
|
||||
// 절토 사면(좌): 측구 바깥에서 원지반까지 상향.
|
||||
svg.append(node("line", { x1: roadLeft - 15, y1: roadYL, x2: 26, y2: 66 }, "b06-diag__cut"));
|
||||
svg.append(node("line", { x1: roadLeft - 15, y1: roadY, x2: 26, y2: 66 }, "b06-diag__cut"));
|
||||
// 성토 사면(우): 노면 우끝에서 원지반까지 하향.
|
||||
svg.append(node("line", { x1: roadRight, y1: roadYR, x2: 278, y2: 210 }, "b06-diag__fill"));
|
||||
svg.append(node("line", { x1: roadRight, y1: roadY, x2: 278, y2: 210 }, "b06-diag__fill"));
|
||||
|
||||
// 횡단경사 라벨·화살표(노면 위, 측구 방향). 노견 좌/우 라벨은 같은 평행선상 좌·우에 둔다.
|
||||
// 노견 좌/우 라벨은 같은 평행선상 좌·우에 둔다(횡단경사 라벨·화살표는 D-7에서 제거).
|
||||
const labelLineY = 104;
|
||||
svg.append(
|
||||
node(
|
||||
"line",
|
||||
{ x1: centerX + 14, y1: labelLineY + 8, x2: centerX - 14, y2: labelLineY + 12 },
|
||||
"b06-diag__slope-arrow",
|
||||
),
|
||||
text(centerX, labelLineY, L("B06_Std_Diagram_CrossSlope"), "b06-diag__label"),
|
||||
text(shoulderL - 6, labelLineY, L("B06_Std_Diagram_ShoulderL"), "b06-diag__label-sm", "end"),
|
||||
text(shoulderR + 6, labelLineY, L("B06_Std_Diagram_ShoulderR"), "b06-diag__label-sm", "start"),
|
||||
);
|
||||
@@ -124,7 +109,7 @@ export function buildStandardDiagram(): HTMLElement {
|
||||
svg.append(
|
||||
text(centerX, 30, L("B06_Std_Diagram_Center"), "b06-diag__label"),
|
||||
text(centerX, 148, L("B06_Std_Diagram_Road"), "b06-diag__label"),
|
||||
text(roadLeft - 32, roadYL + 32, L("B06_Std_Diagram_Ditch"), "b06-diag__label-sm", "middle"),
|
||||
text(roadLeft - 32, roadY + 32, L("B06_Std_Diagram_Ditch"), "b06-diag__label-sm", "middle"),
|
||||
text(46, 54, L("B06_Std_Diagram_Cut"), "b06-diag__label-sm", "middle"),
|
||||
text(252, 196, L("B06_Std_Diagram_Fill"), "b06-diag__label-sm", "middle"),
|
||||
);
|
||||
|
||||
@@ -196,20 +196,14 @@
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
|
||||
.b06-diag__slope-arrow {
|
||||
stroke: var(--color-text-secondary);
|
||||
stroke-width: 1;
|
||||
marker-end: none;
|
||||
}
|
||||
|
||||
.b06-diag__label {
|
||||
fill: var(--color-text-body);
|
||||
font-size: 9px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.b06-diag__label-sm {
|
||||
fill: var(--color-text-secondary);
|
||||
font-size: 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.b06-std__group {
|
||||
@@ -294,398 +288,3 @@
|
||||
word-break: break-all;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* --- 종·횡단 도면 --- */
|
||||
.b06-profile__main,
|
||||
.b06-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-24);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.b06-section {
|
||||
box-sizing: border-box;
|
||||
padding-inline: var(--spacing-24);
|
||||
}
|
||||
|
||||
.b06-section__panel,
|
||||
.b06-cross-card {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
background: var(--color-surface-raised);
|
||||
}
|
||||
|
||||
.b06-section__panel {
|
||||
position: sticky;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.b06-section__panel > header,
|
||||
.b06-cross-card > header,
|
||||
.b06-cross-card > footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-8);
|
||||
padding: var(--spacing-8) var(--spacing-16);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.b06-section__panel > header {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.b06-section__panel > header h3,
|
||||
.b06-section__heading h3 {
|
||||
font-size: var(--text-body);
|
||||
}
|
||||
|
||||
.b06-section__chart-wrap {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.b06-section__chart {
|
||||
display: block;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.b06-cross-card > .b06-section__chart {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.b06-section__heading {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-16);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.b06-section__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(480px, 100%), 1fr));
|
||||
gap: var(--spacing-16);
|
||||
}
|
||||
|
||||
.b06-cross-card {
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color var(--transition-fast),
|
||||
box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
/* 마우스 hover 회색 강조는 선택 강조(빨강)보다 하위 — 미선택 카드에만 적용해
|
||||
:not(...):hover 특정도가 selected 규칙을 넘어 빨강 테두리를 덮지 않게 한다. */
|
||||
.b06-cross-card:not(.b06-cross-card--selected):hover {
|
||||
border-color: var(--color-text-muted);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.b06-cross-card--selected {
|
||||
border-color: var(--color-danger);
|
||||
box-shadow: 0 0 0 2px var(--color-danger);
|
||||
}
|
||||
|
||||
.b06-cross-card > header {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.b06-cross-card > header div {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b06-cross-card > header strong {
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
/* 비정규 측점의 구조물 라벨 — kind(일반측점/BP/EP) 좌측에 자수정색 pill로 표기. */
|
||||
.b06-cross-card__structure {
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--radius-pills);
|
||||
background: color-mix(in srgb, var(--color-royal-amethyst, rgb(109 40 217)) 16%, transparent);
|
||||
color: var(--color-royal-amethyst, rgb(109 40 217));
|
||||
font-size: var(--text-caption);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.b06-cross-card > footer {
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.b06-section__empty {
|
||||
display: flex;
|
||||
min-height: 180px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-24);
|
||||
border: 1px dashed var(--color-text-muted);
|
||||
color: var(--color-text-muted);
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
/* SVG 차트 색상은 테마 토큰만 사용한다. */
|
||||
.b06-chart__bg {
|
||||
fill: var(--color-surface-raised);
|
||||
}
|
||||
|
||||
.b06-chart__grid {
|
||||
stroke: var(--color-border);
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.b06-chart__axis {
|
||||
stroke: var(--color-text-secondary);
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.b06-chart__tick,
|
||||
.b06-chart__station-label,
|
||||
.b06-chart__axis-label {
|
||||
fill: var(--color-text-secondary);
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
|
||||
.b06-chart__tick,
|
||||
.b06-chart__station-label {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* 구조물(비정규) 측점 이름 — 종단면도 측점선 상단 표기. */
|
||||
.b06-chart__structure-label {
|
||||
font-size: 9px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
fill: rgb(180 83 9);
|
||||
}
|
||||
|
||||
.b06-chart__axis-label {
|
||||
fill: var(--color-text-body);
|
||||
font-size: var(--text-caption);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.b06-chart__profile {
|
||||
fill: none;
|
||||
stroke: var(--color-chart-0);
|
||||
stroke-width: 2.5;
|
||||
}
|
||||
|
||||
.b06-chart__cross-profile {
|
||||
fill: none;
|
||||
stroke: var(--color-chart-1);
|
||||
stroke-width: 2.4;
|
||||
}
|
||||
|
||||
/* 종단 계획선(시공계획고): 지반선과 구분되도록 파선 + 강조색 */
|
||||
.b06-chart__design-profile {
|
||||
fill: none;
|
||||
stroke: var(--color-royal-amethyst);
|
||||
stroke-width: 2.2;
|
||||
stroke-dasharray: 8 4;
|
||||
}
|
||||
|
||||
/* 절토(계획고가 지반고보다 낮음) / 성토 구간 음영 */
|
||||
.b06-chart__band {
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.b06-chart__band--cut {
|
||||
fill: rgb(220 38 38 / 14%);
|
||||
}
|
||||
|
||||
.b06-chart__band--fill {
|
||||
fill: rgb(37 99 235 / 14%);
|
||||
}
|
||||
|
||||
.b06-chart__balance-boundary {
|
||||
stroke: var(--color-royal-amethyst);
|
||||
stroke-width: 1.2;
|
||||
stroke-dasharray: 3 3;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.b06-chart__station {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.b06-chart__station-line {
|
||||
stroke-width: 1.2;
|
||||
}
|
||||
|
||||
.b06-chart__station-hit {
|
||||
stroke: transparent;
|
||||
stroke-width: 14;
|
||||
pointer-events: stroke;
|
||||
}
|
||||
|
||||
.b06-chart__station-line--bp,
|
||||
.b06-chart__station-line--regular {
|
||||
stroke: var(--color-warning);
|
||||
}
|
||||
|
||||
.b06-chart__station-line--ep {
|
||||
stroke: var(--color-accent);
|
||||
stroke-dasharray: 4 3;
|
||||
}
|
||||
|
||||
.b06-chart__station-line--selected,
|
||||
.b06-chart__center-marker {
|
||||
stroke: var(--color-danger);
|
||||
stroke-width: 2.5;
|
||||
}
|
||||
|
||||
/* 십자선이 계획 노선(계획고) 위치를 가리킬 때: 종단 계획선과 동일 색으로 대응시킨다 */
|
||||
.b06-chart__center-marker--design {
|
||||
stroke: var(--color-royal-amethyst);
|
||||
}
|
||||
|
||||
.b06-chart__station--selected .b06-chart__station-label {
|
||||
fill: var(--color-danger);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
/* 측점 표준횡단 설계 지정 컨트롤 (카드 헤더 아래) */
|
||||
.b06-design {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8) var(--spacing-16);
|
||||
padding: var(--spacing-8) 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
margin-bottom: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b06-design__seg {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b06-design__seg-legend {
|
||||
font-size: 0.72rem;
|
||||
color: var(--color-text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.b06-design__seg-buttons {
|
||||
display: inline-flex;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-inputs);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.b06-design__btn {
|
||||
padding: 3px 8px;
|
||||
font-size: 0.72rem;
|
||||
color: var(--color-text-secondary);
|
||||
background: var(--color-surface);
|
||||
border: none;
|
||||
border-left: 1px solid var(--color-border);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.b06-design__btn:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.b06-design__btn--active {
|
||||
color: var(--color-surface);
|
||||
background: var(--color-royal-amethyst);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.b06-design__areas {
|
||||
display: inline-flex;
|
||||
gap: var(--spacing-8);
|
||||
margin-left: auto;
|
||||
font-size: 0.72rem;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.b06-design__area--cut {
|
||||
color: rgb(220 38 38);
|
||||
}
|
||||
|
||||
.b06-design__area--fill {
|
||||
color: rgb(37 99 235);
|
||||
}
|
||||
|
||||
.b06-design__area--unset {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* 횡단 표준단면 설계선 오버레이 */
|
||||
.b06-chart__design-cross {
|
||||
fill: none;
|
||||
stroke: var(--color-royal-amethyst);
|
||||
stroke-width: 1.8;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
/* 암 경계선(설계선 복사 + 오프셋): 리핑암·발파암 구간 점선 */
|
||||
.b06-chart__rock-boundary {
|
||||
fill: none;
|
||||
stroke: var(--color-warning);
|
||||
stroke-width: 1.6;
|
||||
stroke-dasharray: 6 4;
|
||||
stroke-linejoin: round;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* 포장층 박스: 노면 양 끝점 기준 두께만큼 하향 채움 */
|
||||
.b06-chart__pavement {
|
||||
fill: color-mix(in srgb, var(--color-text-secondary) 30%, transparent);
|
||||
stroke: var(--color-text-secondary);
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
/* 암 경계선 상/하/리셋 제어 (B05 측점 선 제어 ▲/▼/↺ 버튼 패턴 재활용) */
|
||||
.b06-design__rockb-btn {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
font-size: 0.7rem;
|
||||
line-height: 1;
|
||||
color: var(--color-text-secondary);
|
||||
background: var(--color-surface);
|
||||
border: none;
|
||||
border-left: 1px solid var(--color-border);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.b06-design__rockb-btn:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.b06-design__rockb-btn:hover {
|
||||
color: var(--color-text);
|
||||
background: var(--color-surface-raised);
|
||||
}
|
||||
|
||||
.b06-design__rockb-btn.is-reset {
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.b06-design__rockb-readout {
|
||||
padding: 0 var(--spacing-8);
|
||||
font-size: 0.72rem;
|
||||
font-family: var(--font-mono);
|
||||
color: var(--color-warning);
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
/* 포장 제안 배지: B05 법정 경사 분석이 포장을 권장한 측점 표시 */
|
||||
.b06-design__paved-badge {
|
||||
font-size: 0.72rem;
|
||||
color: var(--color-warning);
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,495 @@
|
||||
/* =============================================================================
|
||||
* B06_wf3_ProfileCross_UI_Style_Cross.css
|
||||
* 종·횡단 도면 뷰(섹션·카드·차트·설계 컨트롤·줌/팬·오버레이) 스타일.
|
||||
* 700줄 제한 대응으로 _UI_Style.css에서 분리(패널/폼/모식도는 원본 유지).
|
||||
* ========================================================================== */
|
||||
|
||||
/* --- 종·횡단 도면 --- */
|
||||
.b06-profile__main,
|
||||
.b06-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-24);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.b06-section {
|
||||
box-sizing: border-box;
|
||||
padding-inline: var(--spacing-24);
|
||||
}
|
||||
|
||||
.b06-section__panel,
|
||||
.b06-cross-card {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
background: var(--color-surface-raised);
|
||||
}
|
||||
|
||||
.b06-section__panel {
|
||||
position: sticky;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.b06-section__panel > header,
|
||||
.b06-cross-card > header,
|
||||
.b06-cross-card > footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-8);
|
||||
padding: var(--spacing-8) var(--spacing-16);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.b06-section__panel > header {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.b06-section__panel > header h3,
|
||||
.b06-section__heading h3 {
|
||||
font-size: var(--text-body);
|
||||
}
|
||||
|
||||
.b06-section__chart-wrap {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.b06-section__chart {
|
||||
display: block;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
/* 횡단도 줌·팬 영역(E-5) — 휠 줌/드래그 팬. */
|
||||
.b06-cross-card__chart-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.b06-cross-card__chart-wrap .b06-section__chart {
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.b06-cross-card__chart-wrap .b06-section__chart:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* 확대해도 선 굵기 고정(E-5) — 줌 시 해상도 저하처럼 보이던 문제 해소. */
|
||||
.b06-section__chart line,
|
||||
.b06-section__chart polyline,
|
||||
.b06-section__chart polygon,
|
||||
.b06-section__chart rect {
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
|
||||
.b06-cross-card > .b06-section__chart,
|
||||
.b06-cross-card__chart-wrap > .b06-section__chart {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.b06-section__heading {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-16);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.b06-section__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(480px, 100%), 1fr));
|
||||
gap: var(--spacing-16);
|
||||
}
|
||||
|
||||
.b06-cross-card {
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color var(--transition-fast),
|
||||
box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
/* 마우스 hover 회색 강조는 선택 강조(빨강)보다 하위 — 미선택 카드에만 적용해
|
||||
:not(...):hover 특정도가 selected 규칙을 넘어 빨강 테두리를 덮지 않게 한다. */
|
||||
.b06-cross-card:not(.b06-cross-card--selected):hover {
|
||||
border-color: var(--color-text-muted);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.b06-cross-card--selected {
|
||||
border-color: var(--color-danger);
|
||||
box-shadow: 0 0 0 2px var(--color-danger);
|
||||
}
|
||||
|
||||
/* 제목행 1행 구조(E-2/E-3): 측점 라벨 → 단면유형 pill → 지반유형 → 구조물·kind. */
|
||||
.b06-cross-card > header {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.b06-cross-card__title,
|
||||
.b06-cross-card > header > .b06-cross-card__meta {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: var(--spacing-8);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
/* 지반유형 세그먼트(D-6): 제목행 1행 내 인라인 배치. 좌측 구분선(3번). */
|
||||
.b06-design__seg--header {
|
||||
flex: 0 0 auto;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.b06-cross-card__ground {
|
||||
padding-left: var(--spacing-8);
|
||||
border-left: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
/* 단면유형 pill(E-3) — 구조물 pill과 동일 표기. */
|
||||
.b06-cross-card__mode {
|
||||
flex: 0 0 auto;
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--radius-pills);
|
||||
background: color-mix(in srgb, var(--color-info, rgb(37 99 235)) 14%, transparent);
|
||||
color: var(--color-info, rgb(37 99 235));
|
||||
font-size: var(--text-caption);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 단면유형 pill을 지반유형 우측·우측 맞춤으로 밀어 붙인다(1번). meta는 그 뒤에 따라온다. */
|
||||
.b06-cross-card__mode--right {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.b06-cross-card > header strong {
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
/* 비정규 측점의 구조물 라벨 — kind(일반측점/BP/EP) 좌측에 자수정색 pill로 표기. */
|
||||
.b06-cross-card__structure {
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--radius-pills);
|
||||
background: color-mix(in srgb, var(--color-royal-amethyst, rgb(109 40 217)) 16%, transparent);
|
||||
color: var(--color-royal-amethyst, rgb(109 40 217));
|
||||
font-size: var(--text-caption);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.b06-cross-card > footer {
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.b06-section__empty {
|
||||
display: flex;
|
||||
min-height: 180px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-24);
|
||||
border: 1px dashed var(--color-text-muted);
|
||||
color: var(--color-text-muted);
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
/* SVG 차트 색상은 테마 토큰만 사용한다. */
|
||||
.b06-chart__bg {
|
||||
fill: var(--color-surface-raised);
|
||||
}
|
||||
|
||||
.b06-chart__grid {
|
||||
stroke: var(--color-border);
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.b06-chart__axis {
|
||||
stroke: var(--color-text-secondary);
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.b06-chart__tick,
|
||||
.b06-chart__station-label,
|
||||
.b06-chart__axis-label {
|
||||
fill: var(--color-text-secondary);
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
|
||||
.b06-chart__tick,
|
||||
.b06-chart__station-label {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* 구조물(비정규) 측점 이름 — 종단면도 측점선 상단 표기. */
|
||||
.b06-chart__structure-label {
|
||||
font-size: 9px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
fill: rgb(180 83 9);
|
||||
}
|
||||
|
||||
.b06-chart__axis-label {
|
||||
fill: var(--color-text-body);
|
||||
font-size: var(--text-caption);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.b06-chart__profile {
|
||||
fill: none;
|
||||
stroke: var(--color-chart-0);
|
||||
stroke-width: 2.5;
|
||||
}
|
||||
|
||||
.b06-chart__cross-profile {
|
||||
fill: none;
|
||||
stroke: var(--color-chart-1);
|
||||
stroke-width: 2.4;
|
||||
}
|
||||
|
||||
/* 종단 계획선(시공계획고): 지반선과 구분되도록 파선 + 강조색 */
|
||||
.b06-chart__design-profile {
|
||||
fill: none;
|
||||
stroke: var(--color-royal-amethyst);
|
||||
stroke-width: 2.2;
|
||||
stroke-dasharray: 8 4;
|
||||
}
|
||||
|
||||
/* 절토(계획고가 지반고보다 낮음) / 성토 구간 음영 */
|
||||
.b06-chart__band {
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.b06-chart__band--cut {
|
||||
fill: rgb(220 38 38 / 14%);
|
||||
}
|
||||
|
||||
.b06-chart__band--fill {
|
||||
fill: rgb(37 99 235 / 14%);
|
||||
}
|
||||
|
||||
.b06-chart__balance-boundary {
|
||||
stroke: var(--color-royal-amethyst);
|
||||
stroke-width: 1.2;
|
||||
stroke-dasharray: 3 3;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.b06-chart__station {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.b06-chart__station-line {
|
||||
stroke-width: 1.2;
|
||||
}
|
||||
|
||||
.b06-chart__station-hit {
|
||||
stroke: transparent;
|
||||
stroke-width: 14;
|
||||
pointer-events: stroke;
|
||||
}
|
||||
|
||||
.b06-chart__station-line--bp,
|
||||
.b06-chart__station-line--regular {
|
||||
stroke: var(--color-warning);
|
||||
}
|
||||
|
||||
.b06-chart__station-line--ep {
|
||||
stroke: var(--color-accent);
|
||||
stroke-dasharray: 4 3;
|
||||
}
|
||||
|
||||
.b06-chart__station-line--selected,
|
||||
.b06-chart__center-marker {
|
||||
stroke: var(--color-danger);
|
||||
stroke-width: 2.5;
|
||||
}
|
||||
|
||||
/* 십자선이 계획 노선(계획고) 위치를 가리킬 때: 종단 계획선과 동일 색으로 대응시킨다 */
|
||||
.b06-chart__center-marker--design {
|
||||
stroke: var(--color-royal-amethyst);
|
||||
}
|
||||
|
||||
.b06-chart__station--selected .b06-chart__station-label {
|
||||
fill: var(--color-danger);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
/* 측점 표준횡단 설계 지정 컨트롤 (카드 헤더 아래) — 좌측 여유 추가(2번). */
|
||||
.b06-design {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8) var(--spacing-16);
|
||||
padding: var(--spacing-8) 0 var(--spacing-8) var(--spacing-16);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
margin-bottom: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b06-design__seg {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
|
||||
.b06-design__seg-legend {
|
||||
font-size: 0.72rem;
|
||||
color: var(--color-text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 선행 조건 미충족 옵션(E-6) — 상시 노출하되 흐리게·클릭 불가. */
|
||||
.b06-design__seg--disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.b06-design__seg--disabled .b06-design__btn {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.b06-design__seg-buttons {
|
||||
display: inline-flex;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-inputs);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.b06-design__btn {
|
||||
padding: 3px 8px;
|
||||
font-size: 0.72rem;
|
||||
color: var(--color-text-secondary);
|
||||
background: var(--color-surface);
|
||||
border: none;
|
||||
border-left: 1px solid var(--color-border);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.b06-design__btn:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.b06-design__btn--active {
|
||||
color: var(--color-surface);
|
||||
background: var(--color-royal-amethyst);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.b06-design__areas {
|
||||
display: inline-flex;
|
||||
gap: var(--spacing-8);
|
||||
margin-left: auto;
|
||||
font-size: 0.72rem;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
/* 암 경계선 제어(E-7) — 그래프 X축 제목 행 우측 맞춤. */
|
||||
.b06-cross-card__rockb {
|
||||
position: absolute;
|
||||
right: var(--spacing-8);
|
||||
bottom: 2px;
|
||||
background: color-mix(in srgb, var(--color-surface) 82%, transparent);
|
||||
border-radius: var(--radius-inputs);
|
||||
padding: 1px 4px;
|
||||
}
|
||||
|
||||
/* 절·성토 면적 오버레이(E-4) — 그래프 중상단, 배경색으로 그래프 선과 겹쳐도 가독. */
|
||||
.b06-cross-card__areas {
|
||||
position: absolute;
|
||||
top: var(--spacing-8);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: inline-flex;
|
||||
gap: var(--spacing-8);
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-inputs);
|
||||
background: color-mix(in srgb, var(--color-surface) 82%, transparent);
|
||||
font-size: 0.72rem;
|
||||
font-family: var(--font-mono);
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.b06-design__area--cut {
|
||||
color: rgb(220 38 38);
|
||||
}
|
||||
|
||||
.b06-design__area--fill {
|
||||
color: rgb(37 99 235);
|
||||
}
|
||||
|
||||
.b06-design__area--unset {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* 횡단 표준단면 설계선 오버레이 */
|
||||
.b06-chart__design-cross {
|
||||
fill: none;
|
||||
stroke: var(--color-royal-amethyst);
|
||||
stroke-width: 1.8;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
/* 암 경계선(설계선 복사 + 오프셋): 리핑암·발파암 구간 점선 */
|
||||
.b06-chart__rock-boundary {
|
||||
fill: none;
|
||||
stroke: var(--color-warning);
|
||||
stroke-width: 1.6;
|
||||
stroke-dasharray: 6 4;
|
||||
stroke-linejoin: round;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* 포장층 박스: 노면 양 끝점 기준 두께만큼 하향 채움 */
|
||||
.b06-chart__pavement {
|
||||
fill: color-mix(in srgb, var(--color-text-secondary) 30%, transparent);
|
||||
stroke: var(--color-text-secondary);
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
/* 암 경계선 상/하/리셋 제어 (B05 측점 선 제어 ▲/▼/↺ 버튼 패턴 재활용) */
|
||||
.b06-design__rockb-btn {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
font-size: 0.7rem;
|
||||
line-height: 1;
|
||||
color: var(--color-text-secondary);
|
||||
background: var(--color-surface);
|
||||
border: none;
|
||||
border-left: 1px solid var(--color-border);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.b06-design__rockb-btn:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.b06-design__rockb-btn:hover {
|
||||
color: var(--color-text);
|
||||
background: var(--color-surface-raised);
|
||||
}
|
||||
|
||||
.b06-design__rockb-btn.is-reset {
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.b06-design__rockb-readout {
|
||||
padding: 0 var(--spacing-8);
|
||||
font-size: 0.72rem;
|
||||
font-family: var(--font-mono);
|
||||
color: var(--color-warning);
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
/* 포장 제안 배지: B05 법정 경사 분석이 포장을 권장한 측점 표시 */
|
||||
.b06-design__paved-badge {
|
||||
font-size: 0.72rem;
|
||||
color: var(--color-warning);
|
||||
cursor: help;
|
||||
}
|
||||
@@ -785,6 +785,9 @@ export const ui_locales = {
|
||||
B06_Profile_View_ElevationAxis: ["지반고 (m)", "Elevation (m)"],
|
||||
B06_Profile_View_CenterElevation: ["중심고", "Center elevation"],
|
||||
B06_Profile_View_Azimuth: ["방위각", "Azimuth"],
|
||||
B06_Profile_Zoom_In: ["확대", "Zoom in"],
|
||||
B06_Profile_Zoom_Out: ["축소", "Zoom out"],
|
||||
B06_Profile_Zoom_Reset: ["원래 크기", "Reset zoom"],
|
||||
B06_Profile_View_Kind_BP: ["BP", "BP"],
|
||||
B06_Profile_View_Kind_EP: ["EP", "EP"],
|
||||
B06_Profile_View_Kind_Station: ["일반 측점", "Station"],
|
||||
@@ -820,8 +823,27 @@ export const ui_locales = {
|
||||
B06_Design_Paved_On: ["포장", "Paved"],
|
||||
B06_Design_Paved_Off: ["비포장", "Unpaved"],
|
||||
B06_Design_TwoStage_Legend: ["2단계 경사", "Two-stage slope"],
|
||||
B06_Design_TwoStage_On: ["적용", "On"],
|
||||
B06_Design_TwoStage_Off: ["단일 경사", "Single slope"],
|
||||
B06_Design_TwoStage_On: ["복합경사", "Compound slope"],
|
||||
B06_Design_TwoStage_Off: ["단경사", "Single slope"],
|
||||
B06_Design_Ditch_Legend2: ["측구", "Ditch"],
|
||||
B06_Design_Ditch_On: ["측구", "Ditch"],
|
||||
B06_Design_Ditch_Off: ["측구", "Ditch"],
|
||||
B06_Design_Disabled_BothCutOnly: [
|
||||
"양절 단면에서만 선택할 수 있습니다.",
|
||||
"Available only for both-cut sections.",
|
||||
],
|
||||
B06_Design_Disabled_RockCut: [
|
||||
"암(리핑/발파) 지반의 절토 단면에서만 사용할 수 있습니다.",
|
||||
"Available only for rock ground with a cut section.",
|
||||
],
|
||||
B06_Design_Disabled_NoDitch: [
|
||||
"측구가 있는 단면(양성 제외)에서만 사용할 수 있습니다.",
|
||||
"Available only for sections with a ditch (not both-fill).",
|
||||
],
|
||||
B06_Design_Disabled_NoDitchType: [
|
||||
"측구를 생성한 경우에만 형식을 선택할 수 있습니다.",
|
||||
"Available only when a ditch is created.",
|
||||
],
|
||||
B06_Design_Paved_Suggested: [
|
||||
"종단경사 법정 상한 초과 — 포장 권장 (임도설치 및 관리 등에 관한 규정 별표 1-2)",
|
||||
"Grade exceeds legal limit — pavement recommended (Forest Road Regulation, Annex 1-2)",
|
||||
|
||||
Reference in New Issue
Block a user