Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_5
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
/* =============================================================================
|
||||
* key_shortcut.ts — Ctrl(⌘)+글자 단축키 판정 공용(PLAN 214-2)
|
||||
* 한글 자판은 `key` 가 「ㅋ」·「ㅛ」로 와 글자로는 안 맞음 — 영문 글자·숫자면 그 글자 ·
|
||||
* 아니면 자판 자리(`code`: KeyZ · Digit1)로 판정. Shift 의 대문자도 소문자로 맞춤.
|
||||
* 쓰는 곳 — 페이지(B05 · M02 · 시트 키) + CAD(`B07_DesignDetail/openwebcad` 의 `input-shortcut.ts` 가 다시 내보냄).
|
||||
* 시험 = `resources/tester/cad/ctrl-shortcut-hangul.test.ts`
|
||||
* ========================================================================== */
|
||||
|
||||
type KeyLike = Pick<KeyboardEvent, "code"> & Partial<Pick<KeyboardEvent, "key">>;
|
||||
|
||||
/** 누른 글자·숫자(소문자) — 글자·숫자 자리가 아니면 빈 글 */
|
||||
export function keyLetter(evt: KeyLike): string {
|
||||
const key = evt.key ?? "";
|
||||
if (/^[a-zA-Z0-9]$/.test(key)) return key.toLowerCase();
|
||||
const m = /^(?:Key([A-Z])|Digit([0-9]))$/.exec(evt.code ?? "");
|
||||
return m ? (m[1] ?? m[2]).toLowerCase() : "";
|
||||
}
|
||||
|
||||
/** Ctrl(Mac ⌘)과 함께 누른 `letter`(소문자 글자·숫자)인가 — Shift · Alt 는 보지 않음 */
|
||||
export function isCtrlKey(
|
||||
evt: KeyLike & Pick<KeyboardEvent, "ctrlKey" | "metaKey">,
|
||||
letter: string,
|
||||
): boolean {
|
||||
return (evt.ctrlKey || evt.metaKey) && keyLetter(evt) === letter;
|
||||
}
|
||||
|
||||
/** Ctrl 단축키 이름 */
|
||||
export type CtrlShortcut = "undo" | "redo" | "save" | "selectAll";
|
||||
|
||||
/** 공용 Ctrl 단축키 — Z · Shift+Z · Y · S · A (Alt 함께는 아님 · Shift 는 Z 만) */
|
||||
export function ctrlShortcut(
|
||||
evt: KeyLike &
|
||||
Pick<KeyboardEvent, "ctrlKey" | "altKey" | "shiftKey"> &
|
||||
Partial<Pick<KeyboardEvent, "metaKey">>,
|
||||
): CtrlShortcut | null {
|
||||
if (!(evt.ctrlKey || evt.metaKey) || evt.altKey) return null;
|
||||
switch (keyLetter(evt)) {
|
||||
case "z":
|
||||
return evt.shiftKey ? "redo" : "undo";
|
||||
case "y":
|
||||
return evt.shiftKey ? null : "redo";
|
||||
case "s":
|
||||
return evt.shiftKey ? null : "save";
|
||||
case "a":
|
||||
return evt.shiftKey ? null : "selectAll";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
* 복사 · 붙여넣기(Ctrl+C/X/V)는 막지 않음 — E 클립보드가 copy · paste 사건으로 받음.
|
||||
* ========================================================================== */
|
||||
|
||||
import { keyLetter } from "../key_shortcut";
|
||||
import { cycleAbs } from "./spreadsheet_editor";
|
||||
import { fillCells } from "./spreadsheet_fill";
|
||||
import {
|
||||
@@ -127,7 +128,7 @@ export function attachKeys(ctx: SpreadsheetContext, ed: Editor, hooks: KeyHooks
|
||||
go(move(sheet, sel, arrow[0], arrow[1], { extend: e.shiftKey, jump: ctrl }));
|
||||
return true;
|
||||
}
|
||||
const k = e.key.length === 1 ? e.key.toLowerCase() : e.key;
|
||||
const k = keyLetter(e) || (e.key.length === 1 ? e.key.toLowerCase() : e.key);
|
||||
if (ctrl && !e.altKey) {
|
||||
switch (k) {
|
||||
case "z":
|
||||
@@ -233,7 +234,8 @@ export function attachKeys(ctx: SpreadsheetContext, ed: Editor, hooks: KeyHooks
|
||||
}
|
||||
|
||||
function onKey(e: KeyboardEvent): void {
|
||||
if (e.isComposing || e.keyCode === 229) return; // 한글 조합 중 — 브라우저 몫
|
||||
// 한글 조합 중 — 브라우저 몫 (Ctrl+글자 단축키는 조합 중에도 판정 · 한글 자판 대응 214-2)
|
||||
if ((e.isComposing || e.keyCode === 229) && !((e.ctrlKey || e.metaKey) && keyLetter(e))) return;
|
||||
const inBar = e.target === ed.formula;
|
||||
const done = ed.editing() ? editingKey(e, inBar) : !inBar && navKey(e);
|
||||
if (done) {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
* 되돌리기 범위가 B05 조작 전부인 이유는 `B05_Profile_UI_Profile_History` 머리말 참조.
|
||||
* ========================================================================== */
|
||||
|
||||
import { isCtrlKey } from "../A00_Common/key_shortcut";
|
||||
import type {
|
||||
AlignmentBase,
|
||||
AlignmentEdits,
|
||||
@@ -310,7 +311,7 @@ export function createPanelTools(ctx: PanelToolsContext): PanelTools {
|
||||
freeView.redraw();
|
||||
return;
|
||||
}
|
||||
if ((event.ctrlKey || event.metaKey) && (key === "z" || key === "Z")) {
|
||||
if (isCtrlKey(event, "z")) {
|
||||
event.preventDefault();
|
||||
flushHold();
|
||||
if (event.shiftKey) history.redo();
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
* 쪼개면 되돌릴 때 어긋나기 쉽다. 노선 하나가 노드 수십 개라 사진 몇 벌은 가볍다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { keyLetter } from "../A00_Common/key_shortcut";
|
||||
|
||||
export type Vertex = [number, number];
|
||||
|
||||
/** 되돌릴 수 있는 편집 상태 한 벌. */
|
||||
@@ -154,7 +156,7 @@ export function bindHistoryControls(params: HistoryControlsParams): {
|
||||
|
||||
const onKey = (event: KeyboardEvent): void => {
|
||||
if (!(event.ctrlKey || event.metaKey)) return;
|
||||
const key = event.key.toLowerCase();
|
||||
const key = keyLetter(event);
|
||||
if (key === "z" && !event.shiftKey) step("undo");
|
||||
else if (key === "y" || (key === "z" && event.shiftKey)) step("redo");
|
||||
else return;
|
||||
|
||||
@@ -124,7 +124,8 @@
|
||||
}
|
||||
|
||||
/* 알림 줄(138-3) — 카드 칸 맨 위 고정 · 알림 없어도 줄 유지 · 오른쪽 끝 열 수 칸.
|
||||
위 여백(카드 칸 padding)만큼 끌어올려 그 틈으로 카드가 비치지 않게 */
|
||||
위 여백(카드 칸 padding)만큼 끌어올려 그 틈으로 카드가 비치지 않게.
|
||||
217-1 — 칩 줄 위 여백 16→8 · 칩 줄 ↔ 첫 카드 28→16(카드 칸 gap 24 에서 8 을 당김) */
|
||||
.b06-section__notice-bar {
|
||||
position: sticky;
|
||||
top: calc(-1 * var(--spacing-16));
|
||||
@@ -132,8 +133,8 @@
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-8);
|
||||
margin-top: calc(-1 * var(--spacing-16));
|
||||
padding-block: var(--spacing-16) var(--spacing-4);
|
||||
margin-block: calc(-1 * var(--spacing-16)) calc(-1 * var(--spacing-8));
|
||||
padding-block: var(--spacing-8) 0;
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,8 @@ export interface CadDrawing {
|
||||
stale_layout?: boolean;
|
||||
/** 횡단 장 — 지금과 다른 표 · 글 배율로 확정된 저장본(180-2). */
|
||||
stale_scale?: boolean;
|
||||
/** 횡단 장 — 측점 도면층(212-2) 앞에 확정된 저장본. [수정]을 누르면 측점 층으로 다시 만든다. */
|
||||
stale_layers?: boolean;
|
||||
layers: {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
칸 가운데 · 작성 척도 1/100 그대로(큰 단면이 칸을 넘으면 겹침 — 사용자 배치로 고침).
|
||||
칸 = 인쇄되는 표 격자선 · 블록 테두리 없음 · 칸보다 큰 단면은 `overflow_cells`(169-2).
|
||||
칸 안 = 표를 칸 아래 가운데에 먼저 · 그 위 글 · 그림(도로 중심 = 칸 가운데)(174-1).
|
||||
측점마다 도면층 하나(「횡단 No.3」) = 원지반 · 계획선 · 구조물 · 암 그림 · 종류는 색 · 선 꼴
|
||||
· 표 · 글은 수량표 층(212-2).
|
||||
"""
|
||||
|
||||
import re
|
||||
@@ -52,6 +54,10 @@ CROSS_SHEET_ID = re.compile(r"cross_s(\d{2,}(?:\.\d{2})?)m?")
|
||||
_BORDER_PAD_MM = CROSS_BORDER_PAD_MM
|
||||
# 칸 아래선 ~ 표 아래선 여백(mm) — 같은 행 표가 한 높이(174-1)
|
||||
CELL_TABLE_PAD_MM = 5.0
|
||||
# 측점 도면층 id 머리(212-2) — 뒤에 측점 · DXF 층 이름도 이 id
|
||||
STATION_LAYER_PREFIX = "b08-station-"
|
||||
# 측점 층으로 옮길 그림 종류 층 — 표 · 글(수량표 층)은 그대로
|
||||
_DRAWING_LAYER_IDS = {GROUND_LAYER_ID, DESIGN_LAYER_ID, STRUCTURE_LAYER_ID, ROCK_LAYER_ID}
|
||||
|
||||
|
||||
def _design_points(design_line: list[Any] | None) -> list[tuple[float, float]]:
|
||||
@@ -178,11 +184,20 @@ def _grid_lines(sheet_id: str, cols: int, rows: int) -> list[dict[str, Any]]:
|
||||
]
|
||||
|
||||
|
||||
def _move_to_layer(entity: dict[str, Any], layer_id: str) -> None:
|
||||
"""그림 종류 층 개체(자식 선까지)를 측점 층으로 — id 는 그대로(212-2)."""
|
||||
if entity.get("layerId") in _DRAWING_LAYER_IDS:
|
||||
entity["layerId"] = layer_id
|
||||
for child in entity.get("children") or []:
|
||||
child["layerId"] = layer_id
|
||||
|
||||
|
||||
def build_cross_sheet(sheet: dict[str, Any], sections: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
"""한 장을 만든다. sections는 이 장에 담을 측점 입력들(배치 순서대로)."""
|
||||
entities: list[dict[str, Any]] = []
|
||||
placements: list[dict[str, Any]] = []
|
||||
overflow: list[list[float]] = []
|
||||
station_layers: list[dict[str, Any]] = []
|
||||
slots = sheet.get("slots") or []
|
||||
cols, rows = sheet.get("grid") or (4, 4)
|
||||
usable_w, usable_h = usable_area()
|
||||
@@ -222,8 +237,17 @@ def build_cross_sheet(sheet: dict[str, Any], sections: list[dict[str, Any]]) ->
|
||||
group_id = str(uuid5(_ENTITY_NS, f"{seed_id}:group"))
|
||||
for entity in placed["entities"]:
|
||||
entity["groupId"] = group_id
|
||||
# 측점 도면층(212-2) — 층 관리에서 그 측점 그림만 고르기 · 켜고 끄기 · 잠그기
|
||||
layer_id = f"{STATION_LAYER_PREFIX}{section['chainage']}"
|
||||
title = section.get("title") or f"{section['chainage']}"
|
||||
station_layers.append(_layer(layer_id, f"횡단 {title}"))
|
||||
design_ids = [e["id"] for e in placed["entities"] if e["layerId"] == DESIGN_LAYER_ID]
|
||||
for entity in placed["entities"]:
|
||||
_move_to_layer(entity, layer_id)
|
||||
for placement in placed.get("cross_placements") or []:
|
||||
placement["group_id"] = group_id
|
||||
placement["layer_id"] = layer_id
|
||||
placement["design_ids"] = design_ids # 화면이 다시 그려 갈아 끼울 서버 설계선
|
||||
entities.extend(placed["entities"])
|
||||
placements.extend(placed.get("cross_placements") or [])
|
||||
|
||||
@@ -246,10 +270,7 @@ def build_cross_sheet(sheet: dict[str, Any], sections: list[dict[str, Any]]) ->
|
||||
# 칸보다 큰 단면의 칸 [x0, y0, x1, y1] — 라우터가 CAD 가이드 붉은 틀로 보냄(169-2)
|
||||
"overflow_cells": overflow,
|
||||
"layers": [
|
||||
_layer(GROUND_LAYER_ID, "원지반"),
|
||||
_layer(DESIGN_LAYER_ID, "계획선"),
|
||||
_layer(STRUCTURE_LAYER_ID, "구조물"),
|
||||
_layer(ROCK_LAYER_ID, "암 경계선"),
|
||||
*station_layers,
|
||||
_layer(CROSS_TABLE_LAYER_ID, "수량 산출표"),
|
||||
_layer(FRAME_LAYER_ID, "도각", locked=True),
|
||||
],
|
||||
|
||||
@@ -18,6 +18,7 @@ from B05_Profile.B05_Profile_Engine_Sections import (
|
||||
from B06_Section.B06_Section_Engine_Design import compute_cross_design, curve_widening_args
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
||||
DRAWING_FORMAT,
|
||||
GROUND_LAYER_ID,
|
||||
build_cross_drawing,
|
||||
infer_station_interval,
|
||||
station_no_label,
|
||||
@@ -32,6 +33,7 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad_MassHaul import build_mass_hau
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanKinds import build_plan_kind_drawing
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Sheet import (
|
||||
CROSS_SHEET_ID,
|
||||
STATION_LAYER_PREFIX,
|
||||
build_cross_sheet,
|
||||
plan_cross_sheets,
|
||||
)
|
||||
@@ -462,12 +464,22 @@ def _read_drawing(
|
||||
else:
|
||||
kind = "longitudinal" if _LONG_ID.fullmatch(drawing_id) else "cross"
|
||||
label = str(manifest_entry.get("label") or drawing_id)
|
||||
layers = [layer for layer in saved.get("layers") or [] if isinstance(layer, dict)]
|
||||
if kind == "cross":
|
||||
# 180-1 앞 확정본 = 원지반 층 잠금째 저장 — 열 때 풂(212-1)
|
||||
for layer in layers:
|
||||
if layer.get("id") == GROUND_LAYER_ID:
|
||||
layer["isLocked"] = False
|
||||
if kind == "cross" and manifest_entry.get("layout") != CROSS_SHEET_LAYOUT:
|
||||
saved["stale_layout"] = True # 화면이 [수정] → 새 배치로 다시 만들기를 안내
|
||||
elif kind == "cross" and drawing_id.startswith("cross_s"):
|
||||
scale = read_cross_layout(project_root)["table_scale"]
|
||||
if manifest_entry.get("table_scale", 1.0) != scale:
|
||||
saved["stale_scale"] = True # 옛 배율로 확정한 장 — 그대로 두고 안내(180-2)
|
||||
elif not any(
|
||||
str(layer.get("id")).startswith(STATION_LAYER_PREFIX) for layer in layers
|
||||
):
|
||||
saved["stale_layers"] = True # 측점 층 앞 확정본 — [수정] 때 측점 층(212-2)
|
||||
stored_table = manifest_entry.get("quantity_table")
|
||||
table = stored_table if kind == "cross" and isinstance(stored_table, dict) else None
|
||||
return kind, label, saved, True, table
|
||||
|
||||
@@ -34,6 +34,16 @@ export interface CrossPlacement {
|
||||
frame?: number[];
|
||||
/** 측점 한 벌 묶음(180-1) — 덧그림도 같은 묶음으로 함께 골라짐. */
|
||||
group_id?: string;
|
||||
/** 측점 도면층(212-2) — 덧그림도 그 층에. */
|
||||
layer_id?: string;
|
||||
/** 갈아 끼울 서버 설계선 id(212-2) — 측점 층이면 층으로는 못 가림. */
|
||||
design_ids?: string[];
|
||||
}
|
||||
|
||||
/** 개체(자식 선까지)를 측점 층으로(212-2). */
|
||||
function moveToLayer(entity: Entity, layerId: string): void {
|
||||
entity.layerId = layerId;
|
||||
for (const child of (entity.children as Entity[] | undefined) ?? []) moveToLayer(child, layerId);
|
||||
}
|
||||
|
||||
/** 측점과 도면 배치를 같은 자리로 볼 허용 오차(m). 정본이 누가거리를 0.01m로 끊어 쓴다. */
|
||||
@@ -62,6 +72,7 @@ export async function appendStructureEntities(
|
||||
let added = 0;
|
||||
const fresh: Entity[] = [];
|
||||
const replaced: number[][] = [];
|
||||
const designIds = new Set<string>();
|
||||
for (const placement of placements) {
|
||||
const section = sections.find(
|
||||
(item) => Math.abs(item.chainage_m - placement.chainage_m) <= CHAINAGE_TOLERANCE_M,
|
||||
@@ -81,6 +92,8 @@ export async function appendStructureEntities(
|
||||
}
|
||||
if (!entities.length) continue;
|
||||
if (placement.group_id) for (const entity of entities) entity.groupId = placement.group_id;
|
||||
if (placement.layer_id) for (const entity of entities) moveToLayer(entity, placement.layer_id);
|
||||
for (const id of placement.design_ids ?? []) designIds.add(id);
|
||||
// 이 블록 설계선은 우리가 다시 그렸다 — 서버가 낸 트림 전 설계선은 걷어낸다.
|
||||
replaced.push(frame);
|
||||
fresh.push(...entities);
|
||||
@@ -89,7 +102,7 @@ export async function appendStructureEntities(
|
||||
if (replaced.length) {
|
||||
// 걷어내기는 새 엔티티를 넣기 **전에** 한다 — 뒤에 하면 우리 것까지 같이 지운다.
|
||||
drawing.entities = drawing.entities.filter((entity) => {
|
||||
if (entity.layerId !== DESIGN_LAYER_ID) return true;
|
||||
if (entity.layerId !== DESIGN_LAYER_ID && !designIds.has(entity.id as string)) return true;
|
||||
const box = entityBox(entity);
|
||||
if (!box) return true;
|
||||
return !replaced.some(
|
||||
|
||||
@@ -365,6 +365,11 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
"옛 표 배율로 확정된 장입니다. [수정]을 누르면 지금 배율로 다시 만듭니다.",
|
||||
"info",
|
||||
);
|
||||
} else if (response.drawing.stale_layers) {
|
||||
showToast(
|
||||
"측점 도면층 앞에 확정된 장입니다. [수정]을 누르면 측점마다 층(횡단 No.)으로 다시 만듭니다.",
|
||||
"info",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
cad.fail(error instanceof Error ? error.message : "CAD 도면을 불러오지 못했습니다.");
|
||||
|
||||
@@ -185,8 +185,9 @@ export interface CrossLayoutExtras {
|
||||
}
|
||||
|
||||
/**
|
||||
* 횡단 장 배치 칸(140-3 · 153-1) — 「배치 열 [4] 행 [4] [이 장] [모든 장]」 + 「표 배율 [1] [적용]」.
|
||||
* `onApply` 가 없으면(장이 아닌 도면) [이 장]을 끔.
|
||||
* 횡단 장 배치 칸(140-3 · 153-1) — 줄마다 한 일(212-3):
|
||||
* 「배치 열 [4] 행 [4]」 · 「[이 장] [모든 장]」 · 「표 배율 [1] [배율 적용]」 · (부르는 쪽이 붙이는 [안내선 숨기기]).
|
||||
* 단추 · 숫자 칸 = 줄 위 작은 단추 한 규격(`--btn-bar-*`) · `onApply` 가 없으면(장이 아닌 도면) [이 장]을 끔.
|
||||
*/
|
||||
export function buildCrossLayoutControls(
|
||||
grid: [number, number],
|
||||
@@ -196,6 +197,12 @@ export function buildCrossLayoutControls(
|
||||
): HTMLElement {
|
||||
const box = document.createElement("div");
|
||||
box.className = "b07-cross-layout";
|
||||
const row = (...items: HTMLElement[]): HTMLElement => {
|
||||
const line = document.createElement("div");
|
||||
line.className = "b07-cross-layout__row";
|
||||
line.append(...items);
|
||||
return line;
|
||||
};
|
||||
const number = (value: number, label: string) =>
|
||||
createInputField({
|
||||
label,
|
||||
@@ -227,10 +234,11 @@ export function buildCrossLayoutControls(
|
||||
return button;
|
||||
};
|
||||
box.append(
|
||||
cols.root,
|
||||
rows.root,
|
||||
action("이 장", "이 장만 이 배치로", onApply),
|
||||
action("모든 장", "모든 장을 이 배치로(장마다 값 지움)", onAllDefault),
|
||||
row(cols.root, rows.root),
|
||||
row(
|
||||
action("이 장", "이 장만 이 배치로", onApply),
|
||||
action("모든 장", "모든 장을 이 배치로(장마다 값 지움)", onAllDefault),
|
||||
),
|
||||
);
|
||||
if (extras.scale) {
|
||||
const { onApply: applyScale } = extras.scale;
|
||||
@@ -255,7 +263,7 @@ export function buildCrossLayoutControls(
|
||||
},
|
||||
});
|
||||
button.title = "모든 장의 표 · 표 위 글 크기(기본 1)";
|
||||
box.append(scale.root, button);
|
||||
box.append(row(scale.root, button));
|
||||
}
|
||||
return box;
|
||||
}
|
||||
|
||||
@@ -225,8 +225,9 @@
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* 횡단 장 배치(153-1) — 횡단도 묶음 맨 아래 전체 폭: 배치 열 [4] 행 [4] [이 장] [모든 장].
|
||||
좌측이 182px 까지 줄어드니 줄바꿈 허용 · 공용 칸의 오류 자리는 접음. */
|
||||
/* 횡단 장 배치(153-1 · 212-3) — 횡단도 묶음 맨 아래 전체 폭 · 한 줄에 한 일:
|
||||
배치 열 · 행 / [이 장] [모든 장] / 표 배율 [배율 적용] / [안내선 숨기기].
|
||||
단추 · 숫자 칸 = 줄 위 작은 단추 한 규격(`--btn-bar-*` · 높이 16) · 한 줄 안 같은 너비 · 오류 자리는 접음. */
|
||||
.b07-cross-layout-slot {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
@@ -237,25 +238,45 @@
|
||||
|
||||
.b07-cross-layout {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4) var(--spacing-8);
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.b07-cross-layout > .ui-field {
|
||||
.b07-cross-layout__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b07-cross-layout__row > * {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.b07-cross-layout .ui-field {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.b07-cross-layout .ui-btn,
|
||||
.b07-cross-layout .ui-field .ui-input {
|
||||
height: auto;
|
||||
padding: var(--btn-bar-padding);
|
||||
font-size: var(--btn-bar-font-size);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.b07-cross-layout .ui-field__label {
|
||||
margin: 0;
|
||||
font-size: var(--btn-bar-font-size);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.b07-cross-layout .ui-input {
|
||||
width: 48px;
|
||||
.b07-cross-layout .ui-field .ui-input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.b07-cross-layout .ui-field__error {
|
||||
|
||||
@@ -1,24 +1,10 @@
|
||||
/** Ctrl 단축키 이름 */
|
||||
export type CtrlShortcut = 'undo' | 'redo' | 'save' | 'selectAll';
|
||||
|
||||
/**
|
||||
* Ctrl 단축키 판정 — 글자(`key`)가 아니라 자판 자리(`code`).
|
||||
* 한글 자판은 `key` 가 「ㅋ」·「ㅛ」로 오고, Shift 를 누르면 `key` 가 대문자(「Z」)라 글자로는 안 맞는다.
|
||||
* Ctrl 단축키 판정 — 공용 `A00_Common/key_shortcut.ts`(214-2)를 그대로 내보냄.
|
||||
* 한글 자판은 `key` 가 「ㅋ」·「ㅛ」로 와 글자로는 안 맞아 자판 자리(`code`)로 본다.
|
||||
*/
|
||||
export function ctrlShortcut(
|
||||
evt: Pick<KeyboardEvent, 'ctrlKey' | 'altKey' | 'shiftKey' | 'code'>
|
||||
): CtrlShortcut | null {
|
||||
if (!evt.ctrlKey || evt.altKey) return null;
|
||||
switch (evt.code) {
|
||||
case 'KeyZ':
|
||||
return evt.shiftKey ? 'redo' : 'undo';
|
||||
case 'KeyY':
|
||||
return evt.shiftKey ? null : 'redo';
|
||||
case 'KeyS':
|
||||
return evt.shiftKey ? null : 'save';
|
||||
case 'KeyA':
|
||||
return evt.shiftKey ? null : 'selectAll';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
export {
|
||||
ctrlShortcut,
|
||||
isCtrlKey,
|
||||
keyLetter,
|
||||
type CtrlShortcut,
|
||||
} from '../../../../A00_Common/key_shortcut';
|
||||
|
||||
@@ -25,6 +25,6 @@
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"include": ["src", "../../A00_Common/key_shortcut.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
* 고치는 핵심은 `M02_DF_Paper.ts`(판 DOM 을 옮겨 끼움).
|
||||
* ========================================================================== */
|
||||
|
||||
import { isCtrlKey } from "../../A00_Common/key_shortcut";
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
@@ -209,7 +210,7 @@ export function mountFlowPanel(host: HTMLElement, opts: FlowPanelOpts): FlowPane
|
||||
|
||||
const root = el("aside", { className: "m02-dfp", children: [grip, open, strip] });
|
||||
root.addEventListener("keydown", (ev) => {
|
||||
if ((ev.ctrlKey || ev.metaKey) && ev.key.toLowerCase() === "z" && !ev.shiftKey) {
|
||||
if (isCtrlKey(ev, "z") && !ev.shiftKey) {
|
||||
if ((ev.target as HTMLElement).closest("input, textarea, [contenteditable]")) return;
|
||||
ev.preventDefault();
|
||||
paper.undo();
|
||||
@@ -258,7 +259,7 @@ export function mountFlowPanel(host: HTMLElement, opts: FlowPanelOpts): FlowPane
|
||||
});
|
||||
full.addEventListener("keydown", (ev) => {
|
||||
if (ev.key === "Escape" && !ev.defaultPrevented) shrink();
|
||||
if ((ev.ctrlKey || ev.metaKey) && ev.key.toLowerCase() === "z" && !ev.shiftKey) {
|
||||
if (isCtrlKey(ev, "z") && !ev.shiftKey) {
|
||||
if ((ev.target as HTMLElement).closest("input, textarea, [contenteditable]")) return;
|
||||
ev.preventDefault();
|
||||
paper.undo();
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
* 접기는 부모의 attachCollapsible(제목 줄 클릭) — 오른쪽 단추를 눌러도 접히지 않음.
|
||||
* ========================================================================== */
|
||||
|
||||
import { isCtrlKey } from "../A00_Common/key_shortcut";
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import { loadingError, showLoading } from "@ui/ui_template_loading";
|
||||
import { StaleError } from "./M02_MasterTemplete_Api_Fetch";
|
||||
@@ -76,7 +77,7 @@ export function cardShell(opt: CardShellOptions): CardShell {
|
||||
const host = opt.stateHost ?? opt.body;
|
||||
let save: (() => void) | null = null;
|
||||
root.addEventListener("keydown", (event) => {
|
||||
if (!save || !(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== "s") return;
|
||||
if (!save || !isCtrlKey(event, "s")) return;
|
||||
event.preventDefault();
|
||||
save();
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import "./M02_MasterTemplete_TreePanel.css";
|
||||
import { isCtrlKey } from "../A00_Common/key_shortcut";
|
||||
import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements";
|
||||
import { loadingError, showLoading } from "@ui/ui_template_loading";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
@@ -598,7 +599,7 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle {
|
||||
|
||||
body.addEventListener("keydown", (event) => {
|
||||
const row = (event.target as HTMLElement).closest<HTMLElement>("[data-node]");
|
||||
if (!row && (event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "v") {
|
||||
if (!row && isCtrlKey(event, "v")) {
|
||||
event.preventDefault(); // 빈 자리에 초점 = 맨 위에 붙여넣기
|
||||
return void paste([]);
|
||||
}
|
||||
@@ -626,10 +627,10 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle {
|
||||
const many = picked.size > 1 && picked.has(row.dataset.node ?? "");
|
||||
if (many && api.many) void removeMany(pickedTargets());
|
||||
else if (!(target.kind === "folder" && !target.path.length)) void remove(target);
|
||||
} else if (target && (event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "c") {
|
||||
} else if (target && isCtrlKey(event, "c")) {
|
||||
event.preventDefault();
|
||||
copy(picked.has(row.dataset.node ?? "") ? pickedTargets() : [target]);
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "v") {
|
||||
} else if (isCtrlKey(event, "v")) {
|
||||
event.preventDefault(); // 붙일 자리 = 초점 줄이 폴더면 그 안 · 아니면 맨 위
|
||||
void paste(target?.kind === "folder" ? target.path : []);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Ctrl 단축키 — 한글 자판에서도 되돌리기 · 다시 하기가 먹는다(PLAN 214-1).
|
||||
* 한글 자판은 key 가 「ㅋ」·「ㅛ」 · Shift 는 대문자 — 자판 자리(code)로 판정한다.
|
||||
* Ctrl 단축키 — 한글 자판에서도 먹는다(PLAN 214-1 CAD · 214-2 페이지 공용 `A00_Common/key_shortcut.ts`).
|
||||
* 한글 자판은 key 가 「ㅋ」·「ㅛ」 · Shift 는 대문자 — 영문이 아니면 자판 자리(code)로 판정한다.
|
||||
* 실행: cd B07_DesignDetail/openwebcad && npx vitest run --dir ../../resources/tester/cad ctrl-shortcut-hangul
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
@@ -111,3 +111,39 @@ describe("키 입력 — 한글 자판", () => {
|
||||
expect(prevented).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ---- 공용 판정 · 페이지(214-2) — 같은 함수를 B05 · M02 · 시트 키가 씀 ----
|
||||
import { isCtrlKey, keyLetter } from "../../../A00_Common/key_shortcut";
|
||||
|
||||
describe("keyLetter · isCtrlKey — 페이지 공용", () => {
|
||||
it("한글 자판 key 「ㅋ」 + KeyZ = z · 영문 · 대문자도 소문자 · 숫자", () => {
|
||||
expect(keyLetter({ key: "ㅋ", code: "KeyZ" })).toBe("z");
|
||||
expect(keyLetter({ key: "ㅊ", code: "KeyC" })).toBe("c");
|
||||
expect(keyLetter({ key: "ㅍ", code: "KeyV" })).toBe("v");
|
||||
expect(keyLetter({ key: "ㄴ", code: "KeyS" })).toBe("s");
|
||||
expect(keyLetter({ key: "Z", code: "KeyZ" })).toBe("z");
|
||||
expect(keyLetter({ key: "1", code: "Digit1" })).toBe("1");
|
||||
expect(keyLetter({ key: "Process", code: "Digit1" })).toBe("1");
|
||||
});
|
||||
it("다른 자판 배열(key 가 영문)은 key 를 따름 · 글자 아닌 키는 빈 글", () => {
|
||||
expect(keyLetter({ key: "z", code: "KeyY" })).toBe("z"); // 독일식 자리 바뀜
|
||||
expect(keyLetter({ key: "Enter", code: "Enter" })).toBe("");
|
||||
expect(keyLetter({ key: "Escape", code: "Escape" })).toBe("");
|
||||
});
|
||||
it("code 없는 합성 사건은 key 로", () => {
|
||||
expect(keyLetter({ key: "Z", code: "" })).toBe("z");
|
||||
});
|
||||
it("isCtrlKey — Ctrl · ⌘ 만 · 글자 맞아야", () => {
|
||||
const e = (extra: object) => ({
|
||||
key: "ㅋ",
|
||||
code: "KeyZ",
|
||||
ctrlKey: false,
|
||||
metaKey: false,
|
||||
...extra,
|
||||
});
|
||||
expect(isCtrlKey(e({ ctrlKey: true }), "z")).toBe(true);
|
||||
expect(isCtrlKey(e({ metaKey: true }), "z")).toBe(true);
|
||||
expect(isCtrlKey(e({}), "z")).toBe(false);
|
||||
expect(isCtrlKey(e({ ctrlKey: true }), "y")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 시트 키 — 한글 자판에서도 Ctrl 단축키(PLAN 214-2).
|
||||
* 실행: cd B07_DesignDetail/openwebcad && npx vitest run --config ../../resources/tester/cad/vitest.ui.config.ts
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { attachKeys } from "../../../A00_Common/spreadsheet/spreadsheet_keys";
|
||||
|
||||
describe("시트 키 — 한글 자판 Ctrl 단축키", () => {
|
||||
const setup = () => {
|
||||
const log: string[] = [];
|
||||
const handlers: ((e: KeyboardEvent) => void)[] = [];
|
||||
const ed = {
|
||||
ta: {
|
||||
addEventListener: (_: string, fn: (e: KeyboardEvent) => void) => handlers.push(fn),
|
||||
removeEventListener: () => {},
|
||||
},
|
||||
formula: { addEventListener: () => {}, removeEventListener: () => {} },
|
||||
editing: () => false,
|
||||
};
|
||||
const ctx = {
|
||||
undo: () => log.push("undo"),
|
||||
redo: () => log.push("redo"),
|
||||
readOnly: false,
|
||||
sheet: () => ({}),
|
||||
selection: {},
|
||||
};
|
||||
attachKeys(ctx as never, ed as never, {
|
||||
find: (replace: boolean) => log.push(replace ? "replace" : "find"),
|
||||
});
|
||||
const press = (extra: object) =>
|
||||
handlers[0]({
|
||||
key: "",
|
||||
code: "",
|
||||
keyCode: 0,
|
||||
isComposing: false,
|
||||
ctrlKey: true,
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
altKey: false,
|
||||
target: null,
|
||||
preventDefault: () => {},
|
||||
stopPropagation: () => {},
|
||||
...extra,
|
||||
} as unknown as KeyboardEvent);
|
||||
return { log, press };
|
||||
};
|
||||
it("「ㅋ」 = 되돌리기 · 「ㅛ」 = 다시 하기 · Shift+「ㅋ」 = 다시 하기", () => {
|
||||
const { log, press } = setup();
|
||||
press({ key: "ㅋ", code: "KeyZ" });
|
||||
press({ key: "ㅛ", code: "KeyY" });
|
||||
press({ key: "ㅋ", code: "KeyZ", shiftKey: true });
|
||||
expect(log).toEqual(["undo", "redo", "redo"]);
|
||||
});
|
||||
it("「ㄹ」(KeyF) = 찾기 · 「ㅗ」(KeyH) = 바꾸기 · 조합 중에도", () => {
|
||||
const { log, press } = setup();
|
||||
press({ key: "ㄹ", code: "KeyF" });
|
||||
press({ key: "Process", code: "KeyH", keyCode: 229, isComposing: true });
|
||||
expect(log).toEqual(["find", "replace"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
/** 페이지 코드(`@ui` 별칭을 쓰는 것)를 시험하는 vitest 설정 — 시험은 `ctrl-shortcut-sheet.test.ts`. */
|
||||
const root = new URL("../../..", import.meta.url).pathname.replace(/^\/(\w:)/, "$1");
|
||||
|
||||
export default {
|
||||
root,
|
||||
resolve: { alias: { "@ui": `${root}ui_template` } },
|
||||
test: { include: ["resources/tester/cad/ctrl-shortcut-sheet.test.ts"] },
|
||||
};
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import GROUND_LAYER_ID
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import GROUND_COLOR
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Sheet import (
|
||||
CELL_TABLE_PAD_MM,
|
||||
build_cross_sheet,
|
||||
@@ -95,7 +95,9 @@ def test_같은_행_표_한_높이_그림_중심_칸_가운데() -> None:
|
||||
assert tables[0][0] == pytest.approx(tables[1][0])
|
||||
# 도로 중심(offset 0) = 칸 가운데 · 그림(원지반) 아래 끝 = 글(없으면 표) 위 8 mm
|
||||
grounds = [
|
||||
entities_bbox([e]) for e in drawing["entities"] if e.get("layerId") == GROUND_LAYER_ID
|
||||
entities_bbox([e])
|
||||
for e in drawing["entities"]
|
||||
if e.get("type") == "PolyLine" and e.get("lineColor") == GROUND_COLOR
|
||||
]
|
||||
for placement, slot, table, design, ground in zip(
|
||||
drawing["cross_placements"], sheet["slots"], tables, designs, grounds, strict=True
|
||||
|
||||
@@ -14,7 +14,7 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import DRAWING_FORMAT, GROUND_LAYER_ID
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import DRAWING_FORMAT, GROUND_COLOR
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Sheet import (
|
||||
CELL_TABLE_PAD_MM,
|
||||
build_cross_sheet,
|
||||
@@ -81,7 +81,9 @@ def test_배율_15_표_크기와_쌓기() -> None:
|
||||
bottom = shape["origin"]["y"] - sum(shape["rowHeights"])
|
||||
assert bottom == pytest.approx(slot[1] - h / 4 + CELL_TABLE_PAD_MM) # 표 아래선 그대로
|
||||
grounds = [
|
||||
entities_bbox([e]) for e in drawing["entities"] if e.get("layerId") == GROUND_LAYER_ID
|
||||
entities_bbox([e])
|
||||
for e in drawing["entities"]
|
||||
if e.get("type") == "PolyLine" and e.get("lineColor") == GROUND_COLOR
|
||||
]
|
||||
# 글 2 줄 × 3.0 × 1.5 = 9 mm · 간격 8 mm 는 배율 안 탐
|
||||
tops = [t["shapeData"]["origin"]["y"] for t in tables]
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
||||
FRAME_LAYER_ID,
|
||||
GROUND_COLOR,
|
||||
GROUND_LAYER_ID,
|
||||
build_cross_drawing,
|
||||
)
|
||||
@@ -57,7 +58,9 @@ def _ground_locked(drawing: dict) -> bool:
|
||||
|
||||
|
||||
def test_원지반_층_안_잠김() -> None:
|
||||
assert _ground_locked(_drawing()) is False
|
||||
# 장 = 원지반이 측점 층에(212-2) · 도각 말고 잠긴 층 없음
|
||||
layers = _drawing()["layers"]
|
||||
assert not any(layer["isLocked"] for layer in layers if layer["id"] != FRAME_LAYER_ID)
|
||||
single = build_cross_drawing(_source(0.0), "cross_00000m", _design_line(), None, None, "")
|
||||
assert _ground_locked(single) is False
|
||||
|
||||
@@ -72,9 +75,9 @@ def test_측점마다_groupId_하나() -> None:
|
||||
assert {e["groupId"] for e in grouped} == set(group_ids)
|
||||
for group_id in group_ids:
|
||||
members = [e for e in grouped if e["groupId"] == group_id]
|
||||
layers = {e["layerId"] for e in members}
|
||||
types = {e["type"] for e in members}
|
||||
assert GROUND_LAYER_ID in layers and "Table" in types and "Text" in types
|
||||
ground = any(e["type"] == "PolyLine" and e["lineColor"] == GROUND_COLOR for e in members)
|
||||
assert ground and "Table" in types and "Text" in types
|
||||
# 격자선 · 도각은 묶지 않음
|
||||
assert all("groupId" not in e for e in drawing["entities"] if e["layerId"] == FRAME_LAYER_ID)
|
||||
assert all(e["layerId"] != FRAME_LAYER_ID for e in grouped)
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""212-3 B07 좌측 「횡단도」 배치 칸 — 단추 · 숫자 칸 한 규격 · 한 줄에 한 일 · 남는 검은 선 판정.
|
||||
|
||||
못박는 것
|
||||
- 줄 = 「배치 열 · 행」 · 「[이 장] [모든 장]」 · 「표 배율 [배율 적용]」
|
||||
· [안내선 숨기기](부르는 쪽).
|
||||
- 단추 · 숫자 칸 · 이름 = 줄 위 작은 단추 토큰(187 `--btn-bar-*` · 높이 16) · 한 줄 안 같은 너비.
|
||||
- 옛 한 줄 감기(flex-wrap) · 숫자 칸 48px 고정 없음.
|
||||
- [안내선 숨기기] 뒤 남는 검은 선 = 인쇄 칸선(도각 층 도면 개체 · 회청 `#8ea0b5`
|
||||
· 밝은 바탕에서 짙게 그려짐) — 안내선(겹판)과 다른 것이라 지우지 않음 · 단추는 겹판만 끔.
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import FRAME_LAYER_ID, TABLE_LINE_COLOR
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Sheet import _grid_lines
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Template import use_project_templates
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
CSS = (ROOT / "B07_DesignDetail/B07_DesignDetail_UI_Style.css").read_text(encoding="utf-8")
|
||||
PANELS = (ROOT / "B07_DesignDetail/B07_DesignDetail_UI_Panels.ts").read_text(encoding="utf-8")
|
||||
PAGE = (ROOT / "B07_DesignDetail/B07_DesignDetail_UI_Page.ts").read_text(encoding="utf-8")
|
||||
GUIDE = (ROOT / "B07_DesignDetail/openwebcad/src/integration/guide-area.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
THEME = (ROOT / "ui_template/ui_template_theme.css").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _block(selector: str) -> str:
|
||||
m = re.search(r"(?:^|\n)" + re.escape(selector) + r"\s*\{([^}]*)\}", CSS)
|
||||
assert m, selector
|
||||
return m.group(1)
|
||||
|
||||
|
||||
def _decl(block: str, prop: str) -> str:
|
||||
m = re.search(r"(?:^|;|\s)" + re.escape(prop) + r"\s*:\s*([^;]+);", block)
|
||||
assert m, prop
|
||||
return m.group(1).strip()
|
||||
|
||||
|
||||
def test_단추_숫자_칸_한_규격_토큰():
|
||||
shared = _block(".b07-cross-layout .ui-btn,\n.b07-cross-layout .ui-field .ui-input")
|
||||
assert _decl(shared, "padding") == "var(--btn-bar-padding)"
|
||||
assert _decl(shared, "font-size") == "var(--btn-bar-font-size)"
|
||||
assert _decl(shared, "line-height") == "1"
|
||||
assert _decl(shared, "height") == "auto" # 공용 작은 칸 30px 를 풀어 단추 높이에 맞춤
|
||||
label = _block(".b07-cross-layout .ui-field__label")
|
||||
assert _decl(label, "font-size") == "var(--btn-bar-font-size)"
|
||||
# 토큰은 theme 한 곳(187)
|
||||
assert "--btn-bar-padding: 1px var(--spacing-8);" in THEME
|
||||
assert "--btn-bar-font-size: var(--text-caption);" in THEME
|
||||
|
||||
|
||||
def test_한_줄에_한_일_같은_너비():
|
||||
box = _block(".b07-cross-layout")
|
||||
assert _decl(box, "flex-direction") == "column"
|
||||
assert "flex-wrap" not in box
|
||||
assert _decl(_block(".b07-cross-layout__row > *"), "flex") == "1 1 0"
|
||||
assert "width: 48px" not in CSS
|
||||
assert 'line.className = "b07-cross-layout__row"' in PANELS
|
||||
assert "row(cols.root, rows.root)" in PANELS
|
||||
assert re.search(r"row\(\s*action\(\"이 장\"[^;]*action\(\"모든 장\"", PANELS, re.S)
|
||||
assert "box.append(row(scale.root, button));" in PANELS
|
||||
# 안내선 단추 = 배치 칸 맨 아래 한 줄(전체 폭)
|
||||
assert "controls.append(guideToggle);" in PAGE
|
||||
|
||||
|
||||
def test_남는_검은_선은_인쇄_칸선_지우지_않음():
|
||||
use_project_templates(None)
|
||||
lines = _grid_lines("cross_s00000m", 4, 4)
|
||||
assert len(lines) == 10 # 세로 5 · 가로 5 그대로
|
||||
assert all(e["layerId"] == FRAME_LAYER_ID for e in lines)
|
||||
assert all(e["lineColor"] == TABLE_LINE_COLOR == "#8ea0b5" for e in lines)
|
||||
# 안내선 단추는 겹판(점선 영역 + 붉은 틀)만 — 도면 개체를 안 건드림
|
||||
draw = GUIDE[GUIDE.index("export function drawGuide") :]
|
||||
draw = draw[: draw.index("\n}\n")]
|
||||
assert "if (!guide || !guideVisible) return;" in draw
|
||||
assert "getEntities" not in draw and "setEntities" not in draw
|
||||
@@ -0,0 +1,185 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""212-1 · 212-2 B07 횡단 장 — 원지반 고르기 · 측점별 도면층.
|
||||
|
||||
못박는 것
|
||||
- 212-1 옛 확정 장(원지반 층 잠금째 저장)도 열 때 원지반 잠금 풂 · 측점 층 없으면 stale_layers.
|
||||
- 212-2 장의 측점마다 층 하나(`b08-station-{측점}` · 이름 「횡단 {제목}」) = 원지반 · 계획선 ·
|
||||
암 그림(자식 선까지) · 표 · 글은 수량표 층 · 격자선 · 도각은 도각 층 · 종류 층 없음.
|
||||
- 개체 id 는 그대로(층만 바뀜) · placement 에 layer_id · design_ids(화면 덧그림 갈아 끼우기).
|
||||
- DXF 층 이름 = 측점 층 id 그대로(금지 문자 없음).
|
||||
- 화면 덧그림도 측점 층 · design_ids 로 서버 설계선 걷음(소스 확인 · typecheck 는 따로).
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from uuid import uuid5
|
||||
|
||||
import ezdxf
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
||||
_POLY_NS,
|
||||
CROSS_TABLE_LAYER_ID,
|
||||
DESIGN_COLOR,
|
||||
DESIGN_LAYER_ID,
|
||||
DRAWING_FORMAT,
|
||||
FRAME_LAYER_ID,
|
||||
GROUND_COLOR,
|
||||
GROUND_LAYER_ID,
|
||||
ROCK_LAYER_ID,
|
||||
STRUCTURE_LAYER_ID,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Sheet import (
|
||||
STATION_LAYER_PREFIX,
|
||||
build_cross_sheet,
|
||||
plan_cross_sheets,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Frame_Export import _layer_name, drawing_to_dxf
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Template import use_project_templates
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support import CROSS_SHEET_LAYOUT, _read_drawing
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
KIND_LAYERS = {GROUND_LAYER_ID, DESIGN_LAYER_ID, STRUCTURE_LAYER_ID, ROCK_LAYER_ID}
|
||||
|
||||
|
||||
def _source(chainage: float) -> dict:
|
||||
return {
|
||||
"chainage_m": chainage,
|
||||
"samples": [
|
||||
{"valid": True, "offset_m": float(x), "elevation_m": 100.0 + 0.1 * x}
|
||||
for x in range(-5, 6)
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _sheet() -> tuple[dict, dict]:
|
||||
use_project_templates(None)
|
||||
chainages = [0.0, 20.0, 40.0, 60.0]
|
||||
sheet = plan_cross_sheets(chainages, {"default": [2, 2]})[0]
|
||||
sections = [
|
||||
{
|
||||
"chainage": c,
|
||||
"source": _source(c),
|
||||
# 암 지반 = 암 경계선도 측점 층으로
|
||||
"design": {"rock_boundary_offset_m": -1.0} if c == 20.0 else None,
|
||||
"design_line": [{"offset_m": x, "elevation_m": 100.0} for x in (-3.0, 0.0, 3.0)],
|
||||
"quantity_table": {"ground": 1.0, "planned": 0.5},
|
||||
"title": f"No.{int(c // 20)}",
|
||||
}
|
||||
for c in chainages
|
||||
]
|
||||
return sheet, build_cross_sheet(sheet, sections)
|
||||
|
||||
|
||||
def _flat(entities: list[dict]) -> list[dict]:
|
||||
out = []
|
||||
for entity in entities:
|
||||
out.append(entity)
|
||||
out.extend(_flat(entity.get("children") or []))
|
||||
return out
|
||||
|
||||
|
||||
def test_측점마다_층_하나_이름_횡단_No():
|
||||
_sheet_, drawing = _sheet()
|
||||
layers = drawing["layers"]
|
||||
ids = [layer["id"] for layer in layers]
|
||||
assert ids[:4] == [f"{STATION_LAYER_PREFIX}{c}" for c in (0.0, 20.0, 40.0, 60.0)]
|
||||
assert [layer["name"] for layer in layers[:4]] == [
|
||||
"횡단 No.0",
|
||||
"횡단 No.1",
|
||||
"횡단 No.2",
|
||||
"횡단 No.3",
|
||||
]
|
||||
assert ids[4:] == [CROSS_TABLE_LAYER_ID, FRAME_LAYER_ID]
|
||||
assert not KIND_LAYERS & set(ids) # 종류 층 없음 — 빈 층이 안 남음
|
||||
assert all(layer["isVisible"] and not layer["isLocked"] for layer in layers[:4])
|
||||
|
||||
|
||||
def test_그림은_측점_층_표_글은_수량표_층_격자는_도각():
|
||||
_sheet_, drawing = _sheet()
|
||||
flat = _flat(drawing["entities"])
|
||||
assert not any(e["layerId"] in KIND_LAYERS for e in flat) # 자식 선까지
|
||||
by_layer: dict[str, list[dict]] = {}
|
||||
for entity in drawing["entities"]:
|
||||
by_layer.setdefault(entity["layerId"], []).append(entity)
|
||||
for chainage in (0.0, 20.0, 40.0, 60.0):
|
||||
members = by_layer[f"{STATION_LAYER_PREFIX}{chainage}"]
|
||||
colors = {e["lineColor"] for e in members}
|
||||
assert GROUND_COLOR in colors and DESIGN_COLOR in colors
|
||||
assert all(e["type"] == "PolyLine" for e in members)
|
||||
assert len({e["groupId"] for e in members}) == 1 # 180-1 groupId 그대로
|
||||
# 암 지반 측점만 점선 암 경계선(종류 = 선 꼴)
|
||||
rock = [e for e in by_layer[f"{STATION_LAYER_PREFIX}20.0"] if e.get("lineDash")]
|
||||
assert len(rock) == 1
|
||||
assert not any(e.get("lineDash") for e in by_layer[f"{STATION_LAYER_PREFIX}0.0"])
|
||||
assert {e["type"] for e in by_layer[CROSS_TABLE_LAYER_ID]} <= {"Table", "Text"}
|
||||
assert all("groupId" not in e for e in by_layer[FRAME_LAYER_ID])
|
||||
|
||||
|
||||
def test_id_그대로_placement_layer_id_design_ids():
|
||||
sheet, drawing = _sheet()
|
||||
by_id = {e["id"]: e for e in drawing["entities"]}
|
||||
for placement in drawing["cross_placements"]:
|
||||
chainage = placement["chainage_m"]
|
||||
layer_id = f"{STATION_LAYER_PREFIX}{chainage}"
|
||||
assert placement["layer_id"] == layer_id
|
||||
seed = f"{sheet['id']}:{chainage}"
|
||||
# 개체 id 는 종류 층 씨앗 그대로 — 저장본 · 덧그림 짝이 안 흔들림
|
||||
ground = by_id[str(uuid5(_POLY_NS, f"{seed}:{GROUND_LAYER_ID}"))]
|
||||
assert ground["layerId"] == layer_id and ground["lineColor"] == GROUND_COLOR
|
||||
design_ids = placement["design_ids"]
|
||||
assert design_ids == [str(uuid5(_POLY_NS, f"{seed}:{DESIGN_LAYER_ID}"))]
|
||||
assert all(by_id[i]["lineColor"] == DESIGN_COLOR for i in design_ids)
|
||||
|
||||
|
||||
def test_DXF_층_이름_측점_층_id(tmp_path: Path):
|
||||
_sheet_, drawing = _sheet()
|
||||
for layer in drawing["layers"]:
|
||||
assert _layer_name(layer["id"]) == layer["id"]
|
||||
data, _skipped = drawing_to_dxf(drawing)
|
||||
(tmp_path / "sheet.dxf").write_bytes(data)
|
||||
names = {layer.dxf.name for layer in ezdxf.readfile(tmp_path / "sheet.dxf").layers}
|
||||
assert {f"{STATION_LAYER_PREFIX}{c}" for c in (0.0, 20.0, 40.0, 60.0)} <= names
|
||||
assert not KIND_LAYERS & names
|
||||
|
||||
|
||||
def test_옛_확정_장_원지반_잠금_풂_측점_층_안내(tmp_path: Path):
|
||||
stage = tmp_path / "B07_DesignDetail"
|
||||
(stage / "drawings").mkdir(parents=True)
|
||||
layers = [
|
||||
{"id": GROUND_LAYER_ID, "name": "원지반", "isVisible": True, "isLocked": True},
|
||||
{"id": FRAME_LAYER_ID, "name": "도각", "isVisible": True, "isLocked": True},
|
||||
]
|
||||
saved = {"format": DRAWING_FORMAT, "entities": [], "layers": layers}
|
||||
path = stage / "drawings" / "cross_s00000m.json"
|
||||
path.write_text(json.dumps(saved), encoding="utf-8")
|
||||
entry = {"kind": "cross", "label": "1장", "confirmed": True, "layout": CROSS_SHEET_LAYOUT}
|
||||
(stage / "manifest.json").write_text(
|
||||
json.dumps({"drawings": {"cross_s00000m": entry}}), encoding="utf-8"
|
||||
)
|
||||
|
||||
def read() -> dict:
|
||||
_k, _l, drawing, confirmed, _t = _read_drawing(
|
||||
tmp_path, tmp_path / "x.json", "cross_s00000m"
|
||||
)
|
||||
assert confirmed
|
||||
return drawing
|
||||
|
||||
drawing = read()
|
||||
locked = {layer["id"]: layer["isLocked"] for layer in drawing["layers"]}
|
||||
assert locked == {GROUND_LAYER_ID: False, FRAME_LAYER_ID: True} # 도각은 그대로 잠금
|
||||
assert drawing.get("stale_layers") is True
|
||||
# 측점 층으로 확정한 장은 안내 없음
|
||||
saved["layers"] = [{"id": f"{STATION_LAYER_PREFIX}0", "name": "횡단 No.0"}, layers[1]]
|
||||
path.write_text(json.dumps(saved), encoding="utf-8")
|
||||
assert "stale_layers" not in read()
|
||||
|
||||
|
||||
def test_화면_덧그림_측점_층_design_ids():
|
||||
struct = (ROOT / "B07_DesignDetail/B07_DesignDetail_UI_Cad_Structures.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert "layer_id?: string;" in struct and "design_ids?: string[];" in struct
|
||||
assert "moveToLayer(entity, placement.layer_id)" in struct
|
||||
assert "!designIds.has(entity.id as string)" in struct
|
||||
page = (ROOT / "B07_DesignDetail/B07_DesignDetail_UI_Page.ts").read_text(encoding="utf-8")
|
||||
assert "response.drawing.stale_layers" in page
|
||||
@@ -0,0 +1,30 @@
|
||||
"""217-1 B06 알림 칩 줄 여백 줄임 — 줄 위 여백 16→8 · 칩 줄 ↔ 첫 카드 28→16 (CSS 값 고정)."""
|
||||
|
||||
from test_196_1_b06_notice_chips import B06
|
||||
|
||||
CSS = (B06 / "B06_Section_UI_Style_Cross.css").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _rule(selector: str) -> str:
|
||||
body = CSS[CSS.index(selector + " {") :]
|
||||
return body[: body.index("}")]
|
||||
|
||||
|
||||
def test_칩_줄_위_여백은_8_아래_여백은_0():
|
||||
bar = _rule(".b06-section__notice-bar")
|
||||
assert "padding-block: var(--spacing-8) 0;" in bar
|
||||
assert "spacing-16) var(--spacing-4)" not in bar
|
||||
|
||||
|
||||
def test_첫_카드와의_간격은_카드_칸_gap_24에서_8을_당겨_16():
|
||||
bar = _rule(".b06-section__notice-bar")
|
||||
# 위 = 카드 칸 padding(16) 상쇄 · 아래 = gap(24) 중 8 당김
|
||||
assert "margin-block: calc(-1 * var(--spacing-16)) calc(-1 * var(--spacing-8));" in bar
|
||||
assert "gap: var(--spacing-24)" in _rule(".b06-section__cards")
|
||||
|
||||
|
||||
def test_sticky_top_과_scroll_margin_그대로():
|
||||
bar = _rule(".b06-section__notice-bar")
|
||||
assert "position: sticky" in bar
|
||||
assert "top: calc(-1 * var(--spacing-16))" in bar
|
||||
assert "scroll-margin-top: calc(var(--b06-notice-h, 0px) + var(--spacing-8))" in CSS
|
||||
Reference in New Issue
Block a user