feat(B06): 관보호공 연동 풀린 옆 측점 벽 0.1 m 걸음 · 푸는 순간 4축(x · d · h · m) 굳힘 · 다시 연동 = 굳힌 값 지움 · 카드 · 좌측 [연동] 한 길(139-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
This commit is contained in:
@@ -409,16 +409,7 @@ export function computeCardCulvert(
|
||||
// 연동이면 소유 측점이 **실제로 적용한 4축**(자동 자리 포함)을 명시값으로 물려준다.
|
||||
// 조작값만 넘기면 손대기 전(전부 자동)에는 이 측점이 자기 지형으로 자동 자리를 다시
|
||||
// 잡아 소유 측점과 어긋난다 — "한 번 움직이면 맞는다"던 증상의 원인(2026-08-24 사용자).
|
||||
const ownerInput = adjustsInput(link.source, revetOffset, extraWalls);
|
||||
// 세월교 · 박스 흙막이는 본체 기준 x 를 절대값으로 바꿔 넘김 — 옆 측점엔 본체가 없음(73-4).
|
||||
const ownerLayout = computeCulvertLayout(
|
||||
link.source,
|
||||
link.source.samples,
|
||||
ownerInput,
|
||||
link.source.culvert?.hidden_pipe ? "revet" : inletStructure?.valueFor(link.source),
|
||||
inletStructure?.adjustFor(link.source),
|
||||
);
|
||||
const ownerApplied = ownerLayout ? bodyAbsolute(ownerLayout) : ownerInput;
|
||||
const ownerApplied = ownerAppliedAdjusts(link.source, revetOffset, inletStructure, extraWalls);
|
||||
// 연동을 풀어도 **같은 기슭막이**다 — 단 수는 소유 측점 것이고, 이 측점이 손댄
|
||||
// 벽만 자기 값으로 선다(2026-08-30 사용자).
|
||||
const merged = detached ? detachedAdjusts(section, ownerApplied, revetOffset) : ownerApplied;
|
||||
@@ -599,6 +590,25 @@ function detachedAdjusts(
|
||||
};
|
||||
}
|
||||
|
||||
/** 소유 측점이 실제로 적용한 4축 — 연동 카드가 물려받는 값 · 연동을 푸는 순간 굳히는 값(139-2). */
|
||||
export function ownerAppliedAdjusts(
|
||||
owner: CrossSection,
|
||||
revetOffset?: RevetOffsetControl,
|
||||
inletStructure?: InletStructureControl,
|
||||
extraWalls?: ExtraWallControl,
|
||||
): WallAdjustSet {
|
||||
const ownerInput = adjustsInput(owner, revetOffset, extraWalls);
|
||||
// 세월교 · 박스 흙막이는 본체 기준 x 를 절대값으로 바꿔 넘김 — 옆 측점엔 본체가 없음(73-4).
|
||||
const ownerLayout = computeCulvertLayout(
|
||||
owner,
|
||||
owner.samples,
|
||||
ownerInput,
|
||||
owner.culvert?.hidden_pipe ? "revet" : inletStructure?.valueFor(owner),
|
||||
inletStructure?.adjustFor(owner),
|
||||
);
|
||||
return ownerLayout ? bodyAbsolute(ownerLayout) : ownerInput;
|
||||
}
|
||||
|
||||
function adjustsInput(
|
||||
section: CrossSection,
|
||||
revetOffset?: RevetOffsetControl,
|
||||
|
||||
@@ -150,8 +150,11 @@ export function structurePanelDeps(ctx: StructurePanelContext): StructurePanelDe
|
||||
canNudge: (key) => key !== "inlet" || !ctx.inletIsBasin(),
|
||||
// 걸음 분류(2026-08-29 사용자): **배관 기준벽만 1m**, 나머지(독립 기슭막이 기준벽·
|
||||
// 배관/독립 종속 추가 벽·옛 D경로 독립 벽)는 0.1m. 세월교 흙막이는 안이 관이라 1m(80).
|
||||
// 연동을 푼 옆 측점 카드는 자기 자리를 잡는 벽이라 0.1m(139-2).
|
||||
fineMove: (key) =>
|
||||
(key !== "inlet" && key !== "outlet") || (ctx.hiddenPipe() && !ctx.fordGuard?.()),
|
||||
(key !== "inlet" && key !== "outlet") ||
|
||||
(ctx.hiddenPipe() && !ctx.fordGuard?.()) ||
|
||||
(ctx.isLinked && ctx.revetLink?.linkedFor(ctx.section) === false),
|
||||
dragAxes: (key, basin) => wallDragAxes(ctx, key, basin),
|
||||
extraState: () => ctx.extraState(),
|
||||
isLinkedCard: () => ctx.isLinked,
|
||||
@@ -203,20 +206,9 @@ export function structurePanelDeps(ctx: StructurePanelContext): StructurePanelDe
|
||||
ctx.isLinked && ctx.drawnWallKeys().length
|
||||
? { linked: ctx.revetLink?.linkedFor(ctx.section) ?? true }
|
||||
: null,
|
||||
setLinked: (linked) => {
|
||||
// 연동을 **푸는 순간** 지금 그려진 높이를 이 측점 값으로 굳힌다(2026-09-06 사용자
|
||||
// 지시). 예전에는 위치(4축)만 갈리고 높이는 소유 측점 값을 계속 따라가, 소유
|
||||
// 측점 높이를 바꾸면 푼 측점까지 같이 움직였다. 굳혀 두면 값이 튀지 않으면서
|
||||
// 이후에는 따로 논다. 단 수·형태는 소유 측점을 그대로 따른다(2026-08-30 확정 유지).
|
||||
if (!linked) {
|
||||
ctx.drawnWallKeys().forEach((key) => {
|
||||
if (ctx.adjustOf(key).h !== null && ctx.adjustOf(key).h !== undefined) return;
|
||||
const height = ctx.heightOfWall(key);
|
||||
if (height > 0) ctx.revetOffset?.update(ctx.section.chainage_m, key, { h: height });
|
||||
});
|
||||
}
|
||||
ctx.revetLink?.setLinked(ctx.section.chainage_m, linked);
|
||||
},
|
||||
// 푸는 순간 4축(x · d · h · m) 굳힘 · 다시 연동 = 지움 — 제어기 `setLinked` 한 길(139-2 ·
|
||||
// 좌측 관 보호공 [연동]과 같음). 단 수 · 형식은 소유 측점을 그대로 따름(2026-08-30).
|
||||
setLinked: (linked) => ctx.revetLink?.setLinked(ctx.section.chainage_m, linked),
|
||||
followGrade: () =>
|
||||
ctx.revetLink?.followGradeFor(ctx.structureSpan?.ownerOf(ctx.section) ?? ctx.section) ?? true,
|
||||
setFollowGrade: (follow) => {
|
||||
|
||||
@@ -10,12 +10,21 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import type { CrossSection } from "./B06_Section_Api_Fetch";
|
||||
import type { RevetKey } from "./B06_Section_UI_Cross_Culvert";
|
||||
import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import {
|
||||
computeCardCulvert,
|
||||
culvertLinkFor,
|
||||
culvertOwnerFor,
|
||||
culvertReach,
|
||||
culvertWallsStandAt,
|
||||
ownerAppliedAdjusts,
|
||||
type ExtraWallControl,
|
||||
type InletStructureControl,
|
||||
type RevetLinkControl,
|
||||
type RevetOffsetControl,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Wire";
|
||||
import { culvertCardState } from "./B06_Section_UI_Cross_View_Structure";
|
||||
import type { PipeRevetChoice } from "../A00_Common/design/design_structure_pipe_calc";
|
||||
|
||||
/** 세션에 담기는 측점별 조작값 한 겹 맵. */
|
||||
@@ -111,10 +120,59 @@ export function createLinkFlagSession(sessionKey: () => string | null): LinkFlag
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 연동을 푸는 순간(카드 [연동] · 좌측 [연동] 둘 다 · 139-2) — 그 카드가 그리던 벽마다 4축을 이
|
||||
* 측점 값으로 굳힘: 위치(x · d) = 소유 측점 적용값(물려받던 값) · 높이 · 형태 = 그 카드가 그린 값.
|
||||
* d 가 차서 이 측점 지형으로 자동 자리 · 관 정수 맞춤을 다시 타지 않음(튐 없음). 연동 중에 부름.
|
||||
*/
|
||||
export function freezeLinkedWalls(
|
||||
section: CrossSection,
|
||||
sections: readonly CrossSection[],
|
||||
c: {
|
||||
revetOffset: RevetOffsetControl;
|
||||
inletStructure?: InletStructureControl;
|
||||
extraWalls?: ExtraWallControl;
|
||||
revetLink: RevetLinkControl;
|
||||
},
|
||||
): void {
|
||||
const link = section.culvert ? undefined : culvertLinkFor(section, sections, () => null);
|
||||
const owner =
|
||||
link && ownerAppliedAdjusts(link.source, c.revetOffset, c.inletStructure, c.extraWalls);
|
||||
if (!link || !owner) return;
|
||||
const card = computeCardCulvert(
|
||||
section,
|
||||
section.samples,
|
||||
null,
|
||||
c.revetOffset,
|
||||
c.inletStructure,
|
||||
c.extraWalls,
|
||||
link,
|
||||
c.revetLink,
|
||||
);
|
||||
const drawn = card ? culvertCardState(card).wallSpecs : new Map();
|
||||
const roles: Array<[RevetKey, WallAdjust]> = [
|
||||
["inlet", owner.inlet],
|
||||
["outlet", owner.outlet],
|
||||
...owner.extras.map((a, i): [RevetKey, WallAdjust] => [`extra${i}` as RevetKey, a]),
|
||||
...owner.basinExtras.map((a, i): [RevetKey, WallAdjust] => [`bextra${i}` as RevetKey, a]),
|
||||
];
|
||||
for (const [role, applied] of roles) {
|
||||
const wall = drawn.get(role);
|
||||
if (!wall) continue;
|
||||
const round3 = (v: number): number => Math.round(v * 1000) / 1000;
|
||||
c.revetOffset.update(section.chainage_m, role, {
|
||||
x: round3(applied.x),
|
||||
d: applied.d == null ? null : round3(applied.d),
|
||||
h: wall.height,
|
||||
m: wall.form,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 좌측 관 보호공 [연동] · [경사](54-30) — 옛 연동 제어 그대로 읽고 씀. 연동 = 벽이 서는 옆 측점이
|
||||
* 있을 때만 · 켬 = 그 측점이 모두 연동 · 끄면 모두 따로. 경사 = 보호공 구간이 있을 때만.
|
||||
* ponytail: 연동을 풀 때 카드 단추가 하던 높이 굳히기는 안 함 — 필요하면 카드 높이 셈을 여기로.
|
||||
* 풀 때 굳히기 · 다시 걸 때 지우기는 `setLinked` 가 함(139-2 · 카드 단추와 한 길).
|
||||
*/
|
||||
export function pipeRevetChoiceBridge(revetLink: RevetLinkControl) {
|
||||
const linkCards = (owner: CrossSection, sections: readonly CrossSection[]) =>
|
||||
|
||||
@@ -26,7 +26,11 @@ import type {
|
||||
import { culvertOwnerFor, culvertReach, wallStandsAt } from "./B06_Section_UI_Cross_Culvert_Wire";
|
||||
import { createCulvertOptionWriter } from "./B06_Section_Api_Culvert_Options";
|
||||
import { createBodyControls } from "./B06_Section_UI_Page_Ford_Controls";
|
||||
import { createLinkFlagSession, createSessionMap } from "./B06_Section_UI_Page_Link_Session";
|
||||
import {
|
||||
createLinkFlagSession,
|
||||
createSessionMap,
|
||||
freezeLinkedWalls,
|
||||
} from "./B06_Section_UI_Page_Link_Session";
|
||||
import { createSpanControl } from "./B06_Section_UI_Page_Span_Control";
|
||||
import type { FordAdjust } from "./B06_Section_UI_Cross_Ford";
|
||||
import type { FordControl } from "./B06_Section_UI_Cross_Ford_Panel";
|
||||
@@ -528,6 +532,16 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
return section.design?.revet_link_detached !== true;
|
||||
},
|
||||
setLinked: (chainageM, linked) => {
|
||||
// 푸는 순간 그린 4축을 이 측점 값으로 굳힘 · 다시 연동 = 굳힌 값 지움(139-2)
|
||||
const section = sectionAt(chainageM);
|
||||
if (linked) dropLinkedAdjusts(chainageM);
|
||||
else if (section && revetLinkControl.linkedFor(section))
|
||||
freezeLinkedWalls(section, deps.detail()?.cross_sections ?? [], {
|
||||
revetOffset: revetOffsetControl,
|
||||
inletStructure: inletStructureControl,
|
||||
extraWalls: extraWallControl,
|
||||
revetLink: revetLinkControl,
|
||||
});
|
||||
linkDetached.set(chainageKey(chainageM), !linked);
|
||||
persistLinkFlags();
|
||||
patchCachedDesign(chainageM, { revet_link_detached: !linked });
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/* 139-2 연동 풀림 굳힘 헬퍼 — 화면 TS(`_Page_Link_Session.freezeLinkedWalls` · `_Cross_Culvert_Wire`)를
|
||||
* 그 자리에서 트랜스파일해 Node 로 돌림. 입력 = 73-4 픽스처(박스 352.14 흙막이 · 옆 측점 340 · 360).
|
||||
* 짝 = `test_139_2_link_freeze.py`.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const cache = new Map();
|
||||
const noop = new Proxy({}, { get: () => () => "" });
|
||||
// 조정창 모듈이 불러올 때 문서 이벤트를 검 — 기하엔 안 쓰임
|
||||
globalThis.document = { addEventListener: () => undefined };
|
||||
globalThis.window = globalThis;
|
||||
|
||||
function loadTs(file) {
|
||||
const full = path.resolve(file);
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
if (full.endsWith(".json")) {
|
||||
const data = JSON.parse(fs.readFileSync(full, "utf8"));
|
||||
cache.set(full, data);
|
||||
return data;
|
||||
}
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const localRequire = (request) => {
|
||||
// 화면 전용(토스트 · 글 · 설정) — 기하엔 안 쓰임
|
||||
if (request.startsWith("@ui/") || request.startsWith("@config/")) return noop;
|
||||
if (request.startsWith("@util/"))
|
||||
return loadTs(resolve(path.join(ROOT, "common_util", request.slice(6))));
|
||||
return loadTs(resolve(path.join(path.dirname(full), request)));
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
cache.set(full, box.exports);
|
||||
return box.exports;
|
||||
}
|
||||
const resolve = (base) =>
|
||||
[base, `${base}.ts`].find((p) => fs.existsSync(p) && fs.statSync(p).isFile()) ?? `${base}.ts`;
|
||||
|
||||
const B06 = (f) => path.join(ROOT, "B06_Section", f);
|
||||
const { freezeLinkedWalls } = loadTs(B06("B06_Section_UI_Page_Link_Session.ts"));
|
||||
const { computeCardCulvert, culvertLinkFor } = loadTs(B06("B06_Section_UI_Cross_Culvert_Wire.ts"));
|
||||
|
||||
const sections = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, "fixtures", "b06_73_4_body_guard_sections.json"), "utf8"),
|
||||
).cross_sections;
|
||||
const at = (c) => sections.find((s) => Math.abs(s.chainage_m - c) < 0.6);
|
||||
|
||||
// 제어기 흉내 — 측점:역할 → 4축(세션 맵과 같은 꼴)
|
||||
const stored = new Map();
|
||||
const keyOf = (c, role) => `${c.toFixed(2)}:${role}`;
|
||||
const ZERO = { x: 0, d: null, h: null, m: null };
|
||||
const revetOffset = {
|
||||
adjustFor: (s, role) => ({ ...ZERO, ...(stored.get(keyOf(s.chainage_m, role)) ?? {}) }),
|
||||
storedAdjustFor: (s, role) => stored.get(keyOf(s.chainage_m, role)) ?? null,
|
||||
update: (c, role, patch) =>
|
||||
stored.set(keyOf(c, role), { ...ZERO, ...(stored.get(keyOf(c, role)) ?? {}), ...patch }),
|
||||
reset: (c, role) => stored.delete(keyOf(c, role)),
|
||||
syncApplied: () => undefined,
|
||||
selectedFor: () => null,
|
||||
highlightFor: () => null,
|
||||
select: () => undefined,
|
||||
};
|
||||
const extraWalls = { countFor: () => 0, consumeEqualize: () => false };
|
||||
let linked = true;
|
||||
const revetLink = { linkedFor: () => linked, followGradeFor: () => true };
|
||||
const controls = { revetOffset, extraWalls, revetLink };
|
||||
|
||||
const round = (v) => Math.round(v * 1000) / 1000;
|
||||
const shape = (s) => {
|
||||
const link = culvertLinkFor(s, sections, () => null);
|
||||
const layout = computeCardCulvert(
|
||||
s,
|
||||
s.samples,
|
||||
null,
|
||||
revetOffset,
|
||||
undefined,
|
||||
extraWalls,
|
||||
link,
|
||||
revetLink,
|
||||
);
|
||||
return (layout?.walls ?? []).map((w) => ({
|
||||
role: w.role,
|
||||
points: w.points.map((p) => [round(p.offset), round(p.elevation)]),
|
||||
}));
|
||||
};
|
||||
|
||||
const owner = at(352.14);
|
||||
const card = at(360);
|
||||
// 소유 측점 벽을 조금 옮겨 둠 — 옆 카드가 물려받는 자리가 자동 자리가 아니게
|
||||
revetOffset.update(owner.chainage_m, "outlet", { x: 0.7, h: 2.3 });
|
||||
const linkedShape = shape(card);
|
||||
freezeLinkedWalls(card, sections, controls);
|
||||
const frozen = Object.fromEntries(
|
||||
[...stored].filter(([k]) => k.startsWith("360.00:")).map(([k, v]) => [k.slice(7), v]),
|
||||
);
|
||||
linked = false;
|
||||
const detachedShape = shape(card);
|
||||
// 푼 뒤 소유 측점을 또 옮김 — 푼 카드는 안 따라감
|
||||
revetOffset.update(owner.chainage_m, "outlet", { x: 2.0 });
|
||||
const afterOwnerMove = shape(card);
|
||||
linked = true; // 대조 — 연동이면 소유 측점 이동을 따라감(시험이 실제로 재는지)
|
||||
const linkedAfterMove = shape(card);
|
||||
process.stdout.write(
|
||||
JSON.stringify({ linkedShape, frozen, detachedShape, afterOwnerMove, linkedAfterMove }),
|
||||
);
|
||||
@@ -0,0 +1,55 @@
|
||||
"""139-2 관보호공 연동 풀림 — 옆 측점 벽 0.1 m 걸음 · 푸는 순간 4축 굳힘 · 다시 연동 = 지움.
|
||||
|
||||
화면 TS 를 그 자리에서 돌린 값(`helper_139_2_link_freeze.cjs` · 73-4 픽스처 박스 352.14 흙막이 · 옆 측점 360).
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HELPER = Path(__file__).parent / "helper_139_2_link_freeze.cjs"
|
||||
NODE_MODULES = ROOT / "config" / "node_modules"
|
||||
|
||||
pytestmark = pytest.mark.skipif(not NODE_MODULES.is_dir(), reason="프론트엔드 의존성 미설치")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out() -> dict:
|
||||
done = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HELPER)], cwd=str(ROOT), capture_output=True, text=True, encoding="utf-8"
|
||||
)
|
||||
assert done.returncode == 0, done.stderr
|
||||
return json.loads(done.stdout)
|
||||
|
||||
|
||||
def test_푸는_순간_4축이_차고_그림은_그대로(out: dict) -> None:
|
||||
assert set(out["frozen"]) == {"inlet", "outlet"}
|
||||
for adjust in out["frozen"].values():
|
||||
assert isinstance(adjust["x"], (int, float))
|
||||
assert isinstance(adjust["d"], (int, float)) # d 가 차서 자동 자리 · 정수 맞춤 안 탐
|
||||
assert adjust["h"] > 0 and adjust["m"]
|
||||
assert out["frozen"]["outlet"]["h"] == pytest.approx(2.3) # 소유 측점 높이 그대로
|
||||
assert out["detachedShape"] == out["linkedShape"] # 튐 없음
|
||||
|
||||
|
||||
def test_푼_뒤엔_소유_측점_이동을_안_따라감(out: dict) -> None:
|
||||
assert out["afterOwnerMove"] == out["detachedShape"]
|
||||
assert out["linkedAfterMove"] != out["linkedShape"] # 대조 — 연동이면 따라감
|
||||
|
||||
|
||||
def test_한_길_걸음_다시_연동_지움() -> None:
|
||||
stc = (ROOT / "B06_Section" / "B06_Section_UI_Page_Station_Controls.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
vs = (ROOT / "B06_Section" / "B06_Section_UI_Cross_View_Structure.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
# 카드 [연동] · 좌측 [연동] 모두 제어기 setLinked 로 — 풀면 굳힘 · 걸면 지움
|
||||
assert "if (linked) dropLinkedAdjusts(chainageM);" in stc
|
||||
assert "freezeLinkedWalls(section" in stc
|
||||
assert "setLinked: (linked) => ctx.revetLink?.setLinked(ctx.section.chainage_m, linked)" in vs
|
||||
# 풀린 옆 측점 카드 = 0.1 m 걸음
|
||||
assert "(ctx.isLinked && ctx.revetLink?.linkedFor(ctx.section) === false)" in vs
|
||||
Reference in New Issue
Block a user