From c94d8c849a08957f13d170d5b3b56196fdb0a292 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 12 Sep 2026 15:03:36 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(b06):=20=EC=95=94=20=EC=A0=88=ED=86=A0?= =?UTF-8?q?=20=EC=9E=85=EB=A0=A5=EC=9D=84=201:n=20=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B0=94=EA=BE=B8=EA=B3=A0=20=EA=B8=B0=EC=8A=AD=EB=A7=89?= =?UTF-8?q?=EC=9D=B4=20=EB=8C=80=EA=B0=81=EC=84=A0=20=EC=A0=9C=EA=B1=B0?= =?UTF-8?q?=C2=B7=EC=A1=B0=EC=A0=95=EC=B0=BD=20=EC=9E=90=EB=A6=AC=20?= =?UTF-8?q?=EB=82=B4=EB=A6=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 암 절토 칸은 속값이 이미 경사비인데 화면만 각도(°)로 바꿔 보여 줘, 도면·좌측 표준 설정·품셈이 모두 1:n 인 자리에서 이 칸만 머리로 환산해야 했음. 머리말 「1:」과 n 입력으로 바꿈. 기슭막이 상단에서 바닥으로 늘 긋던 대각 이음선은 사용자 지시로 뺌. 조정창이 좌측 상단 면적표를 덮어 수치가 안 보이던 것을 표 높이만큼 내려 세움. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Fe1QWPTfw11PaKh2LjwXdR --- B06_Section/B06_Section_UI_Cross_CutSlope.ts | 56 ++++++++++--------- .../B06_Section_UI_Cross_Wall_Hatch.ts | 12 +--- .../B06_Section_UI_Style_Cross_Controls.css | 7 ++- 3 files changed, 39 insertions(+), 36 deletions(-) diff --git a/B06_Section/B06_Section_UI_Cross_CutSlope.ts b/B06_Section/B06_Section_UI_Cross_CutSlope.ts index 23d1f842..bc0e2232 100644 --- a/B06_Section/B06_Section_UI_Cross_CutSlope.ts +++ b/B06_Section/B06_Section_UI_Cross_CutSlope.ts @@ -6,8 +6,9 @@ * 좌측 패널의 표준 횡단면 설정을 이용」. 그래서 **전체 기본값은 좌측 패널**, **이 칸은 그 측점 * 하나**만 바꾼다. 표준을 바꿔도 여기서 만진 측점은 그대로다(사용자 값이라 재계산이 살려 둔다). * - * 화면에는 **각도(°)**로 보이고 속으로는 경사비(1:n 의 n)로 다닌다 — 소단 폼의 - * 「안쪽 기울기 (°)」와 같은 말법이다. 1:0.4 = 68.2° · 1:1.0 = 45° · 1:1.5 = 33.7°. + * 화면도 속도 **경사비(1:n 의 n)** 로 쓴다(2026-09-12 사용자 지시). 종전에는 화면만 각도(°)로 + * 바꿔 보여 줬는데, 도면·좌측 [표준 횡단면 설정]·품셈이 모두 1:n 이라 이 칸만 머리로 환산해야 + * 했다. 참고: 1:0.4 = 68.2° · 1:1.0 = 45° · 1:1.5 = 33.7°. * * ⚠ 경사는 **나르는 값이 아니라 기하 입력**이다. 계산이 끝난 결과에 값만 베껴 붙이면 설계선은 * 옛 경사로 그려지고 숫자만 새것이 된다 — 값은 반드시 **계산 전에** 넘어가야 한다 @@ -37,13 +38,18 @@ export function degreesToRatio(degrees: number): number { return 1 / Math.tan((clamped * Math.PI) / 180); } -/** 0.1° 단위로 같은 값인가 — 표준값으로 되돌아왔는지 가리는 데 쓴다. */ -function sameDegrees(a: number, b: number): boolean { - return Math.abs(a - b) < 0.05; +/** 경사비를 화면 자릿수(0.01)로 맞춘다 — 사면이 서지 않는 각도(1~89° 밖)는 잘라 받는다. */ +export function clampRatio(ratio: number): number { + return Math.round(degreesToRatio(ratioToDegrees(ratio)) * 100) / 100; +} + +/** 0.01 단위로 같은 값인가 — 표준값으로 되돌아왔는지 가리는 데 쓴다. */ +function sameRatio(a: number, b: number): boolean { + return Math.abs(a - b) < 0.005; } /** - * 카드 하단에 서는 「암 절토 68.2° ↺」 칸. 암 경계선 제어와 같은 자리·같은 모양이다. + * 카드 하단에 서는 「암 절토 1:0.40 ↺」 칸. 암 경계선 제어와 같은 자리·같은 모양이다. * * 되돌리기(↺)는 **표준 횡단면 설정값**으로 돌려놓는다 — 좌측 패널을 그 뒤에 바꾸면 이 측점도 * 따라간다(사용자가 만진 흔적이 지워지므로). @@ -58,30 +64,34 @@ export function buildCutSlopeControl(section: CrossSection, control: CutSlopeCon const group = document.createElement("div"); group.className = "b06-design__seg-buttons"; - const ratio = control.ratioFor(section); - const standard = control.standardRatioFor(section); - const degrees = ratioToDegrees(ratio); + const ratio = clampRatio(control.ratioFor(section)); + const standard = clampRatio(control.standardRatioFor(section)); + + // 「1:」은 고정 머리말 — 사용자가 넣는 것은 뒤의 n 하나다. + const prefix = document.createElement("span"); + prefix.className = "b06-design__cutslope-unit"; + prefix.textContent = "1:"; const input = document.createElement("input"); input.type = "number"; input.className = "b06-design__cutslope-input"; - input.step = "0.5"; - input.min = "1"; - input.max = "89"; - input.value = degrees.toFixed(1); + input.step = "0.05"; + input.min = "0.02"; + input.max = "57.29"; + input.value = ratio.toFixed(2); input.title = - `이 측점의 암 절토 경사 — 각도(°)로 넣는다.\n` + - `지금 1:${ratio.toFixed(2)} (${degrees.toFixed(1)}°) · 표준 1:${standard.toFixed(2)} (${ratioToDegrees(standard).toFixed(1)}°)\n` + + `이 측점의 암 절토 경사 — 경사비(1:n 의 n)로 넣는다. n 이 작을수록 급하다.\n` + + `지금 1:${ratio.toFixed(2)} (${ratioToDegrees(ratio).toFixed(1)}°) · 표준 1:${standard.toFixed(2)} (${ratioToDegrees(standard).toFixed(1)}°)\n` + `전체를 바꾸려면 좌측 [표준 횡단면 설정]을 쓴다.`; const commit = (): void => { const entered = Number(input.value); - if (!Number.isFinite(entered)) { - input.value = degrees.toFixed(1); + if (!Number.isFinite(entered) || entered <= 0) { + input.value = ratio.toFixed(2); return; } // 표준값으로 되돌아온 입력은 사용자 값을 남기지 않는다 — 그래야 표준을 바꿀 때 따라간다. - const next = sameDegrees(entered, ratioToDegrees(standard)) ? null : degreesToRatio(entered); - control.set(section.chainage_m, next); + const next = clampRatio(entered); + control.set(section.chainage_m, sameRatio(next, standard) ? null : next); }; input.addEventListener("change", commit); input.addEventListener("keydown", (event) => { @@ -93,22 +103,18 @@ export function buildCutSlopeControl(section: CrossSection, control: CutSlopeCon // 카드 클릭(선택·줌)이 입력을 뺏지 않게 한다 — 암 경계선 버튼과 같은 태도. input.addEventListener("pointerdown", (event) => event.stopPropagation()); - const unit = document.createElement("span"); - unit.className = "b06-design__cutslope-unit"; - unit.textContent = "°"; - const reset = document.createElement("button"); reset.type = "button"; reset.className = "b06-design__rockb-btn is-reset"; reset.textContent = "↺"; reset.title = `표준값으로 되돌리기 (1:${standard.toFixed(2)} · ${ratioToDegrees(standard).toFixed(1)}°)`; - reset.disabled = sameDegrees(degrees, ratioToDegrees(standard)); + reset.disabled = sameRatio(ratio, standard); reset.addEventListener("click", (event) => { event.stopPropagation(); control.set(section.chainage_m, null); }); - group.append(input, unit, reset); + group.append(prefix, input, reset); wrap.append(group); return wrap; } diff --git a/B06_Section/B06_Section_UI_Cross_Wall_Hatch.ts b/B06_Section/B06_Section_UI_Cross_Wall_Hatch.ts index 717aa941..9905c0c2 100644 --- a/B06_Section/B06_Section_UI_Cross_Wall_Hatch.ts +++ b/B06_Section/B06_Section_UI_Cross_Wall_Hatch.ts @@ -172,15 +172,9 @@ export function appendWallHatch( ((offset - wall.bottomBack.offset) / bottomSpan) : wall.bottomBack.elevation; - // 평행사변형 띠와 사다리꼴 사이 대각 이음선(사용자 스케치의 가운데 선) — - // 상단 변 중간점에서 전면과 나란히 바닥으로 내려온다. 형태와 무관하게 늘 긋는다. - const jointBaseOffset = wall.outerOffset - wall.outward * REVET_THICKNESS_M; - layer.append( - line( - [x(wall.topJoint.offset), toDisplayY(wall.topJoint.elevation)], - [x(jointBaseOffset), toDisplayY(bottomAtOffset(jointBaseOffset))], - ), - ); + // 벽을 가로지르던 대각 이음선은 **긋지 않는다**(2026-09-12 사용자: 필요 없음). + // 종전에는 상단 변 중간점에서 바닥까지 형태와 무관하게 늘 그어, 돌쌓기·콘크리트 해칭 + // 위로 선이 하나 더 지나가 도면이 지저분했다. clipSeq += 1; const clipId = `b06-revet-clip-${keyId}-${clipSeq}`; diff --git a/B06_Section/B06_Section_UI_Style_Cross_Controls.css b/B06_Section/B06_Section_UI_Style_Cross_Controls.css index e27a0b25..639ca16a 100644 --- a/B06_Section/B06_Section_UI_Style_Cross_Controls.css +++ b/B06_Section/B06_Section_UI_Style_Cross_Controls.css @@ -4,11 +4,14 @@ * 넘칠 만큼 길어지면 창 안에서 스크롤한다. z-index는 카드 안 오버레이 중 최상단. */ .b06-structure-panel { position: absolute; - top: var(--spacing-8); + /* 좌측 상단에는 절·성토 면적표가 이미 서 있다 — 조정창을 같은 자리에 두면 표를 덮어 + 수치가 안 보인다(2026-09-12 사용자). 표 높이(머리글+절토+성토 세 줄)만큼 내려 시작한다. + 아래를 기준으로 붙이지 않는 까닭은 종전 그대로다 — 낮은 횡단도에서 창이 잘린다. */ + top: calc(var(--spacing-8) + var(--b06-areas-height, 3.2rem)); left: var(--spacing-8); z-index: 20; display: flex; - max-height: calc(100% - 2 * var(--spacing-8)); + max-height: calc(100% - 2 * var(--spacing-8) - var(--b06-areas-height, 3.2rem)); flex-direction: column; gap: 2px; padding: var(--spacing-8); From 4793eb41300db370dc19652766210decc3f5f047 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 12 Sep 2026 15:09:09 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat(B05):=20=EA=B3=84=ED=9A=8D=EB=85=B8?= =?UTF-8?q?=EC=84=A0=20=ED=8E=B8=EC=A7=91=20=ED=99=94=EB=A9=B4=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC=EC=99=80=20=EC=A7=80=EB=8F=84=20=ED=9A=8C=EC=A0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 조작 설명을 지도 좌상단 2열 오버레이로, 상태·범례를 좌하단 오버레이로 (계획서 0-9 ⑩⑫) - 단추를 제목행 오른쪽으로 옮기고 공용 `.ui-btn` 규격을 씀, 하단 정보행 삭제 (⑪⑬) - LAS 등고선을 도엽과 같은 5m 단위로 그림 (⑭) - 바탕이 LAS 면 세류선도 등고선 범위 안쪽만 그림 (⑮) - 지도 오른쪽 위 반시계·시계 단추로 15°씩 그림 전체 회전, 집기도 따라감 (⑯) - R·L 하한을 5m 로 채움. 다만 L 하한은 칸 입력만 막음 — 기하에 걸면 거의 곧은 자리에서 R 이 부풀어 아무것도 안 고쳤는데 노선이 바뀜 (⑰) Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012jsXWphgRUHAG2mFupSKPX --- B04_PreProcess/B04_PreProcess_UI_MapRender.ts | 27 ++++++ B05_Profile/B05_Profile_UI_RouteEdit.ts | 84 ++++++++++------- .../B05_Profile_UI_RouteEdit_Contour.ts | 16 +++- .../B05_Profile_UI_RouteEdit_CurveBar.ts | 1 + B05_Profile/B05_Profile_UI_RouteEdit_Edits.ts | 42 ++++----- B05_Profile/B05_Profile_UI_RouteEdit_Input.ts | 17 ++-- .../B05_Profile_UI_RouteEdit_Render.ts | 24 +++++ .../B05_Profile_UI_RouteEdit_Rotate.ts | 71 +++++++++++++++ .../B05_Profile_UI_Style_RouteEdit.css | 89 ++++++++++++------- config/config_system_design.py | 24 ++--- resources/tester/test_plan_curve_limits.py | 24 ++--- 11 files changed, 301 insertions(+), 118 deletions(-) create mode 100644 B05_Profile/B05_Profile_UI_RouteEdit_Rotate.ts diff --git a/B04_PreProcess/B04_PreProcess_UI_MapRender.ts b/B04_PreProcess/B04_PreProcess_UI_MapRender.ts index 00aa2b95..7921d46e 100644 --- a/B04_PreProcess/B04_PreProcess_UI_MapRender.ts +++ b/B04_PreProcess/B04_PreProcess_UI_MapRender.ts @@ -438,6 +438,33 @@ function isVisible(feature: PreparedFeature, affine: Affine, view: ViewState): b ); } +/** 레이어가 차지하는 **화면 사각형**(px). 피처가 없으면 null. + * + * LAS 등고선처럼 도엽보다 좁은 자료 위에 다른 레이어를 겹칠 때, 그 자료가 있는 데까지만 + * 그리려고 쓴다(계획서 0-9 ⑮ — 세류선이 등고선 밖까지 뻗던 자리). */ +export function layerScreenBounds( + layer: PreparedLayer, + view: ViewState, +): { x: number; y: number; width: number; height: number } | null { + if (layer.features.length === 0) return null; + const affine = affineOf(view); + let minX = Infinity; + let minY = Infinity; + let maxX = -Infinity; + let maxY = -Infinity; + for (const feature of layer.features) { + minX = Math.min(minX, feature.minX); + minY = Math.min(minY, feature.minY); + maxX = Math.max(maxX, feature.maxX); + maxY = Math.max(maxY, feature.maxY); + } + const x0 = minX * affine.ax + affine.bx; + const x1 = maxX * affine.ax + affine.bx; + const y0 = minY * affine.ay + affine.by; + const y1 = maxY * affine.ay + affine.by; + return { x: x0, y: y0, width: x1 - x0, height: y1 - y0 }; +} + /** 등고선을 몇 m 마다 낼지 고를 때 훑는 배수. 성긴 쪽으로 한 칸씩 물러난다. */ const LEVEL_STEP_MULTIPLES = [1, 2, 5, 10, 20, 50, 100]; /** 한 화면에 둘 등고선 가닥 수의 어림 상한 — 이보다 많으면 한 칸 성글게 간다. */ diff --git a/B05_Profile/B05_Profile_UI_RouteEdit.ts b/B05_Profile/B05_Profile_UI_RouteEdit.ts index 99c76a89..dd517a27 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit.ts @@ -45,6 +45,7 @@ import { stationAtScreen, } from "./B05_Profile_UI_RouteEdit_Input"; import { createCrossPreview } from "./B05_Profile_UI_RouteEdit_Cross"; +import { createMapRotation } from "./B05_Profile_UI_RouteEdit_Rotate"; import { createMeasureTool } from "./B05_Profile_UI_RouteEdit_Measure"; import { createCurveBar } from "./B05_Profile_UI_RouteEdit_CurveBar"; import { @@ -98,30 +99,40 @@ export async function openRouteEditModal(