fix(B05 평면 구조물 마커): 연달아 방향키 두 걸음 · 정보 창 법선 끝 밖 · 오른쪽 판 아래 (399)
- 관 목록이 늦게 따라와도 옮긴 자리 · 고름을 쥠(빠르게 두 번 = 2 m · 화면에서 한 번 빠지던 흠) - 정보 창 = 좌우 20 m 법선 끝 밖(최대 60 px) · z-index 1 로 오른쪽 횡단 판 아래 - 시험 test_399 늦게 오는 목록 갈래 더함 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
This commit is contained in:
@@ -51,6 +51,8 @@ const MARK_HALF_PX = 5;
|
||||
const MARK_WIDTH_PX = 8;
|
||||
/** 고른 마커 바깥 테 — 법선과 같은 하늘색(391). */
|
||||
const PICKED_RING = "#0ea5e9";
|
||||
/** 법선 좌우 길이(m) — `_Hover` 법선과 같은 값(391-1) · 정보 창을 그 밖으로. */
|
||||
const NORMAL_HALF_M = 20;
|
||||
|
||||
/** 종단 보기가 건네는 것 — 알약 목록 · 종류 · 측점 간격 · 고름 · 옮김(알약과 같은 길). */
|
||||
export interface PlanMarkSource {
|
||||
@@ -84,15 +86,22 @@ interface PlanMark {
|
||||
name: string;
|
||||
}
|
||||
|
||||
/** 옮기는 중(끌기 · 방향키 누름) — 놓을 때 · 뗄 때 `to` 로 한 번 옮김. 창(캔버스)마다 따로. */
|
||||
/** 옮기는 중(끌기 · 방향키 누름) — 놓을 때 · 뗄 때 `to` 로 한 번 옮김. 창(캔버스)마다 따로.
|
||||
* `done` = 옮겼고 알약 목록이 아직 안 따라옴(관 목록은 늦게 옴) — 따라올 때까지 그 자리 · 그 고름 그대로. */
|
||||
interface Moving {
|
||||
id: string;
|
||||
name: string;
|
||||
from: number;
|
||||
to: number;
|
||||
totalM: number;
|
||||
done?: true;
|
||||
}
|
||||
const movings = new WeakMap<HTMLCanvasElement, Moving | null>();
|
||||
/** 끄는 중 · 누르는 중인 것(옮긴 뒤 기다리는 것은 아님). */
|
||||
const activeMoving = (canvas: HTMLCanvasElement): Moving | null => {
|
||||
const moving = movings.get(canvas);
|
||||
return moving && !moving.done ? moving : null;
|
||||
};
|
||||
const balloons = new WeakMap<HTMLCanvasElement, { key: string; element: HTMLElement }>();
|
||||
|
||||
const lengthOf = (line: ReadonlyArray<Vertex>): number =>
|
||||
@@ -112,7 +121,12 @@ function planMarks(
|
||||
const list = laneStructures(source.marks());
|
||||
const types = new Map(source.types().map((type) => [type.type_id, type]));
|
||||
const texts = structurePillTexts(list, types);
|
||||
const moving = movings.get(canvas) ?? null;
|
||||
const held = movings.get(canvas) ?? null;
|
||||
const lagging = list.some(
|
||||
(structure) => structure.structure_id === held?.id && structureAnchorM(structure) !== held?.to,
|
||||
);
|
||||
if (held?.done && !lagging) movings.set(canvas, null); // 목록이 따라옴(같은 id 가 없어도 끝)
|
||||
const moving = held?.done && !lagging ? null : held;
|
||||
const marks = list
|
||||
.filter((structure) => structure.structure_id)
|
||||
.map((structure): PlanMark => {
|
||||
@@ -210,6 +224,7 @@ function showBalloon(
|
||||
const next = balloonElement(lines, mark.type);
|
||||
next.classList.add("is-center");
|
||||
next.style.pointerEvents = "none";
|
||||
next.style.zIndex = "1"; // 오른쪽 횡단 판(z 2) · 펼친 판 아래로 — 지도 위에만
|
||||
if (element) element.replaceWith(next);
|
||||
else canvas.parentElement.append(next);
|
||||
element = next;
|
||||
@@ -226,8 +241,12 @@ function showBalloon(
|
||||
element.style.left = `${x}px`;
|
||||
element.style.top = `${y}px`;
|
||||
element.style.bottom = "auto";
|
||||
// 법선(좌우 20 m) 끝 밖으로 띄움 — 크게 확대해도 60 px 넘게는 안 떨어짐
|
||||
const gap = Math.min(NORMAL_HALF_M * scene.pxPerMeter, 60) + 10;
|
||||
element.style.transform =
|
||||
y < element.offsetHeight + 24 ? "translate(-50%, 14px)" : "translate(-50%, calc(-100% - 14px))";
|
||||
y < element.offsetHeight + gap + 10
|
||||
? `translate(-50%, ${gap}px)`
|
||||
: `translate(-50%, calc(-100% - ${gap}px))`;
|
||||
}
|
||||
|
||||
export interface StructureMarkInput {
|
||||
@@ -280,9 +299,9 @@ export function bindStructureMarks(
|
||||
};
|
||||
/** 놓음 · 뗌 — 옮겼으면 알약 끌기와 같은 길로 한 번. */
|
||||
const finish = (): boolean => {
|
||||
const moving = movings.get(canvas);
|
||||
const moving = activeMoving(canvas);
|
||||
if (!moving) return false;
|
||||
movings.set(canvas, null);
|
||||
movings.set(canvas, moving.to !== moving.from ? { ...moving, done: true } : null);
|
||||
if (moving.to !== moving.from) {
|
||||
sources.get(draftProjectId())?.move(moving.id, moving.to);
|
||||
say(moving, true);
|
||||
@@ -336,7 +355,7 @@ export function bindStructureMarks(
|
||||
const target = event.target as HTMLElement | null;
|
||||
if (target?.closest?.("input, select, textarea, [contenteditable='true']")) return;
|
||||
if (event.repeat || holdKey || dragging() || params.measureMode()) return;
|
||||
if (params.state().picked >= 0 || movings.get(canvas)) return; // 교각점 방향키 몫
|
||||
if (params.state().picked >= 0 || activeMoving(canvas)) return; // 교각점 방향키 몫
|
||||
const found = planMarks(canvas);
|
||||
const mark = found?.marks.find((entry) => entry.id === found.picked);
|
||||
if (!mark) return;
|
||||
@@ -367,7 +386,7 @@ export function bindStructureMarks(
|
||||
return true;
|
||||
},
|
||||
move(px, py) {
|
||||
const moving = movings.get(canvas);
|
||||
const moving = activeMoving(canvas);
|
||||
if (!moving || holdKey) return false;
|
||||
const hit = routePointAtScreen(lineNow(), params.toScreen, px, py, Infinity);
|
||||
const near = hit ? markChainage(hit.chainageM, moving.totalM) : moving.to;
|
||||
|
||||
@@ -134,6 +134,7 @@ const types = [
|
||||
{ type_id: "box", name: "BOX", style: { color: "#e11d48" } },
|
||||
{ type_id: "pipe", name: "배수관", style: { color: "#2563eb" } },
|
||||
];
|
||||
let lag = false;
|
||||
const selects = [];
|
||||
const moves = [];
|
||||
let redrawn = 0;
|
||||
@@ -142,20 +143,35 @@ marks.setPlanMarkSource("p1", {
|
||||
types: () => types,
|
||||
intervalM: () => 20,
|
||||
select: (id) => selects.push(id),
|
||||
move: (id, to) => moves.push([id, to]),
|
||||
// 옮김 = 알약 목록이 그 자리로(`lag` 면 늦게 옴 — 관 목록)
|
||||
move: (id, to) => {
|
||||
moves.push([id, to]);
|
||||
if (!lag) structures.find((s) => s.structure_id === id).chainage_m = to;
|
||||
},
|
||||
});
|
||||
|
||||
const canvas = { isConnected: true, parentElement: parent };
|
||||
let line = EAST;
|
||||
let picked = -1;
|
||||
const status = { textContent: "" };
|
||||
const scene = () => ({ toScreen, view: { width: 400, height: 300 }, rotationRad: 0 });
|
||||
const scene = () => ({
|
||||
toScreen,
|
||||
pxPerMeter: 2,
|
||||
view: { width: 400, height: 300 },
|
||||
rotationRad: 0,
|
||||
});
|
||||
const style = { getPropertyValue: () => "" };
|
||||
const draw = () => {
|
||||
const fake = fakeContext(canvas);
|
||||
marks.drawStructureMarks(fake.ctx, scene(), line, style);
|
||||
return fake.log;
|
||||
};
|
||||
/** 한 걸음 판정 뒤 — 그려서 기다림을 풀고 처음 자리로. */
|
||||
const settle = () => {
|
||||
draw();
|
||||
structures[0].chainage_m = 40;
|
||||
structures[1].chainage_m = 80.17;
|
||||
};
|
||||
const input = marks.bindStructureMarks(
|
||||
{
|
||||
canvas,
|
||||
@@ -213,16 +229,19 @@ out.dragStatus = status.textContent;
|
||||
out.movesBeforeRelease = moves.length;
|
||||
input.release();
|
||||
out.dragMove = moves[moves.length - 1];
|
||||
settle();
|
||||
out.dragDoneStatus = status.textContent;
|
||||
// 노선 밖 — 끝(200) · 시점(0)에 붙음
|
||||
input.press(80, 0);
|
||||
input.move(9999, 0);
|
||||
input.release();
|
||||
out.pastEnd = moves[moves.length - 1];
|
||||
settle();
|
||||
input.press(80, 0);
|
||||
input.move(-500, 0);
|
||||
input.release();
|
||||
out.beforeStart = moves[moves.length - 1];
|
||||
settle();
|
||||
// 옛 소수 관(80.17) — 눌렀다 반올림 자리에서 떼면 안 옮김 · 1 m 끌면 정수
|
||||
const count = moves.length;
|
||||
input.press(160.34, 0);
|
||||
@@ -233,18 +252,22 @@ input.press(160.34, 0);
|
||||
input.move(162.2, 0);
|
||||
input.release();
|
||||
out.oldDecimalMoved = moves[moves.length - 1];
|
||||
settle();
|
||||
|
||||
// ④ 방향키 — 고른 마커(A) · 교각점 안 고름 = 노선 따라 1 m · 뗄 때 한 번
|
||||
selection = { chainageM: 40, structureId: "A" };
|
||||
key("keydown", "ArrowRight");
|
||||
key("keyup", "ArrowRight");
|
||||
out.keyRight = moves[moves.length - 1];
|
||||
settle();
|
||||
key("keydown", "ArrowLeft");
|
||||
key("keyup", "ArrowLeft");
|
||||
out.keyLeft = moves[moves.length - 1];
|
||||
settle();
|
||||
key("keydown", "ArrowUp"); // 노선과 직각 = → · ↑ 는 +
|
||||
key("keyup", "ArrowUp");
|
||||
out.keyUpPerpendicular = moves[moves.length - 1];
|
||||
settle();
|
||||
// 서→동이 아니라 동→서 노선 — 오른쪽(동) = 뒤로
|
||||
line = [
|
||||
[200, 0],
|
||||
@@ -253,7 +276,27 @@ line = [
|
||||
key("keydown", "ArrowRight");
|
||||
key("keyup", "ArrowRight");
|
||||
out.keyRightWestward = moves[moves.length - 1];
|
||||
settle();
|
||||
line = EAST;
|
||||
// 관 목록이 늦게 옴 — 연달아 두 번 눌러도 두 걸음(옮긴 자리 · 고름을 목록이 따라올 때까지 쥠)
|
||||
lag = true;
|
||||
selection = { chainageM: 80.17, structureId: null };
|
||||
key("keydown", "ArrowRight");
|
||||
key("keyup", "ArrowRight");
|
||||
selection = { chainageM: 81, structureId: null }; // 관 고름이 새 자리로(followMovedPipe)
|
||||
const heldLog = draw();
|
||||
out.lagHeldAt = heldLog.filter((s) => s.width === 15).map((s) => s.at);
|
||||
key("keydown", "ArrowRight");
|
||||
key("keyup", "ArrowRight");
|
||||
out.lagTwoSteps = moves.slice(-2);
|
||||
lag = false;
|
||||
structures[1].chainage_m = 82; // 목록이 따라옴
|
||||
selection = { chainageM: 82, structureId: null };
|
||||
out.lagCaught = draw()
|
||||
.filter((s) => s.width === 15)
|
||||
.map((s) => s.at);
|
||||
settle();
|
||||
selection = { chainageM: 40, structureId: "A" };
|
||||
// 교각점 고름 = 교각점 방향키 몫(마커 안 옮김)
|
||||
picked = 2;
|
||||
const before = moves.length;
|
||||
|
||||
@@ -102,6 +102,12 @@ def test_방향키_노선_따라_1m(got) -> None:
|
||||
assert got["keyUpPerpendicular"] == ["A", 41] # 직각 = → · ↑ 가 +
|
||||
assert got["keyRightWestward"] == ["A", 39] # 노선이 서쪽으로 가면 → = 뒤로
|
||||
assert got["keyWithNodePicked"] == 0 # 교각점 고름 = 교각점 방향키 몫
|
||||
|
||||
|
||||
def test_관_목록이_늦게_와도_연달아_두_걸음(got) -> None:
|
||||
assert got["lagHeldAt"] == [[162, 0]] # 옮긴 자리 · 고름 그대로(목록은 아직 80.17)
|
||||
assert got["lagTwoSteps"] == [["pipe-80.17", 81], ["pipe-80.17", 82]]
|
||||
assert got["lagCaught"] == [[164, 0]] # 목록이 따라오면 목록 자리
|
||||
assert got["keyNoPick"] == 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user