- 433-14 ③ · 433-21 ③ 누름 · 맴돔 · 커서 한 판정(pressWinner) — 노드 가운데 6 px = 노드 · 밖 = 마커 · 노드 · 손잡이 중 가까운 것 - 433-21 ① 손잡이 끌기 = 노드 갈래와 같게 — 맞추기 전 자리에서 셈 · 미리보기 맞춤 · 측점 밖이 늘면 그 걸음 되돌림 - 433-14 ① 막힌 걸음만 하고 놓으면 미리보기 맞춤을 누르기 전 자리로 - 433-14 ② 사진첩 다리도 스냅 · 분석 지형 바뀜을 봄 — 노선이 처음으로 돌아가도 초안 - 433-1 test_365 ⑧ 이름 = 노선 한 걸음 + 계획고 맞춤 - 시험 test_453_plan_pick_draft 새로 · 390_5 손잡이 훑기 · 381_3 · 399 · 409 · 430 배선 글 맞춤 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
433 lines
18 KiB
TypeScript
433 lines
18 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile_UI_RouteEdit_StructureMarks.ts
|
|
* 평면 **구조물 측점 마커**(PLAN 399 · 요구 250-2).
|
|
*
|
|
* · 목록 = 종단 아래 알약과 같은 것(`laneStructures` · 관 · 구조물 · 물넘이 · 사토장 …) · 노선 위 그 측점에
|
|
* 알약 꼴(종류 색) · 일반 측점 눈금 · 스냅 · 간격과 무관.
|
|
* · 고름 = 알약 고름과 같은 길(`select` · 공용 고름 382-2 — 평면 마커 고름 = 종단 알약 강조 · 반대도).
|
|
* 고른 마커 = 알약 벌룬과 같은 창(`balloonElement` · `balloonLines`) + 그 측점 좌우 20 m 법선(391).
|
|
* · 끌기 · 방향키(마커 고른 때 · 교각점 안 고른 때) = 노선 따라 정수 m · 1 m 씩 · 시점 · 종점 밖 안 감 ·
|
|
* 놓을 때 · 뗄 때 한 번 옮김(`move` = 알약 끌기와 같은 길 · 관 · 구조물 초안 · [저장]이 정본).
|
|
* · 그림 · 집기 차례 = 맨 위 · 맨 앞(409 · 재기 다음) — `_Render` · `_Pointer` 가 부름.
|
|
* · 고름 풀기 = 마커 아닌 곳 누름 · Esc(409) — 공용 고름을 `PLAN_PICK_FROM` 으로 비움(오른쪽 횡단 판 그대로 ·
|
|
* 좌측 목록 강조 · 폼은 풂 409-2).
|
|
* 목록 · 고름 · 옮김은 종단 보기가 `setPlanMarkSource` 로 건넴. 걸음 셈은 `logic/…_StructureMarks`.
|
|
* ========================================================================== */
|
|
|
|
import { pickPx } from "../A00_Common/touch_bridge";
|
|
import { draftProjectId } from "../A00_Common/design/design_store";
|
|
import {
|
|
NO_SELECTION,
|
|
PIPE_MATCH_M,
|
|
readSelection,
|
|
selectDesign,
|
|
} from "../A00_Common/design/design_selection";
|
|
import { lastPointerWithin } from "../B06_Section/B06_Section_UI_Cross_Panel_Base";
|
|
import {
|
|
structureAnchorM,
|
|
type StructureInstance,
|
|
type StructureType,
|
|
} from "./B05_Profile_Api_Structures";
|
|
import {
|
|
balloonElement,
|
|
laneStructures,
|
|
positionText,
|
|
structurePillTexts,
|
|
} from "./B05_Profile_UI_Structures_Marks";
|
|
import { balloonLines } from "./logic/B05_Profile_UI_Structures_Balloon";
|
|
import { HOLD_DELAY_MS, HOLD_INTERVAL_MS } from "./B05_Profile_UI_Profile_Edit";
|
|
import { drawStationNormal } from "./B05_Profile_UI_RouteEdit_Hover";
|
|
import { routePointAtScreen } from "./B05_Profile_UI_RouteEdit_Input";
|
|
import type { RouteEditPointerParams } from "./B05_Profile_UI_RouteEdit_Pointer";
|
|
import { PLAN_PICK_FROM } from "../A00_Common/design/design_pick_escape";
|
|
import type { RouteEditScene } from "./B05_Profile_UI_RouteEdit_Render";
|
|
import { stationFrameAt } from "./logic/B05_Profile_UI_RouteEdit_Frame";
|
|
import type { Vertex } from "./logic/B05_Profile_UI_RouteEdit_Curve";
|
|
import { arrowStep, isArrowKey, type ArrowKey } from "./logic/B05_Profile_RouteEdit_KeyNudge";
|
|
import {
|
|
arrowSign,
|
|
markChainage,
|
|
markStep,
|
|
pickedMarkId,
|
|
} from "./logic/B05_Profile_RouteEdit_StructureMarks";
|
|
import { stationLabel as formatStation } from "../common_util/common_util_station";
|
|
|
|
/** 마커를 잡았다고 볼 거리(px). */
|
|
const MARK_HIT_PX = 10;
|
|
/** 알약 마커 반 길이 · 굵기(px) — 고르면 조금 크게. */
|
|
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 {
|
|
marks: () => ReadonlyArray<StructureInstance>;
|
|
types: () => ReadonlyArray<StructureType>;
|
|
intervalM: () => number;
|
|
select: (structureId: string) => void;
|
|
move: (structureId: string, toChainageM: number) => void;
|
|
}
|
|
|
|
const sources = new Map<string, PlanMarkSource>();
|
|
/** 목록이 바뀌면 다시 그릴 편집 창(프로젝트마다). */
|
|
const redraws = new Map<string, Set<() => void>>();
|
|
|
|
/** 알약 목록이 바뀜(추가 · 옮김 · 노선 따라감) — 그 프로젝트 편집 창을 다시 그림. */
|
|
export function planMarksChanged(projectId: string): void {
|
|
redraws.get(projectId)?.forEach((redraw) => redraw());
|
|
}
|
|
|
|
/** 프로젝트의 마커 재료를 건넴 — 돌려준 함수로 뗌(보기를 떠날 때). */
|
|
export function setPlanMarkSource(projectId: string, source: PlanMarkSource): () => void {
|
|
sources.set(projectId, source);
|
|
return () => void (sources.get(projectId) === source && sources.delete(projectId));
|
|
}
|
|
|
|
interface PlanMark {
|
|
id: string;
|
|
chainageM: number;
|
|
structure: StructureInstance;
|
|
type: StructureType | undefined;
|
|
name: string;
|
|
}
|
|
|
|
/** 옮기는 중(끌기 · 방향키 누름) — 놓을 때 · 뗄 때 `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 =>
|
|
line.reduce(
|
|
(sum, vertex, index) =>
|
|
index ? sum + Math.hypot(vertex[0] - line[index - 1][0], vertex[1] - line[index - 1][1]) : 0,
|
|
0,
|
|
);
|
|
|
|
/** 지금 마커 목록(옮기는 것은 그 자리) · 고른 것 — 재료가 없으면 null. */
|
|
function planMarks(
|
|
canvas: HTMLCanvasElement,
|
|
): { source: PlanMarkSource; marks: PlanMark[]; picked: string | null } | null {
|
|
const projectId = draftProjectId();
|
|
const source = sources.get(projectId);
|
|
if (!source) return null;
|
|
const list = laneStructures(source.marks());
|
|
const types = new Map(source.types().map((type) => [type.type_id, type]));
|
|
const texts = structurePillTexts(list, types);
|
|
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 => {
|
|
const id = structure.structure_id as string;
|
|
return {
|
|
id,
|
|
chainageM: moving?.id === id ? moving.to : structureAnchorM(structure),
|
|
structure,
|
|
type: types.get(structure.type_id),
|
|
name: texts.get(id)?.name ?? structure.type_id,
|
|
};
|
|
});
|
|
// 옮기는 중엔 그것 — 관 고름은 누가거리만 들어 옮긴 자리로는 못 찾음
|
|
const picked = moving?.id ?? pickedMarkId(marks, readSelection(projectId), PIPE_MATCH_M);
|
|
return { source, marks, picked };
|
|
}
|
|
|
|
/** 알약 꼴 마커 한 개 — 노선 방향으로 누운 둥근 막대(종류 색 · 흰 테 · 고르면 하늘 테). */
|
|
function paintMark(
|
|
context: CanvasRenderingContext2D,
|
|
scene: RouteEditScene,
|
|
line: ReadonlyArray<Vertex>,
|
|
mark: PlanMark,
|
|
picked: boolean,
|
|
halo: string,
|
|
): void {
|
|
const frame = stationFrameAt(line, mark.chainageM);
|
|
if (!frame) return;
|
|
const [x, y] = scene.toScreen(frame.origin);
|
|
const [fx, fy] = scene.toScreen([
|
|
frame.origin[0] + frame.left[1],
|
|
frame.origin[1] - frame.left[0],
|
|
]); // 앞쪽 1 m(왼쪽 축을 시계로 90°)
|
|
const half = picked ? MARK_HALF_PX + 2 : MARK_HALF_PX;
|
|
context.save();
|
|
context.translate(x, y);
|
|
context.rotate(Math.atan2(fy - y, fx - x));
|
|
context.lineCap = "round";
|
|
const stroke = (color: string, width: number): void => {
|
|
context.strokeStyle = color;
|
|
context.lineWidth = width;
|
|
context.beginPath();
|
|
context.moveTo(-half, 0);
|
|
context.lineTo(half, 0);
|
|
context.stroke();
|
|
};
|
|
if (picked) stroke(PICKED_RING, MARK_WIDTH_PX + 7);
|
|
stroke(halo, MARK_WIDTH_PX + 3);
|
|
stroke(mark.type?.style?.color ?? "#888", MARK_WIDTH_PX);
|
|
context.restore();
|
|
}
|
|
|
|
/** 마커 · 고른 마커 법선 · 정보 창 — `_Render` 가 맨 마지막에 부름(409). */
|
|
export function drawStructureMarks(
|
|
context: CanvasRenderingContext2D,
|
|
scene: RouteEditScene,
|
|
line: ReadonlyArray<Vertex>,
|
|
style: CSSStyleDeclaration,
|
|
): void {
|
|
const canvas = context.canvas;
|
|
const found = line.length >= 2 ? planMarks(canvas) : null;
|
|
const halo = style.getPropertyValue("--map-halo") || "rgba(255,255,255,0.9)";
|
|
const picked = found?.marks.find((mark) => mark.id === found.picked) ?? null;
|
|
found?.marks.forEach(
|
|
(mark) => mark !== picked && paintMark(context, scene, line, mark, false, halo),
|
|
);
|
|
if (picked) {
|
|
drawStationNormal(context, scene, line, picked.chainageM);
|
|
paintMark(context, scene, line, picked, true, halo);
|
|
}
|
|
showBalloon(canvas, scene, line, picked && found ? { mark: picked, source: found.source } : null);
|
|
}
|
|
|
|
/** 고른 마커 위 정보 창 — 알약 벌룬 그대로 · 마커 위(위가 모자라면 아래) · 누름은 지나감. */
|
|
function showBalloon(
|
|
canvas: HTMLCanvasElement,
|
|
scene: RouteEditScene,
|
|
line: ReadonlyArray<Vertex>,
|
|
pick: { mark: PlanMark; source: PlanMarkSource } | null,
|
|
): void {
|
|
const kept = balloons.get(canvas);
|
|
const frame = pick && stationFrameAt(line, pick.mark.chainageM);
|
|
if (!pick || !frame || !canvas.parentElement) {
|
|
kept?.element.remove();
|
|
balloons.delete(canvas);
|
|
return;
|
|
}
|
|
const { mark } = pick;
|
|
const structure = { ...mark.structure, chainage_m: mark.chainageM };
|
|
const position = positionText(structure, pick.source.intervalM());
|
|
const lines = balloonLines(structure, mark.type, { name: mark.name, position });
|
|
const key = JSON.stringify([lines, mark.type?.style?.color]);
|
|
let element = kept?.element;
|
|
if (!element || kept?.key !== key) {
|
|
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;
|
|
balloons.set(canvas, { key, element });
|
|
}
|
|
// 그림은 캔버스 가운데를 축으로 돎 — 창 자리도 같은 만큼 돌림
|
|
const [sx, sy] = scene.toScreen(frame.origin);
|
|
const cx = scene.view.width / 2;
|
|
const cy = scene.view.height / 2;
|
|
const cos = Math.cos(scene.rotationRad);
|
|
const sin = Math.sin(scene.rotationRad);
|
|
const x = cx + (sx - cx) * cos - (sy - cy) * sin;
|
|
const y = cy + (sx - cx) * sin + (sy - cy) * cos;
|
|
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 + gap + 10
|
|
? `translate(-50%, ${gap}px)`
|
|
: `translate(-50%, calc(-100% - ${gap}px))`;
|
|
}
|
|
|
|
export interface StructureMarkInput {
|
|
/** 마커를 눌렀으면 고르고 끌기 시작 · true(재기 다음 맨 앞 · 409). */
|
|
press: (px: number, py: number) => boolean;
|
|
/** 끄는 중이면 자리 바꿈 · true. */
|
|
move: (px: number, py: number) => boolean;
|
|
/** 끄는 중이었으면 놓음(옮겼으면 한 번 옮김) · true. */
|
|
release: () => boolean;
|
|
/** 가장 가까운 마커까지 화면 거리(px) — 없으면 Infinity(누름 · 맴돔 · 커서가 노드 · 손잡이와 견줌 433-21 ③). */
|
|
distance: (px: number, py: number) => number;
|
|
/** 이 평면 마커가 고른 것이면 풂 · true(409 — 마커 아닌 곳 누름 · 다른 화면이 고른 것은 그대로 · 440). */
|
|
deselect: () => boolean;
|
|
}
|
|
|
|
/** 끌기 · 방향키 배선 — `_Pointer` 가 부름(같은 재료 `params`). */
|
|
export function bindStructureMarks(
|
|
params: RouteEditPointerParams,
|
|
dragging: () => boolean,
|
|
): StructureMarkInput {
|
|
const { canvas, status } = params;
|
|
const projectId = draftProjectId();
|
|
/** 이 평면 마커 누름이 고른 것(빈 곳 누름이 풀 것 · 440). */
|
|
let ownPick: string | null = null;
|
|
const list = redraws.get(projectId) ?? new Set<() => void>();
|
|
redraws.set(projectId, list);
|
|
const redraw = (): void => void (canvas.isConnected ? params.draw() : list.delete(redraw));
|
|
list.add(redraw);
|
|
const lineNow = (): Vertex[] => {
|
|
const state = params.state();
|
|
return state.plannedLine.length ? state.plannedLine : state.planned;
|
|
};
|
|
const markAt = (px: number, py: number): { mark: PlanMark; distance: number } | null => {
|
|
const found = planMarks(canvas);
|
|
const line = lineNow();
|
|
let best: PlanMark | null = null;
|
|
let bestDistance = pickPx(MARK_HIT_PX);
|
|
for (const mark of found?.marks ?? []) {
|
|
const frame = stationFrameAt(line, mark.chainageM);
|
|
if (!frame) continue;
|
|
const [x, y] = params.toScreen(frame.origin);
|
|
const distance = Math.hypot(x - px, y - py);
|
|
if (distance <= bestDistance) [best, bestDistance] = [mark, distance];
|
|
}
|
|
return best ? { mark: best, distance: bestDistance } : null;
|
|
};
|
|
const say = (moving: Moving, done: boolean): void => {
|
|
const interval = sources.get(draftProjectId())?.intervalM() ?? params.stationIntervalM;
|
|
const shift = moving.to - moving.from;
|
|
status.textContent =
|
|
`${params.routeHead()} — 구조물 ${moving.name} ${formatStation(moving.from, interval)} → ` +
|
|
`${formatStation(moving.to, interval)}(${shift > 0 ? "+" : ""}${shift} m)` +
|
|
(done ? " 옮김 — [저장]에서 정본." : " 옮기는 중.");
|
|
};
|
|
/** 놓음 · 뗌 — 옮겼으면 알약 끌기와 같은 길로 한 번. */
|
|
const finish = (): boolean => {
|
|
const moving = activeMoving(canvas);
|
|
if (!moving) return false;
|
|
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);
|
|
}
|
|
params.draw();
|
|
return true;
|
|
};
|
|
const start = (mark: PlanMark): Moving => {
|
|
const moving = {
|
|
id: mark.id,
|
|
name: mark.name,
|
|
from: mark.chainageM,
|
|
to: mark.chainageM,
|
|
totalM: lengthOf(lineNow()),
|
|
};
|
|
movings.set(canvas, moving);
|
|
return moving;
|
|
};
|
|
|
|
/** 고름 풂 — 공용 고름을 비움 · 오른쪽 횡단 판은 제 이름(`PLAN_PICK_FROM`)이라 안 비움. */
|
|
const deselect = (): boolean => {
|
|
if (!planMarks(canvas)?.picked) return false;
|
|
selectDesign(projectId, NO_SELECTION, { from: PLAN_PICK_FROM });
|
|
ownPick = null;
|
|
params.draw();
|
|
return true;
|
|
};
|
|
|
|
// 방향키 — 마커를 골랐고 교각점을 안 골랐을 때 · 누름 ~ 뗌 한 번이 한 옮김(교각점 방향키와 같은 반복)
|
|
let holdKey: ArrowKey | null = null;
|
|
let timer = 0;
|
|
const metricAt = (x: number, y: number): Vertex => params.toMetric(...params.unrotate(x, y));
|
|
const step = (moving: Moving, key: ArrowKey): void => {
|
|
const frame = stationFrameAt(lineNow(), moving.to);
|
|
if (!frame) return;
|
|
const tangent: Vertex = [frame.left[1], -frame.left[0]];
|
|
moving.to = markStep(
|
|
moving.to,
|
|
arrowSign(key, arrowStep(key, false, metricAt), tangent),
|
|
moving.totalM,
|
|
);
|
|
say(moving, false);
|
|
params.draw();
|
|
};
|
|
const release = (): void => {
|
|
window.clearTimeout(timer);
|
|
window.clearInterval(timer);
|
|
if (holdKey === null) return;
|
|
holdKey = null;
|
|
finish();
|
|
};
|
|
const detach = (): void => {
|
|
window.removeEventListener("keydown", onKeyDown, true);
|
|
window.removeEventListener("keyup", onKeyUp, true);
|
|
window.removeEventListener("blur", release);
|
|
};
|
|
function onKeyDown(event: KeyboardEvent): void {
|
|
if (!canvas.isConnected) return detach();
|
|
const escape = event.key === "Escape";
|
|
if ((!escape && !isArrowKey(event.key)) || !lastPointerWithin(".b05-routeedit")) return;
|
|
const target = event.target as HTMLElement | null;
|
|
if (target?.closest?.("input, select, textarea, [contenteditable='true']")) return;
|
|
// Esc = 마커 고름만 풂(공용 Esc 풀기는 오른쪽 판까지 비우므로 앞에서 받음)
|
|
if (escape) return void (!activeMoving(canvas) && deselect() && event.preventDefault());
|
|
if (event.repeat || holdKey || dragging() || params.measureMode()) 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;
|
|
event.preventDefault();
|
|
const moving = start(mark);
|
|
const key = (holdKey = event.key);
|
|
step(moving, key);
|
|
timer = window.setTimeout(() => {
|
|
timer = window.setInterval(() => holdKey === key && step(moving, key), HOLD_INTERVAL_MS);
|
|
}, HOLD_DELAY_MS);
|
|
}
|
|
function onKeyUp(event: KeyboardEvent): void {
|
|
if (!canvas.isConnected) return detach();
|
|
if (event.key === holdKey) release();
|
|
}
|
|
window.addEventListener("keydown", onKeyDown, true);
|
|
window.addEventListener("keyup", onKeyUp, true);
|
|
window.addEventListener("blur", release);
|
|
|
|
return {
|
|
press(px, py) {
|
|
const mark = markAt(px, py)?.mark;
|
|
if (!mark) return false;
|
|
release();
|
|
planMarks(canvas)?.source.select(mark.id); // 알약 고름과 같은 길(공용 고름)
|
|
ownPick = mark.id;
|
|
start(mark);
|
|
params.draw();
|
|
return true;
|
|
},
|
|
move(px, py) {
|
|
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;
|
|
// 옛 소수 자리(80.17)는 반올림 자리(80)에 머무는 동안 그대로 — 눌렀다 떼기만 해도 옮겨지지 않게
|
|
const to = near === Math.round(moving.from) ? moving.from : near;
|
|
if (to !== moving.to) {
|
|
moving.to = to;
|
|
say(moving, false);
|
|
params.draw();
|
|
}
|
|
return true;
|
|
},
|
|
release: () => !holdKey && finish(),
|
|
distance: (px, py) => markAt(px, py)?.distance ?? Infinity,
|
|
// 빈 곳 누름 = 이 마커가 고른 것만 풂 — B06 카드 · 목록 · 알약이 고른 것은 그대로(440) · Esc 는 앞 그대로
|
|
deselect: () => planMarks(canvas)?.picked === ownPick && deselect(),
|
|
};
|
|
}
|