feat(b06): 고르기 차례 = 좁은 것 위 — 카드 판정면 넓이 큰 것부터 아래 · 보라 선 맨 위 · 끌기 = 고름 아님 · 모달 선 > 작은 면 > 큰 면 · 띠 · 도수로 띠 위 성토 사면 고름 (295-1 · 294-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
This commit is contained in:
@@ -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"),
|
||||
);
|
||||
|
||||
@@ -236,6 +236,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 +246,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 +271,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 +294,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 +354,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 +364,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]));
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -21,6 +21,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 } from "./B06_Section_UI_Cross_Wall_Hatch";
|
||||
@@ -604,6 +605,7 @@ export function createCrossSectionCard(
|
||||
});
|
||||
setLineActive = linePick.setActive;
|
||||
setLineActive(pickedLineAt(section.station_id));
|
||||
stackPickTargets(plotLayer); // 고르기 차례 = 좁은 것 위 · 보라 선 맨 위(295-1)
|
||||
}
|
||||
|
||||
// 십자선(계획 노선 자리) · 축선 · 높이 축 이름 — `_Cross_View_Frame`(700줄 제한).
|
||||
|
||||
Reference in New Issue
Block a user