Merge remote-tracking branch 'origin/sub_laptop_7' into main_laptop_1
This commit is contained in:
@@ -238,10 +238,8 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult
|
||||
let prevBottom = input.startBottomElevation;
|
||||
|
||||
for (let i = 0; i < input.adjusts.length; i += 1) {
|
||||
// 시작점이 원지반 아래 = 윗단 벽이 0.5m 이상 묻힘 → 성토 불필요, 다단 종료.
|
||||
if (groundAt(src.offset) >= src.elevation - 0.01) break;
|
||||
// 원지반은 단을 끊지 않는다 — 요청한 단 수 그대로 세운다(54-45).
|
||||
const trailing = trailingFill(src, outward, groundAt);
|
||||
if (trailing.lengthM < 0.7) break;
|
||||
|
||||
const adjust = input.adjusts[i];
|
||||
// 형태(B05 폼 6종)가 재질·높이 한계를 정한다 — 조작이 없으면 소유 벽 형태를 따른다
|
||||
@@ -293,8 +291,8 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult
|
||||
bestOffset = x;
|
||||
}
|
||||
}
|
||||
if (autoOffset === null) autoOffset = bestOffset;
|
||||
if (autoOffset === null) break; // 세울 만한 지형이 아니다 — 되는 만큼만.
|
||||
// 앉을 지형이 없으면(묻힘) 이음선이 시작점 바로 밖 — 원지반과 무관하게 선다.
|
||||
autoOffset ??= bestOffset ?? src.offset + outward * jointRunOf(exposed, lean);
|
||||
const autoJointRun = (autoOffset - src.offset) * outward - jointRunOf(exposed, lean);
|
||||
/** 자동 자리(선반 0·수직 0)의 벽 상단 표고 — 조작은 여기서부터 잰다. */
|
||||
const autoTop = src.elevation - autoJointRun / FILL_SLOPE_RATIO_MIN;
|
||||
@@ -314,52 +312,11 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult
|
||||
FILL_SLOPE_RATIO_MIN
|
||||
: (plan[i] ?? 0);
|
||||
const requestedX = plan === "user" ? adjust.x : 0;
|
||||
/** 후보 자리의 바닥 — buildExtraWall과 같은 근입 0.5m 고정(2026-08-23). */
|
||||
const bottomAt = (_anchorX: number, baseElevation: number): number =>
|
||||
baseElevation - REVET_EMBED_DEPTH_M;
|
||||
// 이동 한계 = 원지반 매몰 자리 금지(2026-08-22 사용자 확정): ① 이음선 상단이
|
||||
// 지반 아래(벽이 통째로 묻힘) ② 자기 성토선 시작점(실바닥+0.5 전면 교차점)이
|
||||
// 지반 아래 0.05m 이상(= 0.5m 이상 매몰 → 성토 불필요 자리).
|
||||
const placeable = (xShift: number, vShift: number): boolean => {
|
||||
const topE = autoTop - vShift;
|
||||
const aX = autoOffset + outward * xShift;
|
||||
const jointX = aX - outward * jointRunOf(exposed, lean);
|
||||
if (
|
||||
Math.max(groundAt(jointX), groundAt(jointX + outward * REVET_THICKNESS_M)) >=
|
||||
topE - 0.01
|
||||
)
|
||||
return false;
|
||||
const baseE = topE - exposed;
|
||||
const bottom = bottomAt(aX, baseE);
|
||||
const startE = bottom + REVET_EMBED_DEPTH_M;
|
||||
const topFrontX = jointX + outward * REVET_THICKNESS_M;
|
||||
const startX = topFrontX + outward * lean * (topE - startE);
|
||||
if (groundAt(startX) < startE + 0.05) return true;
|
||||
// 매몰 — 3도 절토선(계류 쪽 내림)이 원지반과 다시 만나면 허용, 아니면 이동 금지.
|
||||
return (
|
||||
slopedCrossing(
|
||||
{ offset: startX, elevation: startE },
|
||||
outward,
|
||||
-PIPE_CONNECT_GRADE,
|
||||
groundAt,
|
||||
limitOffset,
|
||||
) !== null
|
||||
);
|
||||
};
|
||||
let appliedD = Math.max(requestedD, minV);
|
||||
/** 선반 길이가 음수가 되면 각도가 깨진다 — x가 최소 1.2·d까지 따라 나간다. */
|
||||
const shelfFloor = (v: number): number => FILL_SLOPE_RATIO_MIN * v;
|
||||
// 원지반은 자리를 막지 않는다 — 묻히든 뜨든 입력 그대로(54-45).
|
||||
let appliedX = Math.max(requestedX, 0, shelfFloor(appliedD));
|
||||
for (let pass = 0; pass < 400 && !placeable(appliedX, appliedD); pass += 1) {
|
||||
// 매몰 자리 — 요청을 자동 자리 쪽으로 0.05m씩 되돌린다(상하 먼저, 다음 좌우).
|
||||
if (appliedD > minV + 1e-9) {
|
||||
appliedD = Math.max(minV, appliedD - 0.05);
|
||||
appliedX = Math.max(appliedX, shelfFloor(appliedD));
|
||||
} else if (appliedX > shelfFloor(appliedD) + 1e-9) {
|
||||
appliedX = Math.max(shelfFloor(appliedD), appliedX - 0.05);
|
||||
} else break;
|
||||
}
|
||||
if (!placeable(appliedX, appliedD)) break; // 자동 자리조차 매몰 — 이 단은 불가.
|
||||
appliedD = Math.round(appliedD * 10) / 10;
|
||||
appliedX = Math.round(appliedX * 10) / 10;
|
||||
const topElevation = autoTop - appliedD;
|
||||
@@ -378,10 +335,6 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult
|
||||
floatGapM: Math.max(0, base - groundBase),
|
||||
lean,
|
||||
});
|
||||
// 요청한 좌우 이동을 지형이 막았으면 그 양을 남긴다 — 「눌러도 안 움직인다」의 까닭을
|
||||
// 툴팁으로 알리기 위함이다(2026-09-06 실측: 다섯 측점 중 넷이 1.0m 요청에 0.0m 이동).
|
||||
const blocked = Math.max(0, (requestedX ?? 0) - appliedX);
|
||||
if (blocked > 0.005) wall.shiftBlockedM = Math.round(blocked * 100) / 100;
|
||||
// 하한에 눌려 요청이 통째로 무시된 경우 — 지형이 아니라 **선반 길이**가 원인이다
|
||||
// (2026-09-07 실측: d 2.6m 이면 x 하한이 3.12m 라 1.0m 요청이 아무 변화도 못 냄).
|
||||
// 사용자가 실제로 넣었을 때만 알린다 — 요청이 없으면(0) 하한은 그냥 기본 자리라
|
||||
|
||||
@@ -62,7 +62,6 @@ import {
|
||||
designInterpolator,
|
||||
slopeToeOffset,
|
||||
pipeAxisSolver,
|
||||
placeInletWall,
|
||||
placePipeWall,
|
||||
slopeLengthAlong,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Solve";
|
||||
@@ -200,9 +199,8 @@ export function computeCulvertLayout(
|
||||
if (!basinReason && designAt) {
|
||||
const inletBaseAt = (offset: number): number =>
|
||||
Math.min(groundAt(offset), invertCap(inletInfo.edge));
|
||||
// 4축 배치는 공용 풀이(placeInletWall — Solve). 유입을 내리면 유출도 따라
|
||||
// 내려가므로, 유출이 못 받으면 유입도 못 내려간다(outletGuard).
|
||||
const placed = placeInletWall({
|
||||
// 4축 배치는 공용 풀이(placePipeWall — Solve). 원지반은 자리를 막지 않는다(54-45).
|
||||
const placed = placePipeWall({
|
||||
autoOffset: slopeZeroAnchor(
|
||||
inletInfo.edge,
|
||||
inletInfo.outward,
|
||||
@@ -213,30 +211,10 @@ export function computeCulvertLayout(
|
||||
height: inletWallSpec.height,
|
||||
baseElevation0: inletInfo.edge.elevation_m - inletWallSpec.height,
|
||||
edgeElevation: inletInfo.edge.elevation_m,
|
||||
// 독립 기슭막이(관 숨김)는 관 토피·역경사 제약이 없다 — 유입도 노견까지 자유롭게
|
||||
// 오르내린다(관이 없어 유출 수용 검사 outletGuard도 뺀다 — 위 이동을 막던 원인).
|
||||
// 독립 기슭막이(관 숨김)는 관 토피·역경사 제약이 없다 — 유입도 노견까지 자유롭게 오르내린다.
|
||||
invertCap: culvert.hidden_pipe ? Number.POSITIVE_INFINITY : invertCap(inletInfo.edge),
|
||||
adjust: adjInlet,
|
||||
defaultD: PIPE_WALL_DEFAULT_RUN_M,
|
||||
groundAt,
|
||||
limitOffset: inletInfo.limit,
|
||||
// 독립 기슭막이는 유출 벽과 같은 자리 규칙 — 매몰-무교차 자리 금지(2026-08-29).
|
||||
requireCrossing: culvert.hidden_pipe === true,
|
||||
outletGuard: culvert.hidden_pipe
|
||||
? undefined
|
||||
: {
|
||||
edge: outletInfo.edge,
|
||||
outward: outletInfo.outward,
|
||||
height: outletWallSpec.height,
|
||||
toeOffset: slopeToeOffset(
|
||||
designAt,
|
||||
groundAt,
|
||||
outletInfo.edge.offset_m,
|
||||
outletInfo.limit,
|
||||
),
|
||||
limitOffset: outletInfo.limit,
|
||||
lean: outletWallSpec.lean,
|
||||
},
|
||||
});
|
||||
appliedAdjust.inlet.x = placed.x;
|
||||
appliedAdjust.inlet.d = placed.d;
|
||||
@@ -412,7 +390,7 @@ export function computeCulvertLayout(
|
||||
? outletZeroInvert - ((offset - outletZeroAnchor) * outletInfo.outward) / FILL_SLOPE_RATIO_MIN
|
||||
: invertAt(offset);
|
||||
if (designAt) {
|
||||
// 4축 배치(유입과 동일 풀이) + 유출 전용: 매몰-무교차 자리 금지(2026-08-22 ③).
|
||||
// 4축 배치(유입과 동일 풀이).
|
||||
const placed = placePipeWall({
|
||||
autoOffset: outletZeroAnchor,
|
||||
outward: outletInfo.outward,
|
||||
@@ -423,9 +401,6 @@ export function computeCulvertLayout(
|
||||
invertCap: culvert.hidden_pipe ? Number.POSITIVE_INFINITY : inlet.elevation,
|
||||
adjust: adjOutlet,
|
||||
defaultD: PIPE_WALL_DEFAULT_RUN_M,
|
||||
groundAt,
|
||||
limitOffset: outletInfo.limit,
|
||||
requireCrossing: true,
|
||||
});
|
||||
appliedAdjust.outlet.x = placed.x;
|
||||
appliedAdjust.outlet.d = placed.d;
|
||||
|
||||
@@ -8,7 +8,6 @@ import type { SectionSample } from "./B06_Section_Api_Fetch";
|
||||
import {
|
||||
FILL_MIN_RISE_M,
|
||||
FILL_SLOPE_MAX_LENGTH_M,
|
||||
PIPE_CONNECT_GRADE,
|
||||
FILL_SLOPE_RATIO_MIN,
|
||||
REVET_HEIGHT_STEP_M,
|
||||
REVET_LEAN_RATIO,
|
||||
@@ -21,7 +20,6 @@ import type {
|
||||
PipeEnd,
|
||||
WallAdjust,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import { slopedCrossing } from "./B06_Section_UI_Cross_Culvert_Extra";
|
||||
|
||||
/** 기슭막이 하단선 폭(m) — 사다리꼴 밑변. 자리 기준(하단선 중점) 환산에 쓴다.
|
||||
* `lean` = 그 벽의 전면 기울기(표준경사 · `revetWallSpec().lean`). */
|
||||
@@ -370,8 +368,7 @@ export interface PipeWallPlacement {
|
||||
* 지점**(벽 상단 = 노견)의 자리·invert. d는 그 점 기준 절대값(null = 기본
|
||||
* defaultD, 경사길이 4m 지점), 위 한계 = 성토선 0 지점(d ≥ 0)·invert 상한.
|
||||
* · x = 노견 연장 평행이동(안쪽 금지).
|
||||
* · requireCrossing(유출): 매몰인데 0도 절토선이 원지반과 다시 못 만나는 자리는
|
||||
* 금지 — 위쪽으로 0.05m씩 되돌린다(상하 먼저, 다음 좌우).
|
||||
* · 원지반은 자리를 막지 않는다 — 묻히든 뜨든 입력 그대로(54-45).
|
||||
*/
|
||||
export function placePipeWall(input: {
|
||||
/** 성토선 0 지점(벽 상단 = 노견)의 벽 자리(하단선 중점 offset). */
|
||||
@@ -386,37 +383,14 @@ export function placePipeWall(input: {
|
||||
adjust: WallAdjust;
|
||||
/** d 미지정(null) 시의 기본 자리 — 성토사면 경사길이 4m 지점의 수평 성분. */
|
||||
defaultD: number;
|
||||
groundAt: (offset: number) => number;
|
||||
limitOffset: number;
|
||||
requireCrossing: boolean;
|
||||
}): PipeWallPlacement {
|
||||
const top0 = input.baseElevation0 + input.height;
|
||||
const dLow = Math.max(
|
||||
-FILL_SLOPE_RATIO_MIN * (input.edgeElevation - top0),
|
||||
-FILL_SLOPE_RATIO_MIN * (input.invertCap - (top0 - input.height)),
|
||||
);
|
||||
let d = Math.max(input.adjust.d ?? input.defaultD, Math.ceil(dLow * 10) / 10);
|
||||
let x = Math.max(input.adjust.x, 0);
|
||||
if (input.requireCrossing) {
|
||||
const placeable = (anchorOffset: number, invertE: number): boolean =>
|
||||
invertE >= input.groundAt(anchorOffset) - 0.01 ||
|
||||
slopedCrossing(
|
||||
{ offset: anchorOffset, elevation: invertE },
|
||||
input.outward,
|
||||
-PIPE_CONNECT_GRADE,
|
||||
input.groundAt,
|
||||
input.limitOffset,
|
||||
) !== null;
|
||||
for (let pass = 0; pass < 400; pass += 1) {
|
||||
const anchorOffset = input.autoOffset + input.outward * (x + d);
|
||||
if (placeable(anchorOffset, top0 - d / FILL_SLOPE_RATIO_MIN - input.height)) break;
|
||||
if (Math.abs(d) > 1e-9) d = d > 0 ? Math.max(0, d - 0.05) : Math.min(0, d + 0.05);
|
||||
else if (x > 1e-9) x = Math.max(0, x - 0.05);
|
||||
else break;
|
||||
}
|
||||
d = Math.round(d * 10) / 10;
|
||||
x = Math.round(x * 10) / 10;
|
||||
}
|
||||
const d = Math.max(input.adjust.d ?? input.defaultD, Math.ceil(dLow * 10) / 10);
|
||||
const x = Math.max(input.adjust.x, 0);
|
||||
const topElevation = top0 - d / FILL_SLOPE_RATIO_MIN;
|
||||
return {
|
||||
x,
|
||||
@@ -427,92 +401,6 @@ export function placePipeWall(input: {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 유입 invert를 그 높이로 내렸을 때 **유출 벽이 성립하는가**(2026-08-22 사용자 확정).
|
||||
* 유입이 유출보다 높아야 하므로 유입을 내리면 유출도 따라 내려간다 — 그런데 유출이
|
||||
* 원지반에 묻히고 3도 절토선이 지반과 다시 만나지 못하면 그 자리는 둘 수 없다.
|
||||
* 이 경우 **유입도 더 내려가면 안 된다**(종전에는 유입만 내려가 유출이 깨졌다).
|
||||
*/
|
||||
export function outletReceivable(input: {
|
||||
inletElevation: number;
|
||||
edge: { offset_m: number; elevation_m: number };
|
||||
outward: number;
|
||||
height: number;
|
||||
toeOffset: number;
|
||||
limitOffset: number;
|
||||
groundAt: (offset: number) => number;
|
||||
/** 유출 벽 전면 기울기(표준경사). */
|
||||
lean?: number;
|
||||
}): boolean {
|
||||
const invertAt = (offset: number): number =>
|
||||
Math.min(input.groundAt(offset), input.inletElevation);
|
||||
const auto =
|
||||
minShoulderWallOffset(
|
||||
input.edge,
|
||||
input.outward,
|
||||
input.height,
|
||||
invertAt,
|
||||
input.toeOffset,
|
||||
input.limitOffset,
|
||||
input.lean,
|
||||
) ?? input.toeOffset;
|
||||
const invert = invertAt(auto);
|
||||
if (invert >= input.groundAt(auto) - 0.01) return true;
|
||||
return (
|
||||
slopedCrossing(
|
||||
{ offset: auto, elevation: invert },
|
||||
input.outward,
|
||||
-PIPE_CONNECT_GRADE,
|
||||
input.groundAt,
|
||||
input.limitOffset,
|
||||
) !== null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 유입 기슭막이 4축 배치 — placePipeWall에 **유출 수용 가능성 가드**를 얹은 것.
|
||||
* 유입을 내리면 유출도 따라 내려가므로(유입 > 유출 조건), 유출이 못 받는 높이면
|
||||
* 유입도 그만큼 못 내려간다(2026-08-22 사용자 확정).
|
||||
*/
|
||||
export function placeInletWall(input: {
|
||||
autoOffset: number;
|
||||
outward: number;
|
||||
height: number;
|
||||
baseElevation0: number;
|
||||
edgeElevation: number;
|
||||
invertCap: number;
|
||||
adjust: WallAdjust;
|
||||
defaultD: number;
|
||||
groundAt: (offset: number) => number;
|
||||
limitOffset: number;
|
||||
/** 유출 쪽 수용 검사 인자 — 없으면 가드 없이 배치한다. */
|
||||
outletGuard?: {
|
||||
edge: { offset_m: number; elevation_m: number };
|
||||
outward: number;
|
||||
height: number;
|
||||
toeOffset: number;
|
||||
limitOffset: number;
|
||||
lean?: number;
|
||||
};
|
||||
/** 매몰-무교차 자리 금지(유출 벽 규칙) — 독립 기슭막이는 유입도 같은 규칙을 탄다. */
|
||||
requireCrossing?: boolean;
|
||||
}): PipeWallPlacement {
|
||||
const place = (adjust: WallAdjust): PipeWallPlacement =>
|
||||
placePipeWall({ ...input, adjust, requireCrossing: input.requireCrossing === true });
|
||||
let placed = place(input.adjust);
|
||||
const guard = input.outletGuard;
|
||||
if (!guard) return placed;
|
||||
const receivable = (invert: number): boolean =>
|
||||
outletReceivable({ inletElevation: invert, groundAt: input.groundAt, ...guard });
|
||||
for (let pass = 0; pass < 400 && placed.d > 1e-9 && !receivable(placed.invert); pass += 1) {
|
||||
placed = place({
|
||||
...input.adjust,
|
||||
d: Math.max(0, Math.round((placed.d - 0.05) * 100) / 100),
|
||||
});
|
||||
}
|
||||
return placed;
|
||||
}
|
||||
|
||||
/** 관 축·법선·양 끝단 꼭짓점을 한 번에 푸는 도구(본체 700줄 제한 — 2026-08-22). */
|
||||
export interface PipeAxisSolver {
|
||||
axis: OffsetPoint;
|
||||
|
||||
@@ -20,7 +20,6 @@ import { showToast } from "@ui/ui_template_elements";
|
||||
import type { CrossSection } from "./B06_Section_Api_Fetch";
|
||||
import {
|
||||
FILL_SLOPE_RATIO_MIN,
|
||||
PIPE_CONNECT_GRADE,
|
||||
PIPE_WALL_DEFAULT_RUN_M,
|
||||
REVET_EMBED_DEPTH_M,
|
||||
REVET_THICKNESS_M,
|
||||
@@ -28,7 +27,7 @@ import {
|
||||
import { outlineLeanOf } from "./B06_Section_UI_Cross_Lean";
|
||||
import type { RevetMaterial } from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
import { fillWallBaseWidth, groundInterpolator } from "./B06_Section_UI_Cross_Culvert_Solve";
|
||||
import { buildExtrasAt, slopedCrossing } from "./B06_Section_UI_Cross_Culvert_Extra";
|
||||
import { buildExtrasAt } from "./B06_Section_UI_Cross_Culvert_Extra";
|
||||
import {
|
||||
ZERO_ADJUST,
|
||||
type CulvertDesignTrim,
|
||||
@@ -206,36 +205,12 @@ export function computeRevetmentLayout(
|
||||
* (확장량 t는 아래 폴리라인에서 역산 — t ≥ 0이 각도 유지의 한계다).
|
||||
*/
|
||||
const defaultDrop = autoRun / FILL_SLOPE_RATIO_MIN;
|
||||
/** 그 자리에 벽을 세울 수 있나 — 매몰(원지반 아래 daylight 없음) 금지. */
|
||||
const placeable = (xShift: number, drop: number): boolean => {
|
||||
const anchor = autoOffset + outward * (autoRun + xShift);
|
||||
const invert = edge.elevation_m - drop - height;
|
||||
return (
|
||||
invert >= groundAt(anchor) - 0.01 ||
|
||||
slopedCrossing(
|
||||
{ offset: anchor, elevation: invert },
|
||||
outward,
|
||||
-PIPE_CONNECT_GRADE,
|
||||
groundAt,
|
||||
limit,
|
||||
) !== null
|
||||
);
|
||||
};
|
||||
const requestedX = Math.max(adjust?.x ?? 0, 0);
|
||||
/** 각도(1:1.2)를 지키려면 그만큼은 밖으로 나가 있어야 하는 최소 수평 자리. */
|
||||
const xFloor = (drop: number): number => Math.max(0, FILL_SLOPE_RATIO_MIN * drop - autoRun);
|
||||
let appliedDrop = Math.max(adjust?.d ?? defaultDrop, 0);
|
||||
let appliedX = Math.max(requestedX, xFloor(appliedDrop));
|
||||
for (let pass = 0; pass < 400 && !placeable(appliedX, appliedDrop); pass += 1) {
|
||||
// 매몰 자리 — 요청을 자동 자리 쪽으로 0.05m씩 되돌린다(상하 먼저, 다음 좌우).
|
||||
if (appliedDrop > 1e-9) {
|
||||
appliedDrop = Math.max(0, appliedDrop - 0.05);
|
||||
appliedX = Math.max(Math.min(appliedX, requestedX), xFloor(appliedDrop));
|
||||
} else if (appliedX > 1e-9) appliedX = Math.max(0, appliedX - 0.05);
|
||||
else break;
|
||||
}
|
||||
appliedX = Math.round(appliedX * 10) / 10;
|
||||
appliedDrop = Math.round(appliedDrop * 10) / 10;
|
||||
// 원지반은 자리를 막지 않는다 — 묻히든 뜨든 입력 그대로(54-45). 노면 위 금지 = d ≥ 0.
|
||||
const appliedDrop = Math.round(Math.max(adjust?.d ?? defaultDrop, 0) * 10) / 10;
|
||||
const appliedX = Math.round(Math.max(requestedX, xFloor(appliedDrop)) * 10) / 10;
|
||||
|
||||
const anchorOffset = autoOffset + outward * (autoRun + appliedX);
|
||||
const baseElevation = edge.elevation_m - appliedDrop - height;
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
"""원지반 잠금 풀기(54-45) — 흙막이 자리 · 다단 · 높이를 원지반이 막지 않음.
|
||||
|
||||
남는 규칙: 벽 윗면 노면 위 금지(d ≥ 0) · 관 보호공 흙막이 m 단위 자리(관 길이 정수).
|
||||
서버 면적은 같은 TS 를 Node 로 돌림(`B06_Section_Server_Calc_Node.ts`) — 번들도 같은 판인지 봄.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
B06 = PROJECT_ROOT / "B06_Section"
|
||||
TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
|
||||
BUNDLE = PROJECT_ROOT / "config" / "server_calc_node" / "B06_Section_Server_Calc_Node.js"
|
||||
|
||||
_RUNNER = """
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { placePipeWall } from "./B06_Section_UI_Cross_Culvert_Solve.js";
|
||||
import { buildOutletExtras } from "./B06_Section_UI_Cross_Culvert_Extra.js";
|
||||
const pipe = (d, x) => placePipeWall({
|
||||
autoOffset: 10, outward: 1, height: 2, baseElevation0: 98, edgeElevation: 100,
|
||||
invertCap: Number.POSITIVE_INFINITY, adjust: { x, d, h: null, m: null }, defaultD: 4.8,
|
||||
});
|
||||
const extras = (ground, d) => buildOutletExtras({
|
||||
start: { offset: 10, elevation: 95 }, startBottomElevation: 94.5, outward: 1,
|
||||
groundAt: () => ground, limitOffset: 40,
|
||||
adjusts: [{ x: 0, d, h: 2, m: null }, { x: 0, d, h: 2, m: null }],
|
||||
});
|
||||
const buried = extras(100, 1);
|
||||
const high = extras(80, 3);
|
||||
writeFileSync(process.argv[2], JSON.stringify({
|
||||
deep: pipe(6, 2), up: pipe(-5, 0),
|
||||
buriedCount: buried.walls.length, buriedD: buried.appliedAdjusts.map((a) => a.d),
|
||||
highCount: high.walls.length, highD: high.appliedAdjusts.map((a) => a.d),
|
||||
}));
|
||||
"""
|
||||
|
||||
|
||||
@pytest.mark.skipif(not TSC.is_file(), reason="프론트엔드 의존성 미설치")
|
||||
def test_원지반이_자리_다단을_막지_않고_노면_위만_막음(tmp_path: Path) -> None:
|
||||
out = tmp_path / "js"
|
||||
for name in ("Culvert_Solve", "Culvert_Extra"):
|
||||
subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
[
|
||||
"node",
|
||||
str(TSC),
|
||||
str(B06 / f"B06_Section_UI_Cross_{name}.ts"),
|
||||
"--outDir",
|
||||
str(out),
|
||||
"--module",
|
||||
"esnext",
|
||||
"--target",
|
||||
"es2022",
|
||||
"--moduleResolution",
|
||||
"bundler",
|
||||
"--skipLibCheck",
|
||||
"--ignoreConfig",
|
||||
],
|
||||
cwd=str(PROJECT_ROOT),
|
||||
capture_output=True,
|
||||
)
|
||||
for emitted in out.rglob("*.js"):
|
||||
emitted.write_text(
|
||||
re.sub(r'(from\s+"\./[^"]+)"', r'\1.js"', emitted.read_text(encoding="utf-8")),
|
||||
encoding="utf-8",
|
||||
)
|
||||
base = next(path.parent for path in out.rglob("B06_Section_UI_Cross_Culvert_Solve.js"))
|
||||
(base / "runner.mjs").write_text(_RUNNER, encoding="utf-8")
|
||||
result = tmp_path / "out.json"
|
||||
subprocess.run( # noqa: S603
|
||||
["node", str(base / "runner.mjs"), str(result)], check=True, capture_output=True
|
||||
)
|
||||
got = json.loads(result.read_text(encoding="utf-8"))
|
||||
|
||||
assert (got["deep"]["d"], got["deep"]["x"]) == (6, 2), "관 흙막이 깊이 · 좌우가 되돌아감"
|
||||
assert got["up"]["d"] == pytest.approx(0), "관 흙막이 윗면이 노면 위로 올라감"
|
||||
assert got["up"]["topElevation"] == pytest.approx(100), "노면 멈춤 자리 틀림"
|
||||
assert got["buriedCount"] == 2, "원지반에 묻힌 다단이 끊김"
|
||||
assert got["buriedD"] == [1, 1], "묻힌 다단의 높이가 되돌아감"
|
||||
assert got["highCount"] == 2, "원지반 위 다단이 끊김"
|
||||
assert got["highD"] == [3, 3], "뜬 다단의 높이가 되돌아감"
|
||||
|
||||
|
||||
def test_화면_서버_같은_판() -> None:
|
||||
for name in ("Culvert_Solve", "Culvert_Geom", "Revetment"):
|
||||
source = (B06 / f"B06_Section_UI_Cross_{name}.ts").read_text(encoding="utf-8")
|
||||
assert "requireCrossing" not in source, f"{name} 에 매몰 자리 금지가 남음"
|
||||
assert "outletReceivable" not in source, f"{name} 에 유출 수용 가드가 남음"
|
||||
assert "const placeable" not in (B06 / "B06_Section_UI_Cross_Revetment.ts").read_text(
|
||||
encoding="utf-8"
|
||||
), "사면안정 흙막이 · 옹벽 매몰 자리 금지가 남음"
|
||||
assert "const placeable" not in (B06 / "B06_Section_UI_Cross_Culvert_Extra.ts").read_text(
|
||||
encoding="utf-8"
|
||||
), "추가 흙막이 매몰 자리 금지가 남음"
|
||||
bundle = BUNDLE.read_text(encoding="utf-8")
|
||||
assert "outletReceivable" not in bundle and "requireCrossing" not in bundle, (
|
||||
"서버 번들이 옛 판 — npm run build:server-calc"
|
||||
)
|
||||
Reference in New Issue
Block a user