Merge remote-tracking branch 'origin/sub_laptop_7' into main_laptop_1
This commit is contained in:
@@ -246,7 +246,7 @@ export function renderDesignPage(root: HTMLElement): MountedPage {
|
||||
slot.show();
|
||||
applyView(view);
|
||||
void reconcilePagePipeline(projectId); // 들어올 때 입력(세션 · 가게) 대 묶음 맞춤(365-2)
|
||||
} else mountView(view, true);
|
||||
} else void mountView(view, true).ready.then(() => reconcilePagePipeline(projectId)); // 첫 마운트 뒤도(365-5)
|
||||
// 검증용 — 보기 전환에 든 시간(ms)과 횟수를 화면 밖에서 수치로 본다.
|
||||
const probe = window as unknown as { __designPage?: { view: string; switchMs: number[] } };
|
||||
probe.__designPage ??= { view, switchMs: [] };
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* 암 경사 · 소단 깊이) — 노선 초안 동안 B06 고침은 저장 상세에 안 얹혀도 입력에 들어감(365-2 · 365-5). 부를 때마다 모음.
|
||||
* ========================================================================== */
|
||||
|
||||
import { readState } from "../b_page_state";
|
||||
import { readState, readStateRaw } from "../b_page_state";
|
||||
import type { CrossSection, SectionDetailResponse } from "../../B06_Section/B06_Section_Api_Fetch";
|
||||
import {
|
||||
effectiveStandardCross,
|
||||
@@ -41,12 +41,27 @@ const SPEC_KEYS = [
|
||||
/** 같은 측점으로 보는 누가거리 차(m) — 측점 열쇠 글 자릿수(0.01 m). */
|
||||
const SAME_M = 0.01;
|
||||
|
||||
/** 세션 저장 상세(노선 초안 동안 안 덮음 · 365-1) — 글이 같으면 앞 해석 그대로. */
|
||||
let storedText: { text: string | null; detail: SectionDetailResponse | null } | null = null;
|
||||
function storedDetail(projectId: string, routeId: number | null): SectionDetailResponse | null {
|
||||
const text = routeId === null ? null : readStateRaw("section-detail", projectId, routeId);
|
||||
if (storedText?.text !== text) storedText = { text, detail: text ? JSON.parse(text) : null };
|
||||
return storedText.detail;
|
||||
}
|
||||
|
||||
/** 짧은 지문(FNV-1a) — 제원 묶음이 같은지만 견줌. */
|
||||
function digest(text: string): string {
|
||||
let hash = 0x811c9dc5;
|
||||
for (let i = 0; i < text.length; i += 1) hash = Math.imul(hash ^ text.charCodeAt(i), 0x01000193);
|
||||
return `${text.length}:${hash >>> 0}`;
|
||||
}
|
||||
|
||||
const finite = (value: unknown): number | null =>
|
||||
typeof value === "number" && Number.isFinite(value) ? value : null;
|
||||
|
||||
/** 측점 하나의 넘기는 입력(cross 단계 `CrossStationInput` 꼴). */
|
||||
function carried(section: CrossSection): Record<string, unknown> {
|
||||
const design = (section.design ?? {}) as unknown as Record<string, unknown>;
|
||||
function carried(section: CrossSection, saved?: CrossSection["design"]): Record<string, unknown> {
|
||||
const design = (saved ?? section.design ?? {}) as unknown as Record<string, unknown>;
|
||||
const berm = design.berm as
|
||||
{ width_m: number; interval_m: number; slope_deg: number; depth_m?: number } | undefined;
|
||||
const cutSlope = finite(design.cut_slope_ratio_user);
|
||||
@@ -55,6 +70,8 @@ function carried(section: CrossSection): Record<string, unknown> {
|
||||
...(typeof design.ground_type === "string" ? { groundType: design.ground_type } : {}),
|
||||
...(typeof design.ditch_type === "string" ? { ditchType: design.ditch_type } : {}),
|
||||
ditchChoice: typeof design.ditch_choice === "boolean" ? design.ditch_choice : null,
|
||||
// 옛 저장분 측구 결과 — 다시 셈 창구와 같은 규칙(자동과 다를 때만 선택 · 들어온 화면 무관 · 365-5)
|
||||
ditchEnabled: typeof design.ditch_enabled === "boolean" ? design.ditch_enabled : null,
|
||||
twoStageSlope: Boolean(design.two_stage_slope),
|
||||
rockBoundaryOffsetM: finite(design.rock_boundary_offset_m),
|
||||
cutSlopeRatio: cutSlope !== null && cutSlope > 0 ? cutSlope : null,
|
||||
@@ -123,6 +140,18 @@ export function pipelineCrossSource(
|
||||
if (!detail || !standard) return null;
|
||||
const byKey: CrossInput["byKey"] = {};
|
||||
const specs = new Map<string, SectionSpecs>();
|
||||
// 설계 칸 = 세션에 남긴 저장 상세(초안 · 다시 셈이 안 고친 정본 — 들어온 화면 · 고친 차례 무관 · 365-5) ·
|
||||
// 시설 제원 = 화면 상세(초안 형상을 얹은 것)
|
||||
const stored = storedDetail(projectId, routeId);
|
||||
const storedAt = (chainageM: number) =>
|
||||
stored?.cross_sections.find((row) => Math.abs(row.chainage_m - chainageM) < SAME_M)?.design;
|
||||
const put = (key: string, section: CrossSection, at = section.chainage_m): void => {
|
||||
byKey[key] = carried(section, storedAt(at));
|
||||
const own: Record<string, unknown> = {};
|
||||
for (const name of SPEC_KEYS) if (section[name] !== undefined) own[name] = section[name];
|
||||
specs.set(key, own as SectionSpecs);
|
||||
};
|
||||
const strays: CrossSection[] = [];
|
||||
for (const section of detail.cross_sections) {
|
||||
let best: PipelineStation | null = null;
|
||||
for (const station of savedStations) {
|
||||
@@ -130,18 +159,29 @@ export function pipelineCrossSource(
|
||||
if (gap < SAME_M && (!best || gap < Math.abs(best.chainageM - section.chainage_m)))
|
||||
best = station;
|
||||
}
|
||||
if (!best || byKey[best.key]) continue;
|
||||
byKey[best.key] = carried(section);
|
||||
const own: Record<string, unknown> = {};
|
||||
for (const name of SPEC_KEYS) if (section[name] !== undefined) own[name] = section[name];
|
||||
specs.set(best.key, own as SectionSpecs);
|
||||
if (best && !byKey[best.key]) put(best.key, section);
|
||||
else if (!best && section.kind === "irregular") strays.push(section);
|
||||
}
|
||||
// 초안 자리로 미리 뜬 관 · 구조물 측점(초안 둔 채 열기 — 상세가 초안 누가거리) = 남은 저장 시설 측점과 차례로 짝 ·
|
||||
// 수가 다르면 안 짝지음(349-0d 와 같은 규칙 · 들어온 화면 무관 같은 제원 · 365-5)
|
||||
const left = savedStations.filter(
|
||||
(station) => (station.kind === "pipe" || station.kind === "structure") && !byKey[station.key],
|
||||
);
|
||||
left.sort((a, b) => a.chainageM - b.chainageM);
|
||||
if (strays.length === left.length)
|
||||
strays.forEach((section, i) => put(left[i].key, section, left[i].chainageM));
|
||||
if (routeId !== null)
|
||||
for (const [key, entry] of Object.entries(sessionCarried(projectId, routeId)))
|
||||
byKey[key] = { ...byKey[key], ...entry };
|
||||
return {
|
||||
// 암 경계 기본값 = 서버 config(화면이 문맥에서 기억) — 입력 없는 측점(초안 새 측점)도 서버처럼(349-0p)
|
||||
input: { standard, byKey, rockBoundaryDefaultM: readRockBoundaryDefault(projectId) },
|
||||
// 제원 지문 — 보기가 나중에 얹은 시설 제원도 입력 차이로(고리가 다시 셈 · 들어온 화면 무관 같은 면적 · 365-5)
|
||||
input: {
|
||||
standard,
|
||||
byKey,
|
||||
rockBoundaryDefaultM: readRockBoundaryDefault(projectId),
|
||||
specsKey: digest(JSON.stringify([...specs])),
|
||||
},
|
||||
specs,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ import type {
|
||||
import { useDesignStore, type DesignStoreValues } from "./design_store";
|
||||
import {
|
||||
followStructures,
|
||||
placeOnLine,
|
||||
placedOnLine as placed,
|
||||
type FollowingStructure,
|
||||
} from "../../common_util/common_util_route_follow";
|
||||
import { fetchDetailPipePoints } from "../../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
@@ -52,6 +52,7 @@ import type { SectionDetailResponse } from "../../B06_Section/B06_Section_Api_Fe
|
||||
import { createCrossStage } from "../../B06_Section/logic/B06_Section_Cross_Stage";
|
||||
import {
|
||||
emptyEdits,
|
||||
toAlignmentBase,
|
||||
type AlignmentBase,
|
||||
type AlignmentEdits,
|
||||
type ProfileAlignment,
|
||||
@@ -303,6 +304,12 @@ function createPagePipeline(
|
||||
emptyEdits()
|
||||
);
|
||||
};
|
||||
const storedPvi = () => {
|
||||
const stored = canon?.detail && readAlignment(canon.detail.longitudinal);
|
||||
return stored ? toAlignmentBase(stored).basePvi : null;
|
||||
};
|
||||
const baseMoved = () =>
|
||||
!dropping && !pipe.input("route") && !same(savedSource().base, pipe.output("planline"));
|
||||
/** 횡단 입력 — 부를 때마다 저장 상세 + 세션 고침 · 표준을 모음(상세 객체마다 기억하면 고침이 안 감 · 365-2). */
|
||||
const crossNow = (): PipelineCrossSource | null => {
|
||||
const routeId = canon?.routeId;
|
||||
@@ -329,16 +336,6 @@ function createPagePipeline(
|
||||
};
|
||||
},
|
||||
});
|
||||
/** 좌표 없는 관 · 구조물(세션 초안 — 배수유역도 · 구조물 목록이 정본 PUT 꼴로 씀)은 안 옮긴 것 = 정본 좌표 ·
|
||||
* 그 밖 = 앞 노선 위 그 누가거리 자리에서 따라감 — 안 붙이면 측점이 그대로 남음(349-0u). */
|
||||
const placed = (list: unknown, line: Vertex[], saved: FollowingStructure[]): unknown =>
|
||||
Array.isArray(list)
|
||||
? placeOnLine(
|
||||
list as FollowingStructure[],
|
||||
line.map(([x, y]) => ({ x, y })),
|
||||
saved,
|
||||
)
|
||||
: list;
|
||||
/** 다시 셈 창구가 낸 cross 출력(365-4) — 엔진 재사용에서 뺌. */
|
||||
const savedOut = new WeakSet<object>();
|
||||
const pipe = createDesignPipeline({
|
||||
@@ -362,7 +359,7 @@ function createPagePipeline(
|
||||
},
|
||||
stations: createStationsStage({ intervalM: options.stationIntervalM, types: () => types }),
|
||||
ground: createGroundStage(groundDeps),
|
||||
planline: createPlanlineStage({ savedBase: () => savedSource().base, savedLine }),
|
||||
planline: createPlanlineStage({ savedBase: () => savedSource().base, savedLine, storedPvi }),
|
||||
profile: profileStage,
|
||||
// 단면 유형 = 새 지형 상단측 · 소단 = 지금 구조물(365-5) · 저장 노선 = 다시 셈 창구 한 벌(365-4) · 엔진은
|
||||
// 창구 출력을 앞 결과로 안 이어 씀(초안 = 새로고침)
|
||||
@@ -677,7 +674,8 @@ function createPagePipeline(
|
||||
},
|
||||
restart() {
|
||||
if (!started) return start();
|
||||
if (canonChanged()) void loadPlan().then(start); // 갈아끼움 · 같은 상세 다시 그림은 안 셈
|
||||
if (canonChanged()) return void loadPlan().then(start); // 갈아끼움 · 같은 상세 다시 그림은 안 셈
|
||||
if (baseMoved()) start(); // 저장 노선 기준이 바뀜(보기가 늦게 심은 측점 · 되돌린 뒤 · 초안 빼고 · 365-5)
|
||||
},
|
||||
earthwork(context) {
|
||||
earthwork = context;
|
||||
|
||||
@@ -47,6 +47,8 @@ export interface CrossInput {
|
||||
standard: unknown;
|
||||
byKey: Record<string, Record<string, unknown>>;
|
||||
rockBoundaryDefaultM?: number | null;
|
||||
/** 얹힌 시설 제원 지문(365-5 · 단계는 안 씀 — 바뀌면 고리가 다시 셈). */
|
||||
specsKey?: string;
|
||||
}
|
||||
|
||||
const mean = (values: number[]): number => values.reduce((sum, v) => sum + v, 0) / values.length;
|
||||
|
||||
@@ -109,6 +109,8 @@ export interface PlanlineDeps {
|
||||
savedBase(): AlignmentBase | null;
|
||||
/** 저장 노선(그린 선) — 바뀐 구간을 가름. */
|
||||
savedLine(): Vertex[] | null;
|
||||
/** 노선 초안 첫 계획선 꼭짓점 = 서버 저장 종단 — 보기가 심은 측점(초안 자리 관 등)에 안 흔들림(365-5) · 없으면 `savedBase`. */
|
||||
storedPvi?(): AlignmentBase["basePvi"] | null;
|
||||
}
|
||||
|
||||
export function createPlanlineStage(deps: PlanlineDeps): StageFn<"planline"> {
|
||||
@@ -133,12 +135,11 @@ export function createPlanlineStage(deps: PlanlineDeps): StageFn<"planline"> {
|
||||
}) ?? [];
|
||||
const savedLine = deps.savedLine();
|
||||
const span = savedLine ? changedSpan(xy(savedLine), xy(route.line)) : null;
|
||||
const savedPvi = deps.storedPvi?.() ?? saved.basePvi;
|
||||
return {
|
||||
policy: saved.policy,
|
||||
onlyExplicitCurves: saved.onlyExplicitCurves,
|
||||
basePvi: span
|
||||
? carryProfileEdits(saved.basePvi, null, span, groundPvi).basePvi
|
||||
: saved.basePvi,
|
||||
basePvi: span ? carryProfileEdits(savedPvi, null, span, groundPvi).basePvi : savedPvi,
|
||||
chainage: profile.chainage,
|
||||
ground: profile.ground,
|
||||
stations: ground.stations.map((station) => ({
|
||||
|
||||
@@ -178,3 +178,17 @@ export function followStructures<S extends FollowingStructure>(
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** 묶음 follow 입력 꼴 — 목록이면 `placeOnLine`(앞 노선 꼭짓점 `[x, y]`) · 아니면(null 등) 그대로. */
|
||||
export const placedOnLine = (
|
||||
list: unknown,
|
||||
line: ReadonlyArray<[number, number]>,
|
||||
saved: FollowingStructure[],
|
||||
): unknown =>
|
||||
Array.isArray(list)
|
||||
? placeOnLine(
|
||||
list as FollowingStructure[],
|
||||
line.map(([x, y]) => ({ x, y })),
|
||||
saved,
|
||||
)
|
||||
: list;
|
||||
|
||||
@@ -465,8 +465,75 @@ async function run() {
|
||||
depthAfter: bermAfter.find((row) => row[0] === inside[0])[3],
|
||||
runs: runs().length - mark,
|
||||
};
|
||||
const draftCtx = {
|
||||
inputs: { route: bPipe.input("route") },
|
||||
out: { ground: bPipe.output("ground"), route: bPipe.output("route") },
|
||||
};
|
||||
pageMod.disposePagePipeline(B);
|
||||
page.clearDrafts(B);
|
||||
|
||||
// ⑫ 들어온 화면 무관(365-5) — 설계 칸 = 세션 저장 상세 · 초안 자리로 미리 뜬 시설 측점 = 차례 짝 · 제원 지문 ·
|
||||
// 옛 측구 결과(ditch_enabled)도 실음 · 초안 첫 계획선 꼭짓점 = 서버 저장 종단
|
||||
const crossMod = load("A00_Common", "design", "design_pipeline_cross.ts");
|
||||
const C = "proj-365d";
|
||||
const sec = (chainage_m, kind, design, extra = {}) => ({ chainage_m, kind, design, ...extra });
|
||||
page.writeState(
|
||||
"section-detail",
|
||||
{
|
||||
longitudinal: {},
|
||||
cross_sections: [
|
||||
sec(0, "bp", { ground_type: "soil" }),
|
||||
sec(20, "regular", { ground_type: "soil", ditch_choice: null, ditch_enabled: false }),
|
||||
sec(28, "irregular", { ground_type: "soil" }),
|
||||
sec(40, "ep", { ground_type: "soil" }),
|
||||
],
|
||||
},
|
||||
C,
|
||||
7,
|
||||
);
|
||||
const screen = {
|
||||
longitudinal: {},
|
||||
cross_sections: [
|
||||
sec(0, "bp", { ground_type: "soil" }),
|
||||
// 다시 셈이 옛 측구 결과를 선택으로 굳힌 화면 상세 — 설계 칸은 세션 저장 상세가 이김
|
||||
sec(20, "regular", { ground_type: "soil", ditch_choice: false, ditch_enabled: true }),
|
||||
sec(27, "irregular", { ground_type: "soil" }, { culvert: { type: "pipe", diameter_m: 0.8 } }),
|
||||
sec(40, "ep", { ground_type: "soil" }),
|
||||
],
|
||||
};
|
||||
const st = (key, chainageM, kind) => ({ key, chainageM, kind });
|
||||
const savedList = [
|
||||
st("end:BP", 0, "end"),
|
||||
st("grid:1", 20, "grid"),
|
||||
st("pipe:p28", 28, "pipe"),
|
||||
st("end:EP", 40, "end"),
|
||||
];
|
||||
const source = crossMod.pipelineCrossSource(C, screen, savedList, 7);
|
||||
const bare = crossMod.pipelineCrossSource(
|
||||
C,
|
||||
{ ...screen, cross_sections: screen.cross_sections.filter((x) => x.chainage_m !== 27) },
|
||||
savedList,
|
||||
7,
|
||||
);
|
||||
// 보기가 심은 측점으로 흔들린 저장 기준(꼭짓점 +5 m) 대 서버 저장 종단 — 초안 첫 계획선은 저장 종단 쪽
|
||||
const shaken = {
|
||||
...savedBase,
|
||||
basePvi: savedBase.basePvi.map((node) => ({ ...node, elevation_m: node.elevation_m + 5 })),
|
||||
};
|
||||
const planlineOf = (storedPvi) =>
|
||||
grade.createPlanlineStage({ savedBase: () => shaken, savedLine: () => job.line, storedPvi })(
|
||||
draftCtx,
|
||||
);
|
||||
const firstPvi = (base) => r3(base.basePvi[0].elevation_m);
|
||||
const viewFree = {
|
||||
grid20: source.input.byKey["grid:1"],
|
||||
pipeSpec: source.specs.get("pipe:p28") ?? null,
|
||||
specsDiffer: source.input.specsKey !== bare.input.specsKey,
|
||||
firstStored: firstPvi(planlineOf(() => savedBase.basePvi)),
|
||||
firstShaken: firstPvi(planlineOf(undefined)),
|
||||
savedFirst: firstPvi(savedBase),
|
||||
};
|
||||
page.clearDrafts(C);
|
||||
return {
|
||||
kept,
|
||||
b05,
|
||||
@@ -479,6 +546,7 @@ async function run() {
|
||||
shiftRuns,
|
||||
sameAfterReload,
|
||||
berm,
|
||||
viewFree,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
- 저장 노선 ▲▼ 한 번 = 횡단 한 벌 — 묶음 cross 가 다시 셈 창구(상세 설계) 한 번 · 종단도 예약 끔.
|
||||
- 중심점 좌우 = 노선 차례는 cross 전에 밀림 → cross 한 번(계획고 맞춤 차례만).
|
||||
- B06 다시 그림 = 겹친 신호(갈아끼움 · 묶음 cross · 초안) 한 번 · profile 차례는 안 그림.
|
||||
못박는 것(365-5)
|
||||
- 들어온 화면 무관 — 설계 칸 = 세션 저장 상세 · 초안 자리 시설 = 차례 짝 · 제원 지문 ·
|
||||
초안 계획선 = 저장 종단.
|
||||
- 초안 소단 = 지금 구조물 구간 · 쪽 깊이 = 세션 고침.
|
||||
|
||||
짝: `helper_365_page_pipeline.cjs`(진짜 TS · 서버 창구만 가짜 · 틀 = 346-2 시험) ·
|
||||
`helper_365_swap_entry.ts` + `helper_365_swap.mjs`(진짜 상세 가게 · vite ssr).
|
||||
@@ -161,6 +165,18 @@ def test_365_5_초안_소단_고침(r):
|
||||
assert b["depthBefore"] is None and b["depthAfter"] == 2.5 and b["runs"] == 1
|
||||
|
||||
|
||||
def test_365_5_들어온_화면_무관(r):
|
||||
v = r["viewFree"]
|
||||
g = v["grid20"]
|
||||
# 설계 칸 = 세션 저장 상세(화면 상세가 굳힌 선택 false 대신 저장값) · 옛 측구 결과도 실음
|
||||
assert g["ditchChoice"] is None and g["ditchEnabled"] is False
|
||||
assert v["pipeSpec"] == {
|
||||
"culvert": {"type": "pipe", "diameter_m": 0.8}
|
||||
} # 초안 자리 관 = 차례 짝
|
||||
assert v["specsDiffer"] # 제원이 바뀌면 입력 지문도 바뀜(고리가 다시 셈)
|
||||
assert v["firstStored"] == v["savedFirst"] and v["firstShaken"] != v["savedFirst"]
|
||||
|
||||
|
||||
def test_365_4_다시_그림_한_번():
|
||||
swap = (ROOT / "B06_Section/B06_Section_UI_Page_Station_Swap.ts").read_text(encoding="utf-8")
|
||||
view = (ROOT / "A00_Common/design/design_view_cross.ts").read_text(encoding="utf-8")
|
||||
|
||||
Reference in New Issue
Block a user