Merge remote-tracking branches 'origin/sub_laptop_1', 'origin/sub_laptop_2', 'origin/sub_laptop_3', 'origin/sub_laptop_5' and 'origin/sub_laptop_7' into main_laptop_1
This commit is contained in:
@@ -60,6 +60,7 @@ export type {
|
||||
/** 물넘이포장 제원 — 정의처는 렌더 모듈이다(사본 금지, 타입 전용 import라 순환 없음). */
|
||||
import type { FordPavementSpec } from "./B06_Section_UI_Cross_Ford_Pavement";
|
||||
import type { RevetmentSpec } from "./B06_Section_UI_Cross_Revetment";
|
||||
import type { PavementEntry } from "@util/common_util_structure_pavement";
|
||||
export type { FordPavementSpec, RevetmentSpec };
|
||||
|
||||
export interface SectionContextResponse {
|
||||
@@ -315,6 +316,8 @@ export interface CrossSection extends SectionStation {
|
||||
ford_pavement?: FordPavementSpec;
|
||||
/** 독립 기슭막이 제원(있을 때만). 구조물 정본 D군 구간 안의 측점 전부에 붙는다. */
|
||||
revetment?: RevetmentSpec;
|
||||
/** 노면공(G군) 포장 — 범위 안 측점마다(88). 두께 · 무늬 · 확폭 켬. */
|
||||
pavement?: PavementEntry[];
|
||||
}
|
||||
|
||||
export interface SectionDetailResponse {
|
||||
|
||||
@@ -77,6 +77,7 @@ from B06_Section.B06_Section_Schema import (
|
||||
from B06_Section.B06_Section_Server_Calc_Prebuild import conversion_factors_for, haul_limits_for
|
||||
from common_util.common_util_auth import verify_session
|
||||
from common_util.common_util_storage import resolve_stored_project_path
|
||||
from common_util.common_util_structure_pavement import attach_pavement_structures
|
||||
from common_util.common_util_surface_confirmation import get_surface_confirmation_params
|
||||
from common_util.common_util_workflow_state import get_workflow_state
|
||||
from config.config_db import get_db_pool, run_with_connection
|
||||
@@ -274,6 +275,7 @@ def _read_section_detail(project_root: Path, longitudinal_file_path: str) -> dic
|
||||
# 좌측 「구조물 배치」로 넣은 C군 벽(옹벽·돌쌓기 등)도 같은 제원 자리에 얹는다 —
|
||||
# 그래야 횡단도·설계선 트림·폐회로 면적이 그 벽을 본다(2026-09-06 사용자 확정).
|
||||
attach_wall_structures(root, cross_sections)
|
||||
attach_pavement_structures(root, cross_sections) # 노면공 형상(88)
|
||||
return {"longitudinal": longitudinal, "cross_sections": cross_sections}
|
||||
|
||||
|
||||
@@ -497,6 +499,7 @@ async def regenerate_sections(
|
||||
# 재생성 응답도 상세 조회와 같은 배수관 세트·구조물 벽 정보를 실어야 화면이 어긋나지 않는다.
|
||||
attach_culvert_sets(project_root, result["cross_sections"])
|
||||
attach_wall_structures(project_root, result["cross_sections"])
|
||||
attach_pavement_structures(project_root, result["cross_sections"])
|
||||
return SectionDetailResponse(
|
||||
longitudinal=result["longitudinal"], cross_sections=result["cross_sections"]
|
||||
)
|
||||
|
||||
@@ -34,6 +34,11 @@ import {
|
||||
wallSpecsFrom,
|
||||
type WallStructureInput,
|
||||
} from "@util/common_util_structure_walls";
|
||||
import {
|
||||
attachPavementSpecs,
|
||||
pavementSpecsFrom,
|
||||
type PavementStructureInput,
|
||||
} from "@util/common_util_structure_pavement";
|
||||
import { applyStructureAreaRows, structureAreaRows } from "./B06_Section_Structure_Layouts";
|
||||
import type { CrossSectionPatch, SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
import { fetchSectionDetail, saveSections } from "./B06_Section_Api_Fetch";
|
||||
@@ -65,6 +70,15 @@ async function withDraftWalls(
|
||||
await withDraftPipeKinds(detail, projectId);
|
||||
if (!drafts || !drafts.length) return withStructureAreas(detail);
|
||||
const types = await fetchStructureTypes().catch(() => []);
|
||||
// 노면공(88) — 초안 범위로 포장 제원을 다시 얹음(짝 `common_util_structure_pavement`)
|
||||
const paveTypes = new Set(types.filter((type) => type.group === "G").map((type) => type.type_id));
|
||||
if (paveTypes.size) {
|
||||
for (const section of detail.cross_sections) delete section.pavement;
|
||||
attachPavementSpecs(
|
||||
detail.cross_sections as unknown as Array<Record<string, unknown>>,
|
||||
pavementSpecsFrom(drafts as unknown as PavementStructureInput[], paveTypes),
|
||||
);
|
||||
}
|
||||
const names = new Map(
|
||||
types
|
||||
// 소단은 C군 구간형이지만 **벽이 아니다** — 사면을 계단으로 끊는 시설이라
|
||||
|
||||
@@ -210,8 +210,12 @@ export function withoutGuardedFill<T extends BoxLayout | FordLayout>(
|
||||
if (!layout || !culvert?.culvert.body) return layout;
|
||||
const guarded = (outward: number): boolean => culvert.walls.some((w) => w.outward === outward);
|
||||
const mid = layout.label.at.offset;
|
||||
const reach = (points: OffsetPoint[], outward: number, offset: number): OffsetPoint[] =>
|
||||
points.map((p) => ((p.offset - mid) * outward > 0 ? { ...p, offset } : p));
|
||||
const reach = (
|
||||
points: OffsetPoint[],
|
||||
outward: number,
|
||||
offset: (p: OffsetPoint) => number,
|
||||
): OffsetPoint[] =>
|
||||
points.map((p) => ((p.offset - mid) * outward > 0 ? { ...p, offset: offset(p) } : p));
|
||||
if ("box" in layout && "topSlab" in layout) {
|
||||
const sides = layout.sides.map((side) =>
|
||||
guarded(side.role === "left" ? 1 : -1)
|
||||
@@ -226,7 +230,7 @@ export function withoutGuardedFill<T extends BoxLayout | FordLayout>(
|
||||
const to = wall?.bottomBack.offset ?? side.offset;
|
||||
if ((to - side.offset) * outward < 1e-6) continue;
|
||||
[topSlab, bottomSlab, barrel] = [topSlab, bottomSlab, barrel].map((p) =>
|
||||
reach(p, outward, to),
|
||||
reach(p, outward, () => to),
|
||||
);
|
||||
}
|
||||
const run =
|
||||
@@ -237,19 +241,19 @@ export function withoutGuardedFill<T extends BoxLayout | FordLayout>(
|
||||
const sides = ford.sides.map((side) =>
|
||||
guarded(side.outward) ? { ...side, parts: [], fillSegments: [], cutLine: null } : side,
|
||||
);
|
||||
// 관 · 바닥판이 흙막이까지 따라감 — 관 끝 = 흙막이 전면(관 invert 높이) · 바닥판 끝 = 배면(80)
|
||||
// 관 · 바닥판이 흙막이까지 따라감 — 관 끝 꼭짓점 = 그 높이의 흙막이 전면(사면 이용 · 89-1) ·
|
||||
// 아래 꼭짓점(invert)은 종전 자리라 1 m 맞춤 그대로 · 바닥판 끝 = 배면(80)
|
||||
let { pipe, slabBridge } = ford;
|
||||
for (const side of ford.sides) {
|
||||
const wall = culvert.walls.find((w) => w.role === side.role && w.outward === side.outward);
|
||||
if (!wall) continue;
|
||||
const front =
|
||||
wall.points[2].offset +
|
||||
wall.outward * wall.lean * (wall.topJoint.elevation - side.slabTopElevation);
|
||||
const faceAt = (p: OffsetPoint): number =>
|
||||
wall.points[2].offset + wall.outward * wall.lean * (wall.topJoint.elevation - p.elevation);
|
||||
pipe = {
|
||||
outer: reach(pipe.outer, side.outward, front),
|
||||
inner: reach(pipe.inner, side.outward, front),
|
||||
outer: reach(pipe.outer, side.outward, faceAt),
|
||||
inner: reach(pipe.inner, side.outward, faceAt),
|
||||
};
|
||||
slabBridge = reach(slabBridge, side.outward, wall.bottomBack.offset);
|
||||
slabBridge = reach(slabBridge, side.outward, () => wall.bottomBack.offset);
|
||||
}
|
||||
if (pipe === ford.pipe) return { ...ford, sides } as T;
|
||||
const [right, left] = pipe.outer.slice(2);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { PavementEntry } from "@util/common_util_structure_pavement";
|
||||
import type { CrossDesign } from "./B06_Section_Api_Fetch";
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
@@ -29,26 +30,80 @@ export function appendRockBoundaryOverlay(
|
||||
}
|
||||
}
|
||||
|
||||
type Edge = { offset_m: number; elevation_m: number };
|
||||
|
||||
/** 포장사양 → 무늬(88) — 콘크리트 빗금 · 아스콘 점 · 쇄석 돌. 그 밖 = 채움만. */
|
||||
const PAVE_MARKS: Record<string, { size: number; marks: string }> = {
|
||||
콘크리트: { size: 6, marks: '<path d="M0,6 L6,0" class="b06-chart__pave-mark"/>' },
|
||||
아스콘: {
|
||||
size: 5,
|
||||
marks: '<circle cx="1.5" cy="1.5" r="0.7" class="b06-chart__pave-mark--dot"/>',
|
||||
},
|
||||
쇄석: {
|
||||
size: 9,
|
||||
marks:
|
||||
'<circle cx="2.5" cy="2.5" r="1.6" class="b06-chart__pave-mark"/>' +
|
||||
'<ellipse cx="6.8" cy="6.5" rx="1.8" ry="1.2" class="b06-chart__pave-mark"/>',
|
||||
},
|
||||
};
|
||||
/** 무늬 id 는 문서 안에서 유일 — 카드 · 다시 그리기마다 새 번호(물넘이 빗금과 같은 방식). */
|
||||
let paveSeq = 0;
|
||||
|
||||
/** 확폭 끈 구간 — 규격 폭 끝(좌 = left − 확폭좌 · 우 = right + 확폭우 · 표고는 두 끝 사이 직선). */
|
||||
function standardEdges(edges: { left: Edge; right: Edge }, design: CrossDesign) {
|
||||
const { left, right } = edges;
|
||||
const span = right.offset_m - left.offset_m || 1;
|
||||
const at = (offset: number): Edge => ({
|
||||
offset_m: offset,
|
||||
elevation_m:
|
||||
left.elevation_m + ((right.elevation_m - left.elevation_m) * (offset - left.offset_m)) / span,
|
||||
});
|
||||
return {
|
||||
left: at(left.offset_m - (design.widening_left_m ?? 0)),
|
||||
right: at(right.offset_m + (design.widening_right_m ?? 0)),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 포장층 — 노면선(= 포장 윗면) 아래로 두께만큼. 노면공 범위(`section.pavement`)면 그 두께 · 무늬 ·
|
||||
* 확폭 켬/끔(여러 건은 아래로 차례로 쌓음) · 없으면 카드 포장 켬일 때 표준 두께 채움만.
|
||||
*/
|
||||
export function appendPavementOverlay(
|
||||
svg: SVGElement,
|
||||
design: CrossDesign,
|
||||
x: (offset: number) => number,
|
||||
y: (elevation: number) => number,
|
||||
pavement?: readonly PavementEntry[] | null,
|
||||
): void {
|
||||
const edges = design.carriageway_edges ?? design.road_edges;
|
||||
if (!design.paved || !edges) return;
|
||||
const thickness = design.pavement_thickness_m ?? 0.2;
|
||||
const { left, right } = edges;
|
||||
const polygon = document.createElementNS(SVG_NS, "polygon");
|
||||
polygon.setAttribute(
|
||||
"points",
|
||||
[
|
||||
`${x(left.offset_m)},${y(left.elevation_m)}`,
|
||||
`${x(right.offset_m)},${y(right.elevation_m)}`,
|
||||
`${x(right.offset_m)},${y(right.elevation_m - thickness)}`,
|
||||
`${x(left.offset_m)},${y(left.elevation_m - thickness)}`,
|
||||
].join(" "),
|
||||
);
|
||||
polygon.setAttribute("class", "b06-chart__pavement");
|
||||
svg.append(polygon);
|
||||
if (!edges) return;
|
||||
const layers = pavement?.length ? pavement : design.paved ? [null] : [];
|
||||
let depth = 0;
|
||||
for (const entry of layers) {
|
||||
const thickness = entry?.thickness_m ?? design.pavement_thickness_m ?? 0.2;
|
||||
const { left, right } = entry && !entry.widening_on ? standardEdges(edges, design) : edges;
|
||||
const points = [
|
||||
`${x(left.offset_m)},${y(left.elevation_m - depth)}`,
|
||||
`${x(right.offset_m)},${y(right.elevation_m - depth)}`,
|
||||
`${x(right.offset_m)},${y(right.elevation_m - depth - thickness)}`,
|
||||
`${x(left.offset_m)},${y(left.elevation_m - depth - thickness)}`,
|
||||
].join(" ");
|
||||
depth += thickness;
|
||||
const polygon = document.createElementNS(SVG_NS, "polygon");
|
||||
polygon.setAttribute("points", points);
|
||||
polygon.setAttribute("class", "b06-chart__pavement");
|
||||
svg.append(polygon);
|
||||
const spec = entry?.spec;
|
||||
const mark = spec ? PAVE_MARKS[spec] : undefined;
|
||||
if (!spec || !mark) continue;
|
||||
const id = `b06-pave-hatch-${(paveSeq += 1)}`;
|
||||
const defs = document.createElementNS(SVG_NS, "defs");
|
||||
defs.innerHTML = `<pattern id="${id}" patternUnits="userSpaceOnUse" width="${mark.size}" height="${mark.size}">${mark.marks}</pattern>`;
|
||||
const hatch = document.createElementNS(SVG_NS, "polygon");
|
||||
hatch.setAttribute("points", points);
|
||||
hatch.setAttribute("class", "b06-chart__pavement-hatch");
|
||||
hatch.setAttribute("fill", `url(#${id})`);
|
||||
hatch.dataset.paveSpec = spec;
|
||||
svg.append(defs, hatch);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,15 +145,30 @@ export function appendFordOverlay(
|
||||
// 관·채움이 벽 위에 겹쳐 클릭을 가로챈다 — 투명 겹면을 맨 위에 얹어 벽을 고른다
|
||||
// (배수관 기슭막이 `culvert-revet-hit`와 같은 수법).
|
||||
if (onSelectWall) {
|
||||
// 본체(바닥판 · 관) 판정면 — 벽 판정면보다 먼저 깔아 겹친 자리는 벽이 먹는다(66-1).
|
||||
for (const points of [layout.slabBridge, layout.pipe.outer]) {
|
||||
const hit = polygon(points, "b06-chart__culvert-revet-hit", "세월교 본체 선택");
|
||||
hit.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
onSelectWall("body");
|
||||
});
|
||||
layer.append(hit);
|
||||
}
|
||||
// 본체 판정면 = 바닥판 밑 ~ 측벽 상단 최고 사각형 하나(관 위 성토 · 벽-관 사이 포함 · 89-2) —
|
||||
// 측벽 판정면 · 흙막이(뒤에 그리는 배관 오버레이)가 위에 얹혀 겹친 자리는 그쪽이 먹는다(66-1).
|
||||
const offsets = [
|
||||
...layout.slabBridge,
|
||||
...layout.sides.flatMap((side) => side.parts.flatMap((part) => part.points)),
|
||||
].map((point) => point.offset);
|
||||
const top = Math.max(...layout.sides.map((side) => side.top.elevation));
|
||||
const bottom = Math.min(...layout.slabBridge.map((point) => point.elevation));
|
||||
const [from, to] = [Math.min(...offsets), Math.max(...offsets)];
|
||||
const bodyHit = polygon(
|
||||
[
|
||||
{ offset: from, elevation: top },
|
||||
{ offset: to, elevation: top },
|
||||
{ offset: to, elevation: bottom },
|
||||
{ offset: from, elevation: bottom },
|
||||
],
|
||||
"b06-chart__culvert-revet-hit",
|
||||
"세월교 본체 선택",
|
||||
);
|
||||
bodyHit.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
onSelectWall("body");
|
||||
});
|
||||
layer.append(bodyHit);
|
||||
// 측벽 판정면 = ㄴ(벽 + 바닥판)을 감싸는 가상 사각형(79-6) — 얇은 벽만 잡던 것을 넓힘.
|
||||
for (const side of layout.sides) {
|
||||
const points = side.parts.flatMap((part) => part.points);
|
||||
|
||||
@@ -429,7 +429,8 @@ export function createCrossSectionCard(
|
||||
x,
|
||||
toDisplayY,
|
||||
);
|
||||
if (!fordPaved) appendPavementOverlay(plotLayer, section.design, x, toDisplayY);
|
||||
if (!fordPaved)
|
||||
appendPavementOverlay(plotLayer, section.design, x, toDisplayY, section.pavement);
|
||||
// 세월교 측점 — 계획고는 이미 월류 높이만큼 내려와 있다. "월류가 없었다면"
|
||||
// 노면을 점선으로 남겨 얼마나 내려앉았는지 읽히게 한다(2026-08-30 사용자).
|
||||
appendFordSurfaceDropPlan(plotLayer, section.design, x, toDisplayY, fordLayout);
|
||||
|
||||
@@ -185,6 +185,22 @@
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
/* 노면공 포장사양 무늬(88): 채움 박스 위에 무늬만 한 겹 — 콘크리트 빗금 · 아스콘 점 · 쇄석 돌 */
|
||||
.b06-chart__pavement-hatch {
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.b06-chart__pave-mark {
|
||||
fill: none;
|
||||
stroke: color-mix(in srgb, var(--color-text-body) 70%, transparent);
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.b06-chart__pave-mark--dot {
|
||||
fill: color-mix(in srgb, var(--color-text-body) 70%, transparent);
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
/* 독립 기슭막이(2026-08-28): 성토면 끝에 서는 벽 — 배관 세트 기슭막이와 같은 색 계열 */
|
||||
.b06-chart__revetment {
|
||||
fill: color-mix(in srgb, #9b6bdc 22%, transparent);
|
||||
|
||||
Reference in New Issue
Block a user