Merge remote-tracking branch 'origin/sub_laptop_4' into main_laptop_1
This commit is contained in:
@@ -63,6 +63,8 @@ export interface SvgZoomPanOptions {
|
||||
initial?: ZoomPanState;
|
||||
/** 처음 · 원복 배율(기본 1) — 플롯 중앙을 붙잡음 · 이보다 작게도 줄일 수 있음(266-1 B06 상세). */
|
||||
homeScale?: number;
|
||||
/** 처음 · 원복 상태를 통째로(295-2 B06 상세 — 중심점 가운데) · 주면 `homeScale` 대신 · 1 보다 작아도 됨. */
|
||||
home?: ZoomPanState;
|
||||
/** 상태가 바뀔 때마다 부른다 — 바깥이 담아 뒀다가 다음 그림에 되돌린다. */
|
||||
onChange?: (state: ZoomPanState) => void;
|
||||
/** 도형이 실제로 그려진 범위. 확대 상태에서 팬 한계가 여기까지 늘어난다. */
|
||||
@@ -88,11 +90,13 @@ export function attachSvgZoomPan(options: SvgZoomPanOptions): ZoomPanHandle {
|
||||
};
|
||||
const maxScale = options.maxScale ?? 8;
|
||||
const homeScale = Math.min(Math.max(options.homeScale ?? 1, 1), maxScale);
|
||||
const home: ZoomPanState = {
|
||||
const home: ZoomPanState = options.home ?? {
|
||||
scale: homeScale,
|
||||
tx: (plot.x + plot.width / 2) * (1 - homeScale),
|
||||
ty: (plot.y + plot.height / 2) * (1 - homeScale),
|
||||
};
|
||||
/** 축소 한계 — 1배 · 처음 보기가 더 작으면 그만큼. */
|
||||
const minScale = Math.min(1, home.scale);
|
||||
let scale = initial?.scale ?? home.scale;
|
||||
let tx = initial?.tx ?? home.tx;
|
||||
let ty = initial?.ty ?? home.ty;
|
||||
@@ -132,7 +136,7 @@ export function attachSvgZoomPan(options: SvgZoomPanOptions): ZoomPanHandle {
|
||||
|
||||
/** 그 자리를 붙잡고 확대·축소한다 — 버튼은 플롯 중앙을, 휠은 커서 자리를 준다. */
|
||||
const zoomAt = (factor: number, cx: number, cy: number): void => {
|
||||
const next = Math.min(maxScale, Math.max(1, scale * factor));
|
||||
const next = Math.min(maxScale, Math.max(minScale, scale * factor));
|
||||
tx = cx - ((cx - tx) / scale) * next;
|
||||
ty = cy - ((cy - ty) / scale) * next;
|
||||
scale = next;
|
||||
|
||||
@@ -419,7 +419,7 @@ export function attachDesignLinePick(input: {
|
||||
const pieces = visiblePieces(mergeStraightPieces(runs), occluders);
|
||||
if (input.stationId) keysByStation.set(input.stationId, pieceKeys(design, pieces));
|
||||
|
||||
// 판정 선은 구조물 판정면 **아래** — 겹친 자리는 구조물이 이김.
|
||||
// 판정 선은 구조물 판정면 아래에 꽂음 — 카드가 끝에 `stackPickTargets` 로 맨 위에 다시 세움(295-1).
|
||||
const firstStructureHit = kids.find((el) =>
|
||||
el.classList.contains("b06-chart__culvert-revet-hit"),
|
||||
);
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
type ZoomPanState,
|
||||
} from "../A00_Common/b_svg_zoom_pan";
|
||||
import { buildZoomControls } from "./B06_Section_UI_Cross_View_Zoom";
|
||||
import { detailHomeView, layerPoints } from "./B06_Section_UI_Cross_Detail_Home";
|
||||
import { attachDetailTable, designForTable, groupSpan } from "./B06_Section_UI_Cross_Detail_Table";
|
||||
import { createCrossAxes, IDENTITY_VIEW, type CrossAxesOptions } from "./B06_Section_UI_Cross_Axes";
|
||||
import {
|
||||
@@ -194,7 +195,7 @@ export function buildQuantityTable(sheet: CrossQuantitySheet, title: string): HT
|
||||
|
||||
/** 도형 층에만 공용 줌 · 팬(179-3 · 축 · 눈금 · 축선 · clip 제자리) · 휠 = 커서 고정 줌 ·
|
||||
* 바뀔 때마다 축을 보이는 범위로 다시 그림 + 십자 크기 되돌림(카드 `_Cross_View_Frame` 꼴) ·
|
||||
* 처음 · 원복 = `homeScale`(266-1 · 가로 = 카드 반폭 · 줄이면 칸 맞춤 전체). */
|
||||
* 처음 · 원복 = 중심점이 칸 가운데 · 그림 전체(가로 = 카드 반폭 266-1 · 세로 = 그 안 그림)(295-2). */
|
||||
export function attachDetailZoom(
|
||||
clone: SVGSVGElement,
|
||||
axes: CrossAxesOptions | null,
|
||||
@@ -220,6 +221,16 @@ export function attachDetailZoom(
|
||||
);
|
||||
};
|
||||
follow(initial ?? IDENTITY_VIEW);
|
||||
const plot = {
|
||||
x: CROSS_PAD.left,
|
||||
y: CROSS_PAD.top,
|
||||
width: width - CROSS_PAD.left - CROSS_PAD.right,
|
||||
height: height - CROSS_PAD.top - CROSS_PAD.bottom,
|
||||
};
|
||||
const home =
|
||||
markerLine && Number.isFinite(cx) && Number.isFinite(cy)
|
||||
? detailHomeView(layerPoints(layer), [cx, cy], plot, homeScale, 16)
|
||||
: undefined;
|
||||
return attachSvgZoomPan({
|
||||
svg: clone,
|
||||
layer,
|
||||
@@ -232,6 +243,7 @@ export function attachDetailZoom(
|
||||
wheelZoom: true,
|
||||
initial,
|
||||
homeScale,
|
||||
home,
|
||||
onChange: (state) => {
|
||||
follow(state);
|
||||
onChange(state);
|
||||
|
||||
@@ -16,6 +16,8 @@ export type LineKind = CrossPickPiece["line"];
|
||||
export interface SplitPiece {
|
||||
line: LineKind;
|
||||
points: Pt[];
|
||||
/** 구조물 선 하나(295-3) — 나눈 조각이 같은 번호면 고를 때 한 묶음(닫힌 영역 · 저장은 조각대로). */
|
||||
group?: number;
|
||||
}
|
||||
|
||||
/** 서버 `LINE_TOLERANCE_M` 과 같은 값 — 두 끝이 선에서 이만큼 안이면 「선 위」 · 고리 끝 맞음. */
|
||||
@@ -221,7 +223,12 @@ export function splitAtCrossings(lines: SplitPiece[]): SplitPiece[] {
|
||||
const [c0, c1] = [marks[k], marks[k + 1]];
|
||||
const inner = pts.slice(Math.floor(c0) + 1, Math.ceil(c1));
|
||||
const points = [at(c0), ...inner, at(c1)];
|
||||
if (pathLength(points) >= MIN_PIECE_M) out.push({ line: line.line, points });
|
||||
if (pathLength(points) >= MIN_PIECE_M)
|
||||
out.push({
|
||||
line: line.line,
|
||||
points,
|
||||
...(line.group !== undefined && { group: line.group }),
|
||||
});
|
||||
}
|
||||
});
|
||||
return out;
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_UI_Cross_Detail_Home.ts
|
||||
* [상세] 모달 첫 보기(295-2) — 중심점(중심 십자)이 칸 가운데 · 그림 전체가 들어가게
|
||||
* 축마다 중심에서 큰 쪽 반폭으로. [⤢] · 더블클릭 원복도 같은 보기. 카드 보기는 그대로.
|
||||
*
|
||||
* 가로 반폭 = 카드 반폭(266-1 `homeScale` 이 되좁히던 폭)의 큰 쪽 · 세로 반폭 = 그 가로 안에
|
||||
* 그려진 꼭짓점이 중심에서 위 · 아래로 먼 쪽.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { ZoomPanState } from "../A00_Common/b_svg_zoom_pan";
|
||||
|
||||
type Pt = [number, number];
|
||||
interface Rect {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
/** 첫 보기 배율 · 이동 — 중심 `center` 를 `plot` 가운데에. `homeScale` = 1배 대비 카드 반폭 배율. */
|
||||
export function detailHomeView(
|
||||
points: Pt[],
|
||||
center: Pt,
|
||||
plot: Rect,
|
||||
homeScale: number,
|
||||
maxScale: number,
|
||||
): ZoomPanState {
|
||||
const [cx, cy] = center;
|
||||
const halfX = Math.max(cx - plot.x, plot.x + plot.width - cx) / Math.max(homeScale, 1e-6);
|
||||
const ys = points.filter(([x]) => Math.abs(x - cx) <= halfX).map(([, y]) => y);
|
||||
const halfY = Math.max(1, ...ys.map((y) => Math.abs(y - cy)));
|
||||
const scale = Math.min(plot.width / 2 / halfX, plot.height / 2 / halfY, maxScale);
|
||||
return {
|
||||
scale,
|
||||
tx: plot.x + plot.width / 2 - scale * cx,
|
||||
ty: plot.y + plot.height / 2 - scale * cy,
|
||||
};
|
||||
}
|
||||
|
||||
/** 그림 층의 꺾은선 · 다각형 꼭짓점(그림 px). */
|
||||
export function layerPoints(layer: Element): Pt[] {
|
||||
return [...layer.querySelectorAll("polyline, polygon")].flatMap((el) =>
|
||||
(el.getAttribute("points") ?? "")
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.map((pair) => pair.split(",").map(Number))
|
||||
.filter((p): p is Pt => p.length === 2 && p.every(Number.isFinite)),
|
||||
);
|
||||
}
|
||||
@@ -36,6 +36,8 @@ import {
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
export const PIECE_HIT_CLASS = "b06-detail__piece";
|
||||
/** 구조물 선 묶음 걸침 표시(295-3) — 조각 판정 선 위 · 누름은 거리 판정이라 걸침만. */
|
||||
export const PIECE_GROUP_CLASS = "b06-detail__piece-group";
|
||||
/** 닫힌 영역 판정 면(259-2) — 조각 판정 선 밑 · 투명 채움. */
|
||||
export const FACE_HIT_CLASS = "b06-detail__face";
|
||||
|
||||
@@ -100,12 +102,14 @@ export function collectPieces(elements: Element[], axes: CrossAxesOptions): Spli
|
||||
else if (el.classList.contains("b06-chart__rock-boundary"))
|
||||
others.push({ line: "rock", points });
|
||||
}
|
||||
const straight = mergeStraightPieces(runs).map((piece): SplitPiece => ({
|
||||
// 구조물 선(295-3) = 다른 선을 나누는 기준은 그대로 · 자기 조각은 한 묶음(고를 때 통째)
|
||||
const straight = mergeStraightPieces(runs).map((piece, index): SplitPiece => ({
|
||||
line: piece.structure ? "structure" : "design",
|
||||
points: [
|
||||
[piece.from.offset, piece.from.elevation],
|
||||
[piece.to.offset, piece.to.elevation],
|
||||
],
|
||||
...(piece.structure && { group: index }),
|
||||
}));
|
||||
return splitAtCrossings([...straight, ...others]);
|
||||
}
|
||||
@@ -117,6 +121,24 @@ export function toggleSelection(selected: number[], index: number, additive: boo
|
||||
return selected.length === 1 && selected[0] === index ? [] : [index];
|
||||
}
|
||||
|
||||
/** 묶음 고름 바꿈(295-3) — 구조물 선 조각 묶음을 한 몸으로 넣고 빼기 · 하나면 `toggleSelection` 과 같음. */
|
||||
export function toggleMembers(selected: number[], members: number[], additive: boolean): number[] {
|
||||
const all = members.every((m) => selected.includes(m));
|
||||
if (additive)
|
||||
return all
|
||||
? selected.filter((i) => !members.includes(i))
|
||||
: [...selected, ...members.filter((m) => !selected.includes(m))];
|
||||
const same = all && selected.length === members.length;
|
||||
return same ? [] : [...members];
|
||||
}
|
||||
|
||||
/** 같은 묶음 조각 번호 — 묶음 없으면 그 하나. */
|
||||
export function groupMembers(pieces: SplitPiece[], index: number): number[] {
|
||||
const group = pieces[index]?.group;
|
||||
if (group === undefined) return [index];
|
||||
return pieces.flatMap((piece, i) => (piece.group === group ? [i] : []));
|
||||
}
|
||||
|
||||
const LINE_NAME: Record<SplitPiece["line"], string> = {
|
||||
design: "설계선",
|
||||
structure: "구조물 선",
|
||||
@@ -128,6 +150,10 @@ const LINE_NAME: Record<SplitPiece["line"], string> = {
|
||||
/** 고른 것 값 두 줄 — 하나 = 길이 · 경사비율 / 수평 · 높이(카드 꼴) · 여럿 = 길이 합 / 넓이. */
|
||||
export function selectionLabel(design: CrossDesign | undefined, pieces: SplitPiece[]): string {
|
||||
if (!pieces.length) return "";
|
||||
// 구조물 선 한 묶음(295-3) = 선 하나 — 첫 조각 앞 끝 ~ 끝 조각 뒤 끝
|
||||
const group = pieces[0].group;
|
||||
if (pieces.length > 1 && group !== undefined && pieces.every((p) => p.group === group))
|
||||
pieces = [{ ...pieces[0], points: [pieces[0].points[0], pieces.at(-1)!.points.at(-1)!] }];
|
||||
if (pieces.length === 1) {
|
||||
const [piece] = pieces;
|
||||
const from = piece.points[0];
|
||||
@@ -152,8 +178,9 @@ export function selectionLabel(design: CrossDesign | undefined, pieces: SplitPie
|
||||
}
|
||||
const total = pieces.reduce((sum, piece) => sum + pathLength(piece.points), 0);
|
||||
const area = ringArea(pieces.map((piece) => piece.points));
|
||||
const units = new Set(pieces.map((piece) => piece.group ?? piece)).size;
|
||||
return (
|
||||
`${pieces.length}개 · 길이 합 ${total.toFixed(2)}m\n` +
|
||||
`${units}개 · 길이 합 ${total.toFixed(2)}m\n` +
|
||||
(area === null ? "면적 닫히지 않음" : `면적 ${area.toFixed(2)}㎡ · 둘레 ${total.toFixed(2)}m`)
|
||||
);
|
||||
}
|
||||
@@ -236,6 +263,8 @@ const inside = (p: Pt, ring: Pt[]): boolean =>
|
||||
* 누른 자리 → 선 · 영역(261-3) — 좌표는 그림 px · `scale` = 화면 px / 그림 px · `areas` = 영역 넓이.
|
||||
* 가장 가까운 선이 `LINE_PICK_PX` 안이면 선 — 단 영역 안이면 그 선과 끝이 안 닿는 맞은편 선보다
|
||||
* 절반 이하로 가까워야 선(얇은 띠 가운데 ⅓ = 영역) · 아니면 누른 자리를 품은 가장 작은 영역.
|
||||
* 고르기 차례(295-1 · 카드와 같은 잣대) = 선 > 작은 면 > 큰 면 · 띠 — `bandArea`(그 자리 면적 띠
|
||||
* 넓이 · 그림 px²)가 영역보다 작을 때만 띠(표가 맡음).
|
||||
*/
|
||||
export function pickAt(
|
||||
point: Pt,
|
||||
@@ -244,7 +273,8 @@ export function pickAt(
|
||||
rings: Pt[][],
|
||||
areas: number[],
|
||||
pieces: SplitPiece[],
|
||||
): { piece: number } | { face: number } | null {
|
||||
bandArea: number | null = null,
|
||||
): { piece: number } | { face: number } | { band: true } | null {
|
||||
const gaps = paths.map((path) => pathGap(point, path) * scale);
|
||||
const nearest = gaps.reduce((best, gap, i) => (gap < gaps[best] ? i : best), 0);
|
||||
const ends = (piece: SplitPiece): Pt[] => [
|
||||
@@ -268,9 +298,17 @@ export function pickAt(
|
||||
(face === undefined || gaps[nearest] * 2 <= across)
|
||||
)
|
||||
return { piece: nearest };
|
||||
const faceArea = face === undefined ? Infinity : Math.abs(shoelace(rings[face]));
|
||||
if (bandArea !== null && bandArea < faceArea) return { band: true };
|
||||
return face === undefined ? null : { face };
|
||||
}
|
||||
|
||||
const shoelace = (ring: Pt[]): number =>
|
||||
ring.reduce((sum, [x0, y0], i) => {
|
||||
const [x1, y1] = ring[(i + 1) % ring.length];
|
||||
return sum + (x0 * y1 - x1 * y0) / 2;
|
||||
}, 0);
|
||||
|
||||
/** 조각의 자리 이름 — 선 종류 + mm 로 맞춘 꼭짓점(px → m 되짚기의 부동소수 흔들림 거름). */
|
||||
const pieceKey = (piece: SplitPiece | undefined): string =>
|
||||
piece ? `${piece.line}|${piece.points.map((p) => p.map((v) => v.toFixed(3))).join(";")}` : "";
|
||||
@@ -283,8 +321,8 @@ export function attachDetailSelect(input: {
|
||||
picked: (additive: boolean) => void;
|
||||
/** [칸에 넣기] 단추. */
|
||||
commit: () => void;
|
||||
/** 면적 고르기 중 그 자리에 면적 띠가 있나 — 있으면 영역 대신 띠(표가 맡음). */
|
||||
band?: (event: MouseEvent) => boolean;
|
||||
/** 면적 고르기 중 그 자리 면적 띠 넓이(그림 px²) · 없음 = null — 영역보다 작을 때만 띠(295-1). */
|
||||
band?: (event: MouseEvent) => number | null;
|
||||
}): DetailSelect {
|
||||
const { cad } = input;
|
||||
let pieces: SplitPiece[] = [];
|
||||
@@ -343,7 +381,8 @@ export function attachDetailSelect(input: {
|
||||
if (!ctm || !layer?.ownerSVGElement?.contains(event.target as Node)) return null;
|
||||
const p = new DOMPoint(event.clientX, event.clientY).matrixTransform(ctm.inverse());
|
||||
const areas = faces.map((face) => face.area);
|
||||
return pickAt([p.x, p.y], Math.hypot(ctm.a, ctm.b), paths, rings, areas, pieces);
|
||||
const band = mode === "area" ? (input.band?.(event) ?? null) : null;
|
||||
return pickAt([p.x, p.y], Math.hypot(ctm.a, ctm.b), paths, rings, areas, pieces, band);
|
||||
};
|
||||
|
||||
const onClick = (event: MouseEvent): void => {
|
||||
@@ -352,9 +391,10 @@ export function attachDetailSelect(input: {
|
||||
const additive = shift || mode === "area";
|
||||
noteText = null;
|
||||
if (!pick || !("piece" in pick)) {
|
||||
if (pick && !(mode === "area" && input.band?.(event))) {
|
||||
// 영역 안 누름 = 그 영역 둘레 조각 고름(259-2) · 길이 고르기여도 바로 넣지 않음(Enter)
|
||||
event.stopPropagation();
|
||||
if (pick && "face" in pick) {
|
||||
// 영역 안 누름 = 그 영역 둘레 조각 고름(259-2) · 길이 고르기여도 바로 넣지 않음(Enter) ·
|
||||
// 띠보다 좁은 영역이라 표 띠 누름(같은 cad 의 다음 듣기)보다 앞섬(295-1)
|
||||
event.stopImmediatePropagation();
|
||||
const index = pick.face;
|
||||
chosenFaces = toggleSelection(chosenFaces, index, shift);
|
||||
selected = regionEdges(chosenFaces.map((i) => faces[i]));
|
||||
@@ -369,7 +409,7 @@ export function attachDetailSelect(input: {
|
||||
// 선이 표 띠 누름(같은 cad 의 다음 듣기)보다 앞섬
|
||||
event.stopImmediatePropagation();
|
||||
chosenFaces = [];
|
||||
selected = toggleSelection(selected, pick.piece, additive);
|
||||
selected = toggleMembers(selected, groupMembers(pieces, pick.piece), additive);
|
||||
show();
|
||||
input.picked(additive);
|
||||
};
|
||||
@@ -403,6 +443,19 @@ export function attachDetailSelect(input: {
|
||||
layer?.append(hit);
|
||||
return hit;
|
||||
});
|
||||
// 구조물 선 묶음 걸침 표시(295-3) — 조각 대신 선 하나로 비침
|
||||
new Set(pieces.flatMap((piece) => (piece.group === undefined ? [] : [piece.group]))).forEach(
|
||||
(group) => {
|
||||
const members = pieces.flatMap((piece, i) => (piece.group === group ? [i] : []));
|
||||
const el = document.createElementNS(SVG_NS, "polyline");
|
||||
el.setAttribute(
|
||||
"points",
|
||||
[paths[members[0]][0], paths[members.at(-1)!].at(-1)!].join(" "),
|
||||
);
|
||||
el.setAttribute("class", PIECE_GROUP_CLASS);
|
||||
layer?.append(el);
|
||||
},
|
||||
);
|
||||
show();
|
||||
},
|
||||
setMode(kind) {
|
||||
|
||||
@@ -25,6 +25,7 @@ import type { CrossPickPiece, SectionSample } from "./B06_Section_Api_Types";
|
||||
import type { CrossAxesOptions } from "./B06_Section_UI_Cross_Axes";
|
||||
import { pickLines, piecesValue } from "./B06_Section_UI_Cross_Detail_Geom";
|
||||
import { attachDetailSelect } from "./B06_Section_UI_Cross_Detail_Select";
|
||||
import { pointsArea } from "./B06_Section_UI_Cross_Pick_Order";
|
||||
|
||||
/** 서버 `MAX_ROWS` · `MAX_COLUMNS` · `LINE_TOLERANCE_M` 와 같은 값. */
|
||||
export const MAX_ROWS = 12;
|
||||
@@ -387,13 +388,16 @@ export function attachDetailTable(host: DetailTableHost): {
|
||||
if (picking?.kind === "length" && !additive) commitSelection();
|
||||
},
|
||||
commit: commitSelection,
|
||||
band: (event) => !!bandAt(event),
|
||||
band: (event) => {
|
||||
const hit = bandAt(event);
|
||||
return hit ? pointsArea(hit.getAttribute("points")) : null;
|
||||
},
|
||||
});
|
||||
const bandAt = (event: MouseEvent): Element | undefined =>
|
||||
document
|
||||
.elementsFromPoint(event.clientX, event.clientY)
|
||||
.find((el) => cad.contains(el) && el.closest(AREA_PICK_SELECTOR));
|
||||
// 면적 띠 · 사토장 누름 = 그 띠 면적(179-4 그대로 · 영역보다 앞) — 선 누름은 위가 막음(261-3 거리 판정)
|
||||
// 면적 띠 · 사토장 누름 = 그 띠 면적(179-4) — 선 · 띠보다 좁은 영역은 위가 막음(261-3 거리 판정 · 295-1)
|
||||
const onPick = (event: MouseEvent): void => {
|
||||
if (picking?.kind !== "area") return;
|
||||
const hit = bandAt(event);
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_UI_Cross_Pick_Order.ts
|
||||
* 횡단 카드 고르기 차례(295-1) — 고를 자리가 좁은 것이 위(모달 `pickAt` 과 같은 잣대).
|
||||
*
|
||||
* 판정면(구조물 · 띠 · 형상)을 다 꽂은 뒤 넓이 큰 것부터 아래로 다시 세움(작은 것 위) ·
|
||||
* 보라 선 판정(넓이 0 · 요구 185 사면 손잡이)은 맨 위 · 면적 띠(절 · 성토)는 맨 아래 그대로.
|
||||
* 끌고 뗀 누름(3 px 넘게 움직임)은 고름 아님.
|
||||
* ========================================================================== */
|
||||
|
||||
/** 면 판정 — 구조물(벽 · 관 · 박스 · 도수로 띠 등) · 물넘이포장 · 맹암거 형상. */
|
||||
const SURFACE_HIT_CLASSES = ["b06-chart__culvert-revet-hit", "b06-chart__shape-hit"];
|
||||
/** 선 판정 — 보라 설계선 조각(`_Cross_Design_Pick`). */
|
||||
const LINE_HIT_CLASS = "b06-chart__design-hit";
|
||||
/** 끌기로 볼 움직임(px) — 형상 고름(`SHAPE_DRAG_PX`)과 같은 값. */
|
||||
const DRAG_PX = 3;
|
||||
|
||||
/** `points` 속성 다각형 넓이(px²) — 점이 셋 못 되면 0. */
|
||||
export function pointsArea(points: string | null): number {
|
||||
const pts = (points ?? "")
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.map((pair) => pair.split(",").map(Number))
|
||||
.filter((p) => p.length === 2 && p.every(Number.isFinite));
|
||||
if (pts.length < 3) return 0;
|
||||
let twice = 0;
|
||||
pts.forEach(([x0, y0], i) => {
|
||||
const [x1, y1] = pts[(i + 1) % pts.length];
|
||||
twice += x0 * y1 - x1 * y0;
|
||||
});
|
||||
return Math.abs(twice) / 2;
|
||||
}
|
||||
|
||||
/** 판정면을 넓이 차례로 다시 세우고 보라 선 판정을 맨 위로 · 끌기 누름을 막음. */
|
||||
export function stackPickTargets(layer: SVGElement): void {
|
||||
const kids = [...layer.children];
|
||||
const surfaces = kids
|
||||
.filter((el) => SURFACE_HIT_CLASSES.some((name) => el.classList.contains(name)))
|
||||
.map((el) => ({ el, area: pointsArea(el.getAttribute("points")) }))
|
||||
.sort((a, b) => b.area - a.area)
|
||||
.map(({ el }) => el);
|
||||
const lines = kids.filter((el) => el.classList.contains(LINE_HIT_CLASS));
|
||||
layer.append(...surfaces, ...lines);
|
||||
let down: { x: number; y: number } | null = null;
|
||||
layer.addEventListener(
|
||||
"pointerdown",
|
||||
(event) => (down = { x: (event as PointerEvent).clientX, y: (event as PointerEvent).clientY }),
|
||||
true,
|
||||
);
|
||||
layer.addEventListener(
|
||||
"click",
|
||||
(event) => {
|
||||
const { clientX, clientY } = event as MouseEvent;
|
||||
if (down && Math.hypot(clientX - down.x, clientY - down.y) > DRAG_PX) event.stopPropagation();
|
||||
down = null;
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from "./B06_Section_UI_Cross_Ford_Pavement";
|
||||
import { ownWallsDrawAt } from "./B06_Section_UI_Cross_Culvert_Wire";
|
||||
import { attachShapePick, shapePickRoute } from "./B06_Section_UI_Cross_Shape_Pick";
|
||||
import { stackPickTargets } from "./B06_Section_UI_Cross_Pick_Order";
|
||||
import { appendUnderdrainOverlay } from "./B06_Section_UI_Cross_Underdrain";
|
||||
import { raiseWalls } from "./B06_Section_UI_Cross_Wall";
|
||||
import { attachChuteBandPick, groundLine } from "./B06_Section_UI_Cross_Wall_Hatch";
|
||||
@@ -609,6 +610,7 @@ export function createCrossSectionCard(
|
||||
});
|
||||
setLineActive = linePick.setActive;
|
||||
setLineActive(pickedLineAt(section.station_id));
|
||||
stackPickTargets(plotLayer); // 고르기 차례 = 좁은 것 위 · 보라 선 맨 위(295-1)
|
||||
}
|
||||
|
||||
// 십자선(계획 노선 자리) · 축선 · 높이 축 이름 — `_Cross_View_Frame`(700줄 제한).
|
||||
|
||||
@@ -196,7 +196,8 @@
|
||||
}
|
||||
|
||||
/* 선 조각 판정 선(209-1) — 투명 굵은 선 · 배율 무관 굵기 · 고른 조각 = 붉은 띠 */
|
||||
.b06-detail__piece {
|
||||
.b06-detail__piece,
|
||||
.b06-detail__piece-group {
|
||||
fill: none;
|
||||
stroke: transparent;
|
||||
/* 누름 판정은 거리(LINE_PICK_PX 4) — 굵기는 걸침 표시만 */
|
||||
@@ -207,7 +208,9 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.b06-detail__piece:hover {
|
||||
/* 구조물 선 묶음(295-3) — 나뉜 조각 위 한 줄 · 걸치면 선 하나로 비침 */
|
||||
.b06-detail__piece:hover,
|
||||
.b06-detail__piece-group:hover {
|
||||
stroke: color-mix(in srgb, var(--color-accent) 25%, transparent);
|
||||
}
|
||||
|
||||
|
||||
@@ -92,6 +92,8 @@ function el(tag) {
|
||||
};
|
||||
return walk(self);
|
||||
},
|
||||
// 첫 보기 꼭짓점(295-2) — 이 그림엔 꺾은선 · 다각형 없음
|
||||
querySelectorAll: () => [],
|
||||
addEventListener: (type, fn) => (listeners[type] ??= []).push(fn),
|
||||
fire: (type, event) => {
|
||||
const full = { preventDefault() {}, stopPropagation() {}, pointerId: 1, ...event };
|
||||
@@ -109,7 +111,7 @@ globalThis.document = { createElementNS: (_ns, tag) => el(tag), createElement: e
|
||||
|
||||
const { attachDetailZoom } = loadTs(path.join(B06, "B06_Section_UI_Cross_Detail.ts"));
|
||||
|
||||
// 카드 svg 꼴 — 바탕 · 축 층 · clip 그룹 > 도형 층 > 십자(중심 300,150)
|
||||
// 카드 svg 꼴 — 바탕 · 축 층 · clip 그룹 > 도형 층 > 십자(중심 319,142 = 칸 가운데 · 295-2 첫 보기 = 1배)
|
||||
const AXES = {
|
||||
widthPx: 600,
|
||||
heightPx: 300,
|
||||
@@ -126,7 +128,7 @@ function chart() {
|
||||
const clipGroup = svgElement("g", { "clip-path": "url(#c)" });
|
||||
const plot = svgElement("g", { class: "b06-chart__plot-layer" });
|
||||
const center = svgElement("g", { class: "b06-chart__center" });
|
||||
center.append(svgElement("line", { x1: 300, y1: 132, x2: 300, y2: 168 }));
|
||||
center.append(svgElement("line", { x1: 319, y1: 124, x2: 319, y2: 160 }));
|
||||
plot.append(center);
|
||||
clipGroup.append(plot);
|
||||
svg.append(svgElement("rect"), axes, clipGroup, svgElement("line", { class: "b06-chart__axis" }));
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
/* PLAN 295 단위검증 헬퍼 — 횡단 카드 고르기 차례 · [상세] 모달 고르기 차례 · 첫 보기 · 구조물 선 안 나눔.
|
||||
* 화면 TS 를 그 자리에서 트랜스파일해 Node 로 돌림 · 결과 = 표준출력 JSON 한 벌.
|
||||
* 짝 = `test_295_pick_order_detail.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 B06 = path.join(ROOT, "B06_Section");
|
||||
const common = fs.readFileSync(path.join(B06, "B06_Section_UI_Section_Common.ts"), "utf8");
|
||||
const CROSS_PAD = Function(`return ${/export const CROSS_PAD = (\{[^}]*\})/.exec(common)[1]}`)();
|
||||
globalThis.window = { sessionStorage: new Map(), localStorage: new Map() };
|
||||
globalThis.document = { addEventListener: () => {}, removeEventListener: () => {} };
|
||||
|
||||
const STUBS = {
|
||||
"@ui/ui_template_context_menu": { createMapContextMenu: () => ({}) },
|
||||
"./B06_Section_UI_Section_Common": { CROSS_PAD },
|
||||
};
|
||||
const resolve = (base) => (fs.existsSync(`${base}.ts`) ? `${base}.ts` : base);
|
||||
function loadTs(full) {
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
const localRequire = (request) => {
|
||||
if (STUBS[request]) return STUBS[request];
|
||||
if (request.endsWith(".css")) return {};
|
||||
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);
|
||||
return box.exports;
|
||||
}
|
||||
const P = loadTs(path.join(B06, "B06_Section_UI_Cross_Pick_Order.ts"));
|
||||
const G = loadTs(path.join(B06, "B06_Section_UI_Cross_Detail_Geom.ts"));
|
||||
const S = loadTs(path.join(B06, "B06_Section_UI_Cross_Detail_Select.ts"));
|
||||
const H = loadTs(path.join(B06, "B06_Section_UI_Cross_Detail_Home.ts"));
|
||||
const out = {};
|
||||
|
||||
/* ── 카드: 흉내 층 ───────────────────────────────────────────────────── */
|
||||
function node(name, classes, points) {
|
||||
return {
|
||||
name,
|
||||
parent: null,
|
||||
tagName: points && classes.includes("b06-chart__design-hit") ? "polyline" : "polygon",
|
||||
classList: { contains: (c) => classes.includes(c) },
|
||||
getAttribute: (a) => (a === "points" ? points : null),
|
||||
};
|
||||
}
|
||||
function layer(kids) {
|
||||
const listeners = [];
|
||||
const self = {
|
||||
children: [],
|
||||
append: (...nodes) =>
|
||||
nodes.forEach((n) => {
|
||||
const at = self.children.indexOf(n);
|
||||
if (at >= 0) self.children.splice(at, 1);
|
||||
self.children.push(n);
|
||||
}),
|
||||
addEventListener: (type, fn, capture) => listeners.push({ type, fn, capture }),
|
||||
fire: (type, event) => {
|
||||
let stopped = false;
|
||||
const full = { ...event, stopPropagation: () => (stopped = true) };
|
||||
listeners.filter((l) => l.type === type && l.capture).forEach((l) => l.fn(full));
|
||||
return stopped;
|
||||
},
|
||||
};
|
||||
self.append(...kids);
|
||||
return self;
|
||||
}
|
||||
const box = (x0, y0, x1, y1) => `${x0},${y0} ${x1},${y0} ${x1},${y1} ${x0},${y1}`;
|
||||
const HIT = "b06-chart__culvert-revet-hit";
|
||||
// 꽂힌 옛 차례 — 면적 띠(맨 아래) · 보라 선(구조물 판정면 앞) · 형상 · 도수로 띠 · 벽 · 관
|
||||
const kids = [
|
||||
node("band", ["b06-chart__area"], box(0, 0, 600, 300)),
|
||||
node("slope", ["b06-chart__design-hit"], "400,100 500,200"),
|
||||
node("top", ["b06-chart__design-hit"], "300,100 400,100"),
|
||||
node("shape", ["b06-chart__shape-hit"], box(300, 100, 310, 110)), // 100
|
||||
node("chute", [HIT, "b06-chart__chute-hit"], box(390, 90, 510, 210)), // 14400 — 성토 사면을 덮음
|
||||
node("wall", [HIT], box(200, 100, 220, 300)), // 4000
|
||||
node("pipe", [HIT], box(250, 250, 255, 255)), // 25
|
||||
];
|
||||
const L = layer(kids);
|
||||
P.stackPickTargets(L);
|
||||
out.cardOrder = L.children.map((n) => n.name);
|
||||
// 누른 자리에서 맨 위 판정(뒤에서부터) — 다각형 안 · 선은 5 px 안
|
||||
const inside = (p, pts) => {
|
||||
let inn = false;
|
||||
for (let i = 0, j = pts.length - 1; i < pts.length; j = i++) {
|
||||
const [ax, ay] = pts[i];
|
||||
const [bx, by] = pts[j];
|
||||
if (ay > p[1] !== by > p[1] && p[0] < ((bx - ax) * (p[1] - ay)) / (by - ay) + ax) inn = !inn;
|
||||
}
|
||||
return inn;
|
||||
};
|
||||
const gap = (p, [a, b]) => {
|
||||
const d = [b[0] - a[0], b[1] - a[1]];
|
||||
const t = Math.max(
|
||||
0,
|
||||
Math.min(1, ((p[0] - a[0]) * d[0] + (p[1] - a[1]) * d[1]) / (d[0] ** 2 + d[1] ** 2)),
|
||||
);
|
||||
return Math.hypot(p[0] - a[0] - t * d[0], p[1] - a[1] - t * d[1]);
|
||||
};
|
||||
const ptsOf = (n) =>
|
||||
n
|
||||
.getAttribute("points")
|
||||
.split(" ")
|
||||
.map((q) => q.split(",").map(Number));
|
||||
const topAt = (p, order) =>
|
||||
[...order]
|
||||
.reverse()
|
||||
.find((n) =>
|
||||
n.classList.contains("b06-chart__design-hit") ? gap(p, ptsOf(n)) <= 5 : inside(p, ptsOf(n)),
|
||||
)?.name;
|
||||
out.slopeClick = topAt([450, 150], L.children); // 띠 안 성토 사면 위
|
||||
out.slopeClickOld = topAt([450, 150], kids); // 옛 차례
|
||||
out.chuteClick = topAt([480, 120], L.children); // 띠 안 · 선 밖
|
||||
out.pipeOverWall = topAt([252, 252], [...L.children]);
|
||||
out.area = P.pointsArea(box(0, 0, 10, 20));
|
||||
// 끌기 = 고름 아님
|
||||
L.fire("pointerdown", { clientX: 10, clientY: 10 });
|
||||
out.dragStopped = L.fire("click", { clientX: 20, clientY: 10 });
|
||||
L.fire("pointerdown", { clientX: 10, clientY: 10 });
|
||||
out.clickStopped = L.fire("click", { clientX: 11, clientY: 11 });
|
||||
|
||||
/* ── 모달: 고르기 차례 ───────────────────────────────────────────────── */
|
||||
const sq = (x0, y0, x1, y1) => [
|
||||
[x0, y0],
|
||||
[x1, y0],
|
||||
[x1, y1],
|
||||
[x0, y1],
|
||||
];
|
||||
const big = sq(0, 0, 100, 100);
|
||||
const small = sq(40, 40, 60, 60);
|
||||
const edges = (ring) => ring.map((p, i) => [p, ring[(i + 1) % ring.length]]);
|
||||
const paths = [...edges(big), ...edges(small)];
|
||||
const pieces = paths.map((points) => ({ line: "design", points }));
|
||||
const rings = [big, small];
|
||||
const areas = [10000, 400];
|
||||
const pick = (p, band = null) => S.pickAt(p, 1, paths, rings, areas, pieces, band);
|
||||
out.modal = {
|
||||
line: pick([50, 41]), // 작은 면 둘레 1 px
|
||||
small: pick([50, 50]),
|
||||
big: pick([20, 20]),
|
||||
bandSmaller: pick([50, 50], 100),
|
||||
bandBigger: pick([50, 50], 5000),
|
||||
bandBigFace: pick([20, 20], 5000),
|
||||
bandOnly: S.pickAt([500, 500], 1, paths, rings, areas, pieces, 50),
|
||||
lineOverBand: pick([50, 41], 1),
|
||||
};
|
||||
|
||||
/* ── 모달: 첫 보기 ───────────────────────────────────────────────────── */
|
||||
const plot = { x: 50, y: 10, width: 500, height: 280 };
|
||||
const center = [300, 150];
|
||||
const drawn = [
|
||||
[100, 120],
|
||||
[500, 140],
|
||||
[300, 360], // 아래로 210 — 큰 쪽
|
||||
[200, 100],
|
||||
[5000, 9000], // 가로 창 밖 = 셈 안 함
|
||||
];
|
||||
const home = H.detailHomeView(drawn, center, plot, 1, 16);
|
||||
const map = ([x, y]) => [home.tx + home.scale * x, home.ty + home.scale * y];
|
||||
out.home = {
|
||||
scale: home.scale,
|
||||
center: map(center),
|
||||
plotCenter: [plot.x + plot.width / 2, plot.y + plot.height / 2],
|
||||
fits: drawn.slice(0, 4).every(([x, y]) => {
|
||||
const [sx, sy] = map([x, y]);
|
||||
return (
|
||||
sx >= plot.x - 1e-6 &&
|
||||
sx <= plot.x + plot.width + 1e-6 &&
|
||||
sy >= plot.y - 1e-6 &&
|
||||
sy <= plot.y + plot.height + 1e-6
|
||||
);
|
||||
}),
|
||||
flat: H.detailHomeView([[300, 150]], center, plot, 2, 16).scale,
|
||||
};
|
||||
|
||||
/* ── 모달: 구조물 선 안 나눔 ─────────────────────────────────────────── */
|
||||
// 상자(구조물 네 변 · 변마다 묶음) + 가로지르는 원지반선
|
||||
const boxLines = edges(sq(0, 0, 10, 10)).map((points, group) => ({
|
||||
line: "structure",
|
||||
points,
|
||||
group,
|
||||
}));
|
||||
const split = G.splitAtCrossings([
|
||||
...boxLines,
|
||||
{
|
||||
line: "ground",
|
||||
points: [
|
||||
[-5, 5],
|
||||
[15, 5],
|
||||
],
|
||||
},
|
||||
]);
|
||||
const left = split.findIndex(
|
||||
(p) => p.line === "structure" && p.points.every(([x]) => Math.abs(x) < 1e-9),
|
||||
);
|
||||
const members = S.groupMembers(split, left);
|
||||
const chosen = S.toggleMembers([], members, false);
|
||||
out.structure = {
|
||||
pieces: split.length,
|
||||
groundPieces: split.filter((p) => p.line === "ground").length,
|
||||
members: members.length,
|
||||
chosen: chosen.length,
|
||||
again: S.toggleMembers(chosen, members, false).length,
|
||||
additiveOff: S.toggleMembers([...chosen, 99], members, true),
|
||||
label: S.selectionLabel(
|
||||
undefined,
|
||||
chosen.map((i) => split[i]),
|
||||
),
|
||||
faces: G.findFaces(split)
|
||||
.map((f) => Math.round(f.area * 100) / 100)
|
||||
.sort(),
|
||||
// 옛 저장 조각(구조물 선 아래 반쪽) — 그대로 다시 고름
|
||||
oldPick: split.filter((p) => S.samePiece(p, { line: "structure", start: [0, 0], end: [0, 5] }))
|
||||
.length,
|
||||
wholeValue: G.piecesValue(
|
||||
"length",
|
||||
chosen.map((i) => G.pieceSource(split[i])),
|
||||
{},
|
||||
),
|
||||
};
|
||||
// collectPieces — 구조물 선(설계선 표식 없음)에 묶음 · 설계선은 없음
|
||||
const axes = {
|
||||
widthPx: 600,
|
||||
heightPx: 300,
|
||||
pixelsPerMeter: 10,
|
||||
maxOffset: 20,
|
||||
displayMax: 120,
|
||||
elevationMid: 110,
|
||||
exaggeration: 1,
|
||||
};
|
||||
const px = ([offset, elevation]) =>
|
||||
[
|
||||
CROSS_PAD.left + (axes.maxOffset - offset) * axes.pixelsPerMeter,
|
||||
CROSS_PAD.top +
|
||||
(axes.displayMax -
|
||||
(axes.elevationMid + (elevation - axes.elevationMid) * axes.exaggeration)) *
|
||||
axes.pixelsPerMeter,
|
||||
].join(",");
|
||||
const el = (classes, points, attrs = {}) => ({
|
||||
tagName: "polyline",
|
||||
classList: { contains: (name) => classes.includes(name) },
|
||||
hasAttribute: (name) => name in attrs,
|
||||
getAttribute: (name) => (name === "points" ? points.map(px).join(" ") : (attrs[name] ?? null)),
|
||||
});
|
||||
const collected = S.collectPieces(
|
||||
[
|
||||
el(
|
||||
["b06-chart__design-cross"],
|
||||
[
|
||||
[-10, 110],
|
||||
[10, 110],
|
||||
],
|
||||
{ "data-design-line": "" },
|
||||
),
|
||||
el(
|
||||
["b06-chart__design-cross"],
|
||||
[
|
||||
[0, 105],
|
||||
[0, 115],
|
||||
],
|
||||
),
|
||||
],
|
||||
axes,
|
||||
);
|
||||
out.collected = collected.map((p) => ({ line: p.line, group: p.group ?? null }));
|
||||
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -54,8 +54,8 @@ def test_축_고정_도형_층만_움직임_눈금_다시(out):
|
||||
assert out["axesTransform"] is None and out["clipTransform"] is None
|
||||
assert out["ticksChanged"] is True and out["ticksAfter"] > 0
|
||||
assert out["restoredTicksShifted"] is True # 다시 떠도 이어 간 범위로
|
||||
# 확대 — 십자는 중심(300,150)에서 배율 되돌림
|
||||
assert out["centerTransform"].startswith("translate(300 150) scale(0.869")
|
||||
# 확대 — 십자는 중심(319,142 · 칸 가운데)에서 배율 되돌림
|
||||
assert out["centerTransform"].startswith("translate(319 142) scale(0.869")
|
||||
assert out["noAxesHandle"] is True # 축 옵션 없음 = 줌만
|
||||
assert out["noLayer"] is None # 도형 층 없음 = 줌 없음
|
||||
|
||||
|
||||
@@ -81,5 +81,5 @@ def test_배선():
|
||||
in view
|
||||
)
|
||||
src = " ".join(_read("B06_Section_UI_Cross_Detail.ts").split())
|
||||
assert "drawn.homeScale" in src and "homeScale, onChange" in src
|
||||
assert "drawn.homeScale" in src and "homeScale, home, onChange" in src # 295-2 첫 보기
|
||||
assert "homeScale: drawn.homeScale" in src
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
"""PLAN 295 — 횡단 카드 · [상세] 모달 고르기 차례 · 모달 첫 보기 · 구조물 선 안 나눔(294-5 포함).
|
||||
|
||||
- 295-1 카드 = 판정면 넓이 큰 것부터 아래(작은 것 위) · 보라 선(사면 손잡이) 맨 위 · 띠 있는 성토 사면 누름 = 사면 ·
|
||||
끌기 = 고름 아님 / 모달 `pickAt` = 선 > 작은 면 > 큰 면 · 띠(띠는 영역보다 작을 때만)
|
||||
- 295-2 모달 첫 보기 = 중심점이 칸 가운데 · 그림 전체가 들어가게 축마다 큰 쪽 반폭
|
||||
- 295-3 모달 구조물 선 = 다른 선을 나누는 기준 그대로 · 고를 땐 한 선 통째 · 닫힌 영역 그대로 · 옛 조각 다시 고름
|
||||
|
||||
실제 TS 를 `helper_295_pick_order_detail.cjs` 가 Node 로 돌린다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(__file__)
|
||||
ROOT = os.path.join(HERE, "..", "..")
|
||||
B06 = os.path.join(ROOT, "B06_Section")
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not os.path.isdir(os.path.join(ROOT, "config", "node_modules")),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
|
||||
def _read(name: str) -> str:
|
||||
with open(os.path.join(B06, name), encoding="utf-8") as f:
|
||||
return f.read().replace(chr(13), "")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out() -> dict:
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", os.path.join(HERE, "helper_295_pick_order_detail.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_카드_작은_것_위_보라_선_맨_위(out):
|
||||
# 띠(14400) > 벽(4000) > 형상(100) > 관(25) · 보라 선 둘 맨 위 · 면적 띠는 맨 아래 그대로
|
||||
assert out["cardOrder"] == ["band", "chute", "wall", "shape", "pipe", "slope", "top"]
|
||||
assert out["area"] == 200
|
||||
|
||||
|
||||
def test_카드_띠_있는_성토_사면_누름은_사면_294_5(out):
|
||||
assert out["slopeClickOld"] == "chute" # 옛 차례 = 띠가 사면을 가림
|
||||
assert out["slopeClick"] == "slope"
|
||||
assert out["chuteClick"] == "chute" # 선 밖 띠 자리는 띠
|
||||
assert out["pipeOverWall"] == "pipe"
|
||||
|
||||
|
||||
def test_카드_끌기는_고름_아님(out):
|
||||
assert out["dragStopped"] is True
|
||||
assert out["clickStopped"] is False
|
||||
|
||||
|
||||
def test_모달_선_작은_면_큰_면_띠(out):
|
||||
m = out["modal"]
|
||||
assert m["line"] == {"piece": 4}
|
||||
assert m["small"] == {"face": 1}
|
||||
assert m["big"] == {"face": 0}
|
||||
assert m["bandBigger"] == {"face": 1} # 띠보다 좁은 영역이 앞(옛 「띠가 앞」 폐기)
|
||||
assert m["bandSmaller"] == {"band": True}
|
||||
assert m["bandBigFace"] == {"band": True}
|
||||
assert m["bandOnly"] == {"band": True}
|
||||
assert m["lineOverBand"] == {"piece": 4}
|
||||
|
||||
|
||||
def test_모달_첫_보기_중심점_가운데(out):
|
||||
h = out["home"]
|
||||
assert h["center"] == pytest.approx(h["plotCenter"])
|
||||
assert h["scale"] == pytest.approx(140 / 210) # 아래로 먼 쪽 210 px 이 반높이에
|
||||
assert h["fits"] is True
|
||||
assert h["flat"] == pytest.approx(2) # 세로가 낮으면 가로(카드 반폭) 배율
|
||||
|
||||
|
||||
def test_모달_구조물_선_한_조각(out):
|
||||
s = out["structure"]
|
||||
assert s["groundPieces"] == 3 # 구조물 선이 원지반선을 나눔(기준 그대로)
|
||||
assert s["members"] == 2 and s["chosen"] == 2 # 나뉜 조각 둘 = 한 선 통째
|
||||
assert s["again"] == 0
|
||||
assert s["additiveOff"] == [99]
|
||||
assert s["label"].startswith("구조물 선 길이 10.00m")
|
||||
assert s["wholeValue"] == pytest.approx(10)
|
||||
assert out["collected"] == [
|
||||
{"line": "design", "group": None},
|
||||
{"line": "design", "group": None},
|
||||
{"line": "structure", "group": 1},
|
||||
{"line": "structure", "group": 1},
|
||||
]
|
||||
|
||||
|
||||
def test_모달_닫힌_영역_옛_조각(out):
|
||||
s = out["structure"]
|
||||
assert s["faces"] == [50, 50]
|
||||
assert s["oldPick"] == 1
|
||||
|
||||
|
||||
def test_배선():
|
||||
view = _read("B06_Section_UI_Cross_View.ts")
|
||||
assert view.index("setLineActive(pickedLineAt(") < view.index("stackPickTargets(plotLayer)")
|
||||
select = " ".join(_read("B06_Section_UI_Cross_Detail_Select.ts").split())
|
||||
assert "groupMembers(pieces, pick.piece)" in select
|
||||
assert 'pick && "face" in pick' in select
|
||||
table = _read("B06_Section_UI_Cross_Detail_Table.ts")
|
||||
assert 'pointsArea(hit.getAttribute("points"))' in table
|
||||
detail = " ".join(_read("B06_Section_UI_Cross_Detail.ts").split())
|
||||
assert "detailHomeView(layerPoints(layer), [cx, cy], plot, homeScale, 16)" in detail
|
||||
for name in ("B06_Section_UI_Cross_View.ts", "B06_Section_UI_Cross_Detail_Select.ts"):
|
||||
assert len(_read(name).splitlines()) <= 700
|
||||
Reference in New Issue
Block a user