feat(B06 횡단): 교차점 = 사면처럼 — 올림 강조 · 제목 「사면 이동」 · 절·성토 없음 알림 · 판정 원 8 px (379-1)
- 보라 선 · 교차점 손잡이 · 암 경계선 마우스 올림 = 고른 모양(고름은 누를 때만) - 손잡이 제목 「사면 이동 (좌/우)」 · 노견 끝 = 원지반(±5 mm · edgeOnGround)이면 값 앞 「절·성토 없음 · 」 - 시험: 정확히 0 자리(멈춘 자리) 경우 — 절·성토 없음 · ◀↺▶ · 방향키 듣개 · 원 8 px Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
This commit is contained in:
@@ -37,7 +37,7 @@ import {
|
||||
type LineCellState,
|
||||
type RockCellSpec,
|
||||
} from "./B06_Section_UI_Cross_Line_Cell";
|
||||
import { extendStop, toePoints } from "./logic/B06_Section_UI_Cross_Fit";
|
||||
import { edgeOnGround, extendStop, toePoints } from "./logic/B06_Section_UI_Cross_Fit";
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
const DESIGN_CLASS = "b06-chart__design-cross";
|
||||
@@ -379,6 +379,7 @@ export function designLineMoves(
|
||||
berm?: BermDepthControl;
|
||||
center?: number;
|
||||
toes?: ReturnType<typeof toePoints>;
|
||||
bare?: ReturnType<typeof edgeOnGround>;
|
||||
rock?: RockCellSpec;
|
||||
} {
|
||||
const design = section.design;
|
||||
@@ -400,6 +401,7 @@ export function designLineMoves(
|
||||
stopAt: (side, now, next) => extendStop(section.samples, design.road_edges, side, now, next),
|
||||
},
|
||||
toes: toePoints(section), // 교차점 손잡이(370-2)
|
||||
bare: edgeOnGround(section.samples, design.road_edges), // 절·성토 없는 쪽(379-1)
|
||||
berm: {
|
||||
set: (side, valueM) => send({ berm_depth: { side, valueM } }),
|
||||
// 끔은 이 측점이 켠 때만 — 다른 카드 소단을 고르며 옛 카드가 풀릴 때 새 켬을 안 지움
|
||||
@@ -438,6 +440,8 @@ export function attachDesignLinePick(input: {
|
||||
center?: number;
|
||||
/** 있으면 쪽마다 교차점 손잡이(설계선 ∩ 원지반 · 키 `toe:left|right` · 늘 보임 · 370-2). */
|
||||
toes?: ReturnType<typeof toePoints>;
|
||||
/** 있으면 쪽마다 노견 끝 = 원지반(절·성토 없음 · 379-1) — 손잡이 값 앞 알림. */
|
||||
bare?: ReturnType<typeof edgeOnGround>;
|
||||
/** 있으면 암 경계선 고름(키 `rock` · ▲▼↺ · 세로 끌기) · 안 고른 때 공용 칸 = 암(370-3). */
|
||||
rock?: RockCellSpec;
|
||||
}): DesignLinePick {
|
||||
@@ -500,7 +504,6 @@ export function attachDesignLinePick(input: {
|
||||
const toe = input.toes?.[at] as { offset_m: number; elevation_m: number };
|
||||
const hit = centerHit(x(toe.offset_m), toDisplayY(toe.elevation_m));
|
||||
hit.classList.add(TOE_CLASS);
|
||||
hit.setAttribute("r", "5");
|
||||
hits.push(hit);
|
||||
}
|
||||
// 암 경계선(370-3) — 점선(`appendRockBoundaryOverlay`) 위 판정 선 하나(끊긴 토막도 한 path) · 키 rock
|
||||
@@ -631,6 +634,7 @@ export function attachDesignLinePick(input: {
|
||||
const moved = extend > 0 ? ` · 이동 +${extendText(extend)}m` : "";
|
||||
const toe = toeSide ? input.toes?.[toeSide] : null;
|
||||
const dims = piece ? pieceLabel(design, piece).replace("\n", " · ") : "";
|
||||
const bare = toeSide && input.bare?.[toeSide] ? "절·성토 없음 · " : ""; // 379-1
|
||||
// 아래 띠 공용 칸(370-3) — 안 고르면 암(암 지반만) · 고르면 그 선 제목 · 버튼 · 값 한 줄
|
||||
const state: LineCellState | null =
|
||||
onRock || (!chosen && rock)
|
||||
@@ -639,8 +643,8 @@ export function attachDesignLinePick(input: {
|
||||
? { title: "중심점", value: centerPreviewText(0), buttons: sideways(false) }
|
||||
: toe
|
||||
? {
|
||||
title: `교차점 이동 ${sideName}`,
|
||||
value: `노선에서 ${Math.abs(toe.offset_m).toFixed(2)}m${moved}`,
|
||||
title: `사면 이동 ${sideName}`,
|
||||
value: `${bare}노선에서 ${Math.abs(toe.offset_m).toFixed(2)}m${moved}`,
|
||||
buttons: shift ? sideways(true) : {},
|
||||
}
|
||||
: !piece
|
||||
|
||||
@@ -170,6 +170,8 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 마우스 올림 = 고른 모양(379-1) · 고름은 누를 때만 */
|
||||
.b06-chart__design-hit:hover,
|
||||
.b06-chart__design-hit.is-active {
|
||||
stroke: color-mix(in srgb, var(--color-royal-amethyst) 45%, transparent);
|
||||
stroke-width: 7;
|
||||
@@ -193,21 +195,26 @@
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
/* 교차점 손잡이(370-2) — 사면 ∩ 원지반 작은 원 · 늘 보임 · 고르면 채움. */
|
||||
/* 교차점 손잡이(370-2) — 사면 ∩ 원지반 작은 원 · 늘 보임 · 고르면 채움 · 올리면 같은 모양(379-1). */
|
||||
.b06-chart__toe-hit {
|
||||
fill: var(--color-surface);
|
||||
stroke: var(--color-royal-amethyst);
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
|
||||
.b06-chart__toe-hit:hover,
|
||||
.b06-chart__toe-hit.is-active {
|
||||
fill: var(--color-royal-amethyst);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 암 경계선 고름(370-3) — 점선 위 투명 판정 선 · 고르면 경고색 띠 · 세로 끌기. */
|
||||
.b06-chart__design-hit.b06-chart__rock-hit:hover,
|
||||
.b06-chart__design-hit.b06-chart__rock-hit.is-active {
|
||||
stroke: color-mix(in srgb, var(--color-warning) 40%, transparent);
|
||||
}
|
||||
|
||||
.b06-chart__design-hit.b06-chart__rock-hit.is-active {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
|
||||
@@ -112,6 +112,19 @@ export function extendStop(
|
||||
return next;
|
||||
}
|
||||
|
||||
/** 절·성토 없는 쪽(379-1) — 노견 끝 = 원지반(±5 mm · 멈춤 반올림 0.001 m 를 덮음). 지반 모르면 false. */
|
||||
export function edgeOnGround(
|
||||
samples: CrossSection["samples"],
|
||||
edges: CrossDesign["road_edges"] | undefined,
|
||||
): Record<"left" | "right", boolean> {
|
||||
const groundAt = groundInterpolator(samples ?? []);
|
||||
const on = (side: "left" | "right") =>
|
||||
!!groundAt &&
|
||||
!!edges &&
|
||||
Math.abs(groundAt(edges[side].offset_m) - edges[side].elevation_m) <= 0.005;
|
||||
return { left: on("left"), right: on("right") };
|
||||
}
|
||||
|
||||
/**
|
||||
* 성토측 사면의 **본래** 경사길이(m) — 성토가 아닌 측은 null (2026-09-03 사용자 지시).
|
||||
*
|
||||
|
||||
@@ -107,11 +107,12 @@ class FakeNode {
|
||||
(this.listeners.click ?? []).forEach((fn) => fn(event));
|
||||
}
|
||||
}
|
||||
const docKeys = new Set(); // 방향키 듣개(379-1 · 정확히 0 자리)
|
||||
globalThis.document = {
|
||||
createElement: (tag) => new FakeNode(tag),
|
||||
createElementNS: (_ns, tag) => new FakeNode(tag),
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
addEventListener: (type, fn) => type === "keydown" && docKeys.add(fn),
|
||||
removeEventListener: (type, fn) => type === "keydown" && docKeys.delete(fn),
|
||||
};
|
||||
globalThis.window = { setTimeout, clearTimeout, addEventListener: () => {} };
|
||||
|
||||
@@ -302,4 +303,36 @@ out.inherit = {
|
||||
after.handle.setActive(afterIndex);
|
||||
out.inherit.label = cellOf(after.handle).title;
|
||||
|
||||
// ④ 정확히 0 자리(379-1) — 노견 끝 = 원지반(±5 mm = 절·성토 없음) · 손잡이 · 누르면 사면처럼 ◀↺▶ · 방향키
|
||||
// 조각 0 은 안 봄 — 그림 규칙(노견 끝 → 첫 접점 보호 구간)이 지반 위 짧은 선을 남김(브레인 A)
|
||||
const zero = section(out.stop.value);
|
||||
const zeroPicks = [];
|
||||
const zeroLayer = new FakeNode("g");
|
||||
appendCrossDesignOverlay(zeroLayer, zero.design, x, y, zero.samples, undefined);
|
||||
zeroLayer.children.forEach((el) => el.setAttribute(pick.DESIGN_LINE_ATTR, ""));
|
||||
const zeroHandle = pick.attachDesignLinePick({
|
||||
layer: zeroLayer,
|
||||
design: zero.design,
|
||||
x,
|
||||
toDisplayY: y,
|
||||
onPick: (i) => zeroPicks.push(i),
|
||||
...pick.designLineMoves(zero, () => undefined),
|
||||
});
|
||||
const zeroToes = zeroLayer.children.filter((k) => k.classList.contains("b06-chart__toe-hit"));
|
||||
const rightToe = zeroToes.find((t) => Number(t.getAttribute("cx")) > x(0));
|
||||
docKeys.clear();
|
||||
rightToe?.click();
|
||||
zeroHandle.setActive(zeroPicks[0]);
|
||||
const zeroCell = cellOf(zeroHandle);
|
||||
out.zero = {
|
||||
bare: pick.designLineMoves(zero, () => undefined).bare,
|
||||
nearBare: pick.designLineMoves(near, () => undefined).bare,
|
||||
toe: !!rightToe,
|
||||
radius: rightToe ? rightToe.getAttribute("r") : null,
|
||||
title: zeroCell.title,
|
||||
value: zeroCell.value,
|
||||
buttons: ["left", "reset", "right"].filter((slot) => zeroCell.button(slot)),
|
||||
keys: docKeys.size,
|
||||
};
|
||||
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""PLAN 370-2 — B06 사면 이동 멈춤 · 교차점 손잡이 · 고름 이어받기(요구 240-14).
|
||||
|
||||
짝: `B06_Section/logic/B06_Section_UI_Cross_Fit.ts`(`extendStop` · `toePoints`) ·
|
||||
짝: `B06_Section/logic/B06_Section_UI_Cross_Fit.ts`(`extendStop` · `toePoints` · `edgeOnGround`) ·
|
||||
`B06_Section/B06_Section_UI_Cross_Design_Pick.ts`(손잡이 · 키 이어받기). 실제 TS 를
|
||||
`helper_370_2_toe_stop.cjs` 가 Node 로 돌린다.
|
||||
"""
|
||||
@@ -63,7 +63,7 @@ def test_교차점_손잡이_쪽마다_늘_보임(out) -> None:
|
||||
def test_손잡이를_고르면_그_쪽_사면_좌우_이동(out) -> None:
|
||||
got = out["toePick"]
|
||||
assert got["active"] is True and got["panelOpen"] is True
|
||||
assert got["title"] == "교차점 이동 (우)" # 아래 띠 공용 칸(370-3)
|
||||
assert got["title"] == "사면 이동 (우)" # 아래 띠 공용 칸(370-3)
|
||||
assert got["value"].startswith("노선에서 ")
|
||||
assert len(got["sent"]) == 1 and got["sent"][0]["side"] == "right"
|
||||
low, high = out["stop"]["crossing"]
|
||||
@@ -76,4 +76,15 @@ def test_절에서_성으로_넘어가도_고름_유지(out) -> None:
|
||||
assert inherit["cutIndex"] >= 0 and inherit["before"] == inherit["cutIndex"]
|
||||
assert inherit["farMode"] == "left_cut" # 우 = 성토로 넘어감
|
||||
assert inherit["afterIndex"] is not None
|
||||
assert inherit["label"] == "교차점 이동 (우)"
|
||||
assert inherit["label"] == "사면 이동 (우)"
|
||||
|
||||
|
||||
def test_정확히_0_자리는_절성토_없음_사면처럼_고름(out) -> None:
|
||||
"""379-1 — 멈춘 자리(노견 끝 = 원지반) 손잡이를 누르면 사면과 같은 조작."""
|
||||
zero = out["zero"]
|
||||
assert zero["bare"]["right"] is True and zero["nearBare"]["right"] is False
|
||||
assert zero["toe"] is True and zero["radius"] == "8" # 판정 원 8 px
|
||||
assert zero["title"] == "사면 이동 (우)"
|
||||
assert zero["value"].startswith("절·성토 없음 · 노선에서 ")
|
||||
assert zero["buttons"] == ["left", "reset", "right"] # ◀↺▶
|
||||
assert zero["keys"] >= 1 # 방향키 듣개
|
||||
|
||||
Reference in New Issue
Block a user