Merge remote-tracking branch 'origin/sub_laptop_6' into main_laptop_1
This commit is contained in:
@@ -39,6 +39,8 @@ export interface CadDrawing {
|
||||
}[];
|
||||
/** 횡단 장 — 옛 배치(칸 통일)로 확정된 저장본. [수정]을 누르면 새 배치로 다시 만든다. */
|
||||
stale_layout?: boolean;
|
||||
/** 횡단 장 — 지금과 다른 표 · 글 배율로 확정된 저장본(180-2). */
|
||||
stale_scale?: boolean;
|
||||
layers: {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -178,10 +180,11 @@ export function invalidateDesignDrawing(projectId: string, drawingId: string): P
|
||||
);
|
||||
}
|
||||
|
||||
/** 횡단 장 배치 — 기본 [열, 행] · 장마다 {장 번호: [열, 행]}(140-3). */
|
||||
/** 횡단 장 배치 — 기본 [열, 행] · 장마다 {장 번호: [열, 행]}(140-3) · 표 · 글 배율(180-2). */
|
||||
export interface CrossLayout {
|
||||
default: [number, number];
|
||||
sheets: Record<string, [number, number]>;
|
||||
table_scale?: number;
|
||||
}
|
||||
|
||||
export function fetchCrossLayout(projectId: string): Promise<CrossLayout> {
|
||||
|
||||
@@ -19,7 +19,7 @@ from uuid import UUID, uuid5
|
||||
|
||||
from config.config_system import DRAWING_SCALE_CROSS
|
||||
|
||||
# 레이어 정의 — 지표면선은 상세설계 제어 대상에서 제외하므로 잠금한다(N-1-3).
|
||||
# 레이어 정의 — 지표면선은 종단에서 잠금(N-1-3) · 횡단은 장에서 옮기므로 풂(180-1).
|
||||
GROUND_LAYER_ID = "b08-ground"
|
||||
GROUND_COLOR = "#f5f7fa"
|
||||
DESIGN_LAYER_ID = "b08-design"
|
||||
@@ -498,6 +498,7 @@ def build_cross_drawing(
|
||||
overlay: tuple[float, float, float, float] | None = None,
|
||||
border: bool = True,
|
||||
table_bottom: float | None = None,
|
||||
table_scale: float = 1.0,
|
||||
) -> dict[str, Any]:
|
||||
"""횡단도 한 장을 지표/설계/구조물(+암 경계) 레이어 + CAD 수량 산출표로 만든다.
|
||||
|
||||
@@ -507,6 +508,7 @@ def build_cross_drawing(
|
||||
배치에 쓰지 않지만 향후 표고 주석용으로 시그니처를 유지한다.
|
||||
table_bottom(장 칸 · 174-1)을 주면 표 아래선을 그 y 에 두고 글 · 그림을 위로 쌓는다 —
|
||||
표 · 글 가운데 x = 도로 중심(origin x) · origin y 는 쌓은 결과로 정함.
|
||||
table_scale = 표 · 표 위 글 배율(180-2) — 8 mm 간격은 그대로.
|
||||
"""
|
||||
ox, oy = origin
|
||||
raw_ground = points_from_samples(source.get("samples", []), "offset_m")
|
||||
@@ -546,7 +548,11 @@ def build_cross_drawing(
|
||||
# 아래에서 위로 — 표 → 글(150-3) → 간격 8 → 그림(174-1)
|
||||
stack = 0.0
|
||||
if quantity_table is not None:
|
||||
stack = cross_table_height() + cross_note_height(cross_note_lines(design)) + 8.0
|
||||
stack = (
|
||||
cross_table_height(table_scale)
|
||||
+ cross_note_height(cross_note_lines(design), table_scale)
|
||||
+ 8.0
|
||||
)
|
||||
oy = table_bottom + stack + own_half_height * CROSS_MM
|
||||
|
||||
def paper(point: tuple[float, float]) -> tuple[float, float]:
|
||||
@@ -576,18 +582,23 @@ def build_cross_drawing(
|
||||
table_top = oy - half_height - 8.0
|
||||
# 표 위 글(150-3) — 줄 수만큼 표를 내림
|
||||
note_lines = cross_note_lines(design)
|
||||
entities.extend(_cross_note_entities(drawing_id, note_lines, table_top, center_x))
|
||||
table_top -= cross_note_height(note_lines)
|
||||
entities.extend(
|
||||
_cross_table_entities(drawing_id, quantity_table, table_top, title_label, center_x)
|
||||
_cross_note_entities(drawing_id, note_lines, table_top, center_x, table_scale)
|
||||
)
|
||||
table_bottom = table_top - cross_table_height()
|
||||
table_top -= cross_note_height(note_lines, table_scale)
|
||||
entities.extend(
|
||||
_cross_table_entities(
|
||||
drawing_id, quantity_table, table_top, title_label, center_x, table_scale
|
||||
)
|
||||
)
|
||||
table_bottom = table_top - cross_table_height(table_scale)
|
||||
|
||||
# 외곽 테두리: 단면 범위와 표를 함께 감싸고 1 mm 띄운다.
|
||||
pad = CROSS_BORDER_PAD_MM
|
||||
# 그림 범위와 표 중 바깥 — 장 칸에서는 표가 도로 중심이라 좌우가 다를 수 있음(174-1)
|
||||
frame_left = min(ox + x0 * CROSS_MM, center_x - cross_table_width() / 2.0) - pad
|
||||
frame_right = max(ox + x1 * CROSS_MM, center_x + cross_table_width() / 2.0) + pad
|
||||
table_half = cross_table_width(table_scale) / 2.0
|
||||
frame_left = min(ox + x0 * CROSS_MM, center_x - table_half) - pad
|
||||
frame_right = max(ox + x1 * CROSS_MM, center_x + table_half) + pad
|
||||
frame_top = oy + half_height + pad
|
||||
frame_bottom = table_bottom - pad
|
||||
corners = [
|
||||
@@ -622,7 +633,7 @@ def build_cross_drawing(
|
||||
}
|
||||
],
|
||||
"layers": [
|
||||
_layer(GROUND_LAYER_ID, "원지반", locked=True),
|
||||
_layer(GROUND_LAYER_ID, "원지반"),
|
||||
_layer(DESIGN_LAYER_ID, "계획선"),
|
||||
_layer(STRUCTURE_LAYER_ID, "구조물"),
|
||||
_layer(ROCK_LAYER_ID, "암 경계선"),
|
||||
|
||||
@@ -9,9 +9,11 @@
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
from uuid import uuid5
|
||||
|
||||
from B05_Profile.B05_Profile_Engine_Sections import cross_key
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
||||
_ENTITY_NS,
|
||||
CROSS_BORDER_PAD_MM,
|
||||
CROSS_MM,
|
||||
CROSS_TABLE_LAYER_ID,
|
||||
@@ -70,6 +72,7 @@ def section_block_size(
|
||||
with_table: bool = True,
|
||||
design: dict[str, Any] | None = None,
|
||||
overlay: tuple[float, float, float, float] | None = None,
|
||||
table_scale: float = 1.0,
|
||||
) -> tuple[float, float]:
|
||||
"""단면 블록 하나의 종이 크기(mm) — 엔티티를 만들지 않고 치수만 잰다.
|
||||
|
||||
@@ -91,13 +94,14 @@ def section_block_size(
|
||||
|
||||
width = 2.0 * max(
|
||||
(x1 - x0) / 2.0 * CROSS_MM + _BORDER_PAD_MM,
|
||||
cross_table_width() / 2.0 + _BORDER_PAD_MM,
|
||||
cross_table_width(table_scale) / 2.0 + _BORDER_PAD_MM,
|
||||
)
|
||||
# 위: 단면 반높이 + 테두리 여유 / 아래: 반높이 + (표 간격 + 표) + 테두리 여유
|
||||
top = half_height + _BORDER_PAD_MM
|
||||
below = half_height + _BORDER_PAD_MM
|
||||
if with_table:
|
||||
below += 8.0 + cross_note_height(cross_note_lines(design_spec)) + cross_table_height()
|
||||
note_h = cross_note_height(cross_note_lines(design_spec), table_scale)
|
||||
below += 8.0 + note_h + cross_table_height(table_scale)
|
||||
return (width, top + below)
|
||||
|
||||
|
||||
@@ -144,6 +148,8 @@ def plan_cross_sheets(
|
||||
"chainages": group,
|
||||
"grid": [cols, rows],
|
||||
"slots": grid_slots(len(group), cols, rows),
|
||||
# 표 · 글 배율(180-2) — 모든 장 한 값
|
||||
"table_scale": float((layout or {}).get("table_scale") or 1.0),
|
||||
}
|
||||
)
|
||||
start += len(group)
|
||||
@@ -205,12 +211,19 @@ def build_cross_sheet(sheet: dict[str, Any], sections: list[dict[str, Any]]) ->
|
||||
overlay=section.get("overlay"),
|
||||
border=False,
|
||||
table_bottom=cell[1] + CELL_TABLE_PAD_MM,
|
||||
table_scale=float(sheet.get("table_scale") or 1.0),
|
||||
)
|
||||
# 칸을 넘는 단면 — 축척 그대로 두고 그 칸을 화면 가이드로 짚음(169-2)
|
||||
frames = [p.get("frame") for p in placed.get("cross_placements") or []]
|
||||
left, _bottom, right, top = frames[0] if frames and frames[0] else cell
|
||||
if left < cell[0] - 1e-6 or right > cell[2] + 1e-6 or top > cell[3] + 1e-6:
|
||||
overflow.append(cell)
|
||||
# 측점 한 벌 = groupId 하나 — 한 번 클릭에 그림 · 표 · 글 함께(180-1)
|
||||
group_id = str(uuid5(_ENTITY_NS, f"{seed_id}:group"))
|
||||
for entity in placed["entities"]:
|
||||
entity["groupId"] = group_id
|
||||
for placement in placed.get("cross_placements") or []:
|
||||
placement["group_id"] = group_id
|
||||
entities.extend(placed["entities"])
|
||||
placements.extend(placed.get("cross_placements") or [])
|
||||
|
||||
@@ -233,7 +246,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, "원지반", locked=True),
|
||||
_layer(GROUND_LAYER_ID, "원지반"),
|
||||
_layer(DESIGN_LAYER_ID, "계획선"),
|
||||
_layer(STRUCTURE_LAYER_ID, "구조물"),
|
||||
_layer(ROCK_LAYER_ID, "암 경계선"),
|
||||
|
||||
@@ -128,16 +128,17 @@ def _cross_table_entities(
|
||||
table_top: float,
|
||||
title_label: str,
|
||||
center_x: float = 0.0,
|
||||
scale: float = 1.0,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""횡단 수량 산출표(납품 양식)를 표 객체 하나로 만든다.
|
||||
"""횡단 수량 산출표(납품 양식)를 표 객체 하나로 만든다 — scale = 표 · 글 배율(180-2).
|
||||
|
||||
머리행은 폭을 8등분하고 본문은 11열 가중치를 쓴다 — 격자가 서로 다르므로 두 경계를
|
||||
합친 열로 표를 만들고 병합으로 원래 칸을 되살린다. 병합 자리에서 선을 끊는 일은
|
||||
표가 스스로 하므로, 예전처럼 가로선을 손으로 끊지 않는다.
|
||||
"""
|
||||
width = _CROSS_TABLE_WIDTH
|
||||
row_h = _CROSS_TABLE_ROW_HEIGHT
|
||||
font = _CROSS_TABLE_FONT
|
||||
width = _CROSS_TABLE_WIDTH * scale
|
||||
row_h = _CROSS_TABLE_ROW_HEIGHT * scale
|
||||
font = _CROSS_TABLE_FONT * scale
|
||||
left = center_x - width / 2.0
|
||||
right = center_x + width / 2.0
|
||||
body_edges = _cross_column_edges(width, center_x)
|
||||
@@ -331,24 +332,24 @@ def cross_note_lines(design: dict[str, Any] | None) -> list[str]:
|
||||
return [line.rstrip() for line in note.strip().splitlines()]
|
||||
|
||||
|
||||
def cross_note_height(lines: list[str]) -> float:
|
||||
"""표 위 글 높이(mm) — 줄 수 × 줄 높이."""
|
||||
return _CROSS_NOTE_LINE_HEIGHT * len(lines)
|
||||
def cross_note_height(lines: list[str], scale: float = 1.0) -> float:
|
||||
"""표 위 글 높이(mm) — 줄 수 × 줄 높이 × 배율."""
|
||||
return _CROSS_NOTE_LINE_HEIGHT * scale * len(lines)
|
||||
|
||||
|
||||
def _cross_note_entities(
|
||||
drawing_id: str, lines: list[str], top: float, center_x: float = 0.0
|
||||
drawing_id: str, lines: list[str], top: float, center_x: float = 0.0, scale: float = 1.0
|
||||
) -> list[dict[str, Any]]:
|
||||
"""표 위 글 줄마다 Text — 표 왼쪽 끝 맞춤 · 위에서 아래로(id `{도면}:note:{n}`)."""
|
||||
left = center_x - _CROSS_TABLE_WIDTH / 2.0
|
||||
left = center_x - _CROSS_TABLE_WIDTH * scale / 2.0
|
||||
return [
|
||||
_text_entity(
|
||||
f"{drawing_id}:note:{index}",
|
||||
line,
|
||||
left,
|
||||
top - _CROSS_NOTE_LINE_HEIGHT * (index + 0.5),
|
||||
top - _CROSS_NOTE_LINE_HEIGHT * scale * (index + 0.5),
|
||||
CROSS_TABLE_LAYER_ID,
|
||||
_CROSS_TABLE_FONT,
|
||||
_CROSS_TABLE_FONT * scale,
|
||||
TABLE_LABEL_COLOR,
|
||||
align="left",
|
||||
)
|
||||
@@ -357,11 +358,11 @@ def _cross_note_entities(
|
||||
]
|
||||
|
||||
|
||||
def cross_table_width() -> float:
|
||||
def cross_table_width(scale: float = 1.0) -> float:
|
||||
"""수량 산출표 가로 폭(mm)."""
|
||||
return _CROSS_TABLE_WIDTH
|
||||
return _CROSS_TABLE_WIDTH * scale
|
||||
|
||||
|
||||
def cross_table_height() -> float:
|
||||
def cross_table_height(scale: float = 1.0) -> float:
|
||||
"""수량 산출표 세로 높이(mm) — 머리 2행 + 본문 6행."""
|
||||
return _CROSS_TABLE_ROW_HEIGHT * (len(_CROSS_LEFT_ROWS) + 2)
|
||||
return _CROSS_TABLE_ROW_HEIGHT * scale * (len(_CROSS_LEFT_ROWS) + 2)
|
||||
|
||||
@@ -551,10 +551,11 @@ async def invalidate_design_drawing(
|
||||
|
||||
|
||||
class CrossLayoutRequest(BaseModel):
|
||||
"""횡단 장 배치 — 기본 [열, 행] · 장마다 {장 번호: [열, 행]}(140-3)."""
|
||||
"""횡단 장 배치 — 기본 [열, 행] · 장마다 {장 번호: [열, 행]}(140-3) · 표 · 글 배율(180-2)."""
|
||||
|
||||
default: list[int] = Field(default_factory=lambda: [4, 4])
|
||||
sheets: dict[str, list[int]] = Field(default_factory=dict)
|
||||
table_scale: float = 1.0
|
||||
|
||||
|
||||
@router.get("/{project_id}/cross-layout")
|
||||
|
||||
@@ -104,6 +104,8 @@ CROSS_SHEET_LAYOUT = 5
|
||||
CROSS_LAYOUT_FILE = "cross_layout.json"
|
||||
CROSS_GRID_DEFAULT = [4, 4]
|
||||
_CROSS_GRID_MAX = 10
|
||||
# 표 · 표 위 글 배율(180-2) — 모든 장 한 값 · 기본 1
|
||||
CROSS_SCALE_RANGE = (0.5, 2.0)
|
||||
|
||||
|
||||
def _cross_grid(value: Any) -> list[int]:
|
||||
@@ -116,13 +118,22 @@ def _cross_grid(value: Any) -> list[int]:
|
||||
return [int(value[0]), int(value[1])]
|
||||
|
||||
|
||||
def _cross_scale(value: Any) -> float:
|
||||
low, high = CROSS_SCALE_RANGE
|
||||
if value is None:
|
||||
return 1.0
|
||||
if isinstance(value, bool) or not isinstance(value, (int, float)) or not low <= value <= high:
|
||||
raise ValueError(f"표 배율은 {low}~{high} 사이로 적어 주세요.")
|
||||
return round(float(value), 2)
|
||||
|
||||
|
||||
def read_cross_layout(project_root: Path) -> dict[str, Any]:
|
||||
"""횡단 장 배치 — 파일이 없거나 깨졌으면 기본 4 × 4 만."""
|
||||
"""횡단 장 배치 — 파일이 없거나 깨졌으면 기본 4 × 4 · 배율 1 만."""
|
||||
path = _design_root(project_root) / CROSS_LAYOUT_FILE
|
||||
try:
|
||||
return checked_cross_layout(_read_json(path))
|
||||
except (OSError, ValueError, AttributeError):
|
||||
return {"default": list(CROSS_GRID_DEFAULT), "sheets": {}}
|
||||
return {"default": list(CROSS_GRID_DEFAULT), "sheets": {}, "table_scale": 1.0}
|
||||
|
||||
|
||||
def checked_cross_layout(layout: dict[str, Any]) -> dict[str, Any]:
|
||||
@@ -132,6 +143,7 @@ def checked_cross_layout(layout: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"default": _cross_grid(layout.get("default") or CROSS_GRID_DEFAULT),
|
||||
"sheets": {str(int(key)): _cross_grid(grid) for key, grid in sheets.items()},
|
||||
"table_scale": _cross_scale(layout.get("table_scale")),
|
||||
}
|
||||
|
||||
|
||||
@@ -449,6 +461,10 @@ def _read_drawing(
|
||||
label = str(manifest_entry.get("label") or drawing_id)
|
||||
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)
|
||||
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
|
||||
@@ -594,6 +610,7 @@ def _store_confirmed_drawing(
|
||||
}
|
||||
if item.kind == "cross":
|
||||
entry["layout"] = CROSS_SHEET_LAYOUT
|
||||
entry["table_scale"] = read_cross_layout(project_root)["table_scale"]
|
||||
if item.kind == "cross" and isinstance(quantity_table, dict):
|
||||
entry["quantity_table"] = quantity_table
|
||||
if isinstance(quantity_tables, dict) and quantity_tables:
|
||||
|
||||
@@ -32,6 +32,8 @@ export interface CrossPlacement {
|
||||
x1: number;
|
||||
/** 블록 테두리(종이 mm) [x0, y0, x1, y1] — 그림을 이 안으로 자른다. */
|
||||
frame?: number[];
|
||||
/** 측점 한 벌 묶음(180-1) — 덧그림도 같은 묶음으로 함께 골라짐. */
|
||||
group_id?: string;
|
||||
}
|
||||
|
||||
/** 측점과 도면 배치를 같은 자리로 볼 허용 오차(m). 정본이 누가거리를 0.01m로 끊어 쓴다. */
|
||||
@@ -78,6 +80,7 @@ export async function appendStructureEntities(
|
||||
continue; // 한 측점의 기하 실패가 도면 전체를 막으면 안 된다.
|
||||
}
|
||||
if (!entities.length) continue;
|
||||
if (placement.group_id) for (const entity of entities) entity.groupId = placement.group_id;
|
||||
// 이 블록 설계선은 우리가 다시 그렸다 — 서버가 낸 트림 전 설계선은 걷어낸다.
|
||||
replaced.push(frame);
|
||||
fresh.push(...entities);
|
||||
|
||||
@@ -191,7 +191,13 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
number
|
||||
? (grid) => void save({ ...layout, sheets: { ...layout.sheets, [String(number)]: grid } })
|
||||
: null,
|
||||
(grid) => void save({ default: grid, sheets: {} }),
|
||||
(grid) => void save({ ...layout, default: grid, sheets: {} }),
|
||||
{
|
||||
scale: {
|
||||
value: layout.table_scale ?? 1,
|
||||
onApply: (scale) => void save({ ...layout, table_scale: scale }),
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
};
|
||||
@@ -343,6 +349,11 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
"옛 배치로 확정된 장입니다. [수정]을 누르면 표를 칸 아래 가운데에 맞춘 새 배치로 다시 만듭니다.",
|
||||
"info",
|
||||
);
|
||||
} else if (response.drawing.stale_scale) {
|
||||
showToast(
|
||||
"옛 표 배율로 확정된 장입니다. [수정]을 누르면 지금 배율로 다시 만듭니다.",
|
||||
"info",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
cad.fail(error instanceof Error ? error.message : "CAD 도면을 불러오지 못했습니다.");
|
||||
|
||||
@@ -175,14 +175,24 @@ export function isCrossSheet(drawing: DesignDrawingItem): boolean {
|
||||
return drawing.kind === "cross" && drawing.id.startsWith("cross_s");
|
||||
}
|
||||
|
||||
/** 표 · 글 배율 범위(180-2) — 서버 `CROSS_SCALE_RANGE` 와 같음. */
|
||||
const CROSS_SCALE_MIN = 0.5;
|
||||
const CROSS_SCALE_MAX = 2;
|
||||
|
||||
/** 배치 칸 줄 뒤 덧칸 — 표 배율(180-2). */
|
||||
export interface CrossLayoutExtras {
|
||||
scale?: { value: number; onApply: (scale: number) => void };
|
||||
}
|
||||
|
||||
/**
|
||||
* 횡단 장 배치 칸(140-3 · 153-1) — 「배치 열 [4] 행 [4] [이 장] [모든 장]」.
|
||||
* 횡단 장 배치 칸(140-3 · 153-1) — 「배치 열 [4] 행 [4] [이 장] [모든 장]」 + 「표 배율 [1] [적용]」.
|
||||
* `onApply` 가 없으면(장이 아닌 도면) [이 장]을 끔.
|
||||
*/
|
||||
export function buildCrossLayoutControls(
|
||||
grid: [number, number],
|
||||
onApply: ((grid: [number, number]) => void) | null,
|
||||
onAllDefault: (grid: [number, number]) => void,
|
||||
extras: CrossLayoutExtras = {},
|
||||
): HTMLElement {
|
||||
const box = document.createElement("div");
|
||||
box.className = "b07-cross-layout";
|
||||
@@ -222,6 +232,31 @@ export function buildCrossLayoutControls(
|
||||
action("이 장", "이 장만 이 배치로", onApply),
|
||||
action("모든 장", "모든 장을 이 배치로(장마다 값 지움)", onAllDefault),
|
||||
);
|
||||
if (extras.scale) {
|
||||
const { onApply: applyScale } = extras.scale;
|
||||
const scale = createInputField({
|
||||
label: "표 배율",
|
||||
type: "number",
|
||||
value: String(extras.scale.value),
|
||||
min: CROSS_SCALE_MIN,
|
||||
max: CROSS_SCALE_MAX,
|
||||
compact: true,
|
||||
});
|
||||
scale.input.setAttribute("step", "0.1");
|
||||
const button = createButton({
|
||||
label: "배율 적용",
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => {
|
||||
const value = Number(scale.input.value);
|
||||
if (value >= CROSS_SCALE_MIN && value <= CROSS_SCALE_MAX) applyScale(value);
|
||||
else
|
||||
showToast(`표 배율은 ${CROSS_SCALE_MIN}~${CROSS_SCALE_MAX} 사이로 적어 주세요.`, "error");
|
||||
},
|
||||
});
|
||||
button.title = "모든 장의 표 · 표 위 글 크기(기본 1)";
|
||||
box.append(scale.root, button);
|
||||
}
|
||||
return box;
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -49,6 +49,8 @@
|
||||
"name": "forest-road-webapp",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@kninnug/constrainautor": "^4.1.0",
|
||||
"delaunator": "^5.1.0",
|
||||
"maplibre-gl": "^5.24.0",
|
||||
"three": "^0.185.0"
|
||||
},
|
||||
|
||||
@@ -162,4 +162,30 @@ out.notSheet = {
|
||||
values: inputs().map((n) => n.value),
|
||||
};
|
||||
|
||||
// 180-2 표 배율 칸 — 배치 칸 줄 뒤 · 0.5~2.0 · step 0.1 · [배율 적용]
|
||||
const scales = [];
|
||||
const toastStart = toasts.length;
|
||||
slot.replaceChildren(
|
||||
buildCrossLayoutControls([4, 4], null, () => {}, {
|
||||
scale: { value: 1, onApply: (value) => scales.push(value) },
|
||||
}),
|
||||
);
|
||||
const scaleInput = inputs()[2];
|
||||
out.scale = {
|
||||
labels: byClass(slot, "ui-field__label").map((n) => n.textContent),
|
||||
input: {
|
||||
value: scaleInput.value,
|
||||
min: scaleInput.attrs.min,
|
||||
max: scaleInput.attrs.max,
|
||||
step: scaleInput.attrs.step,
|
||||
},
|
||||
buttons: buttons().map((n) => n.textContent),
|
||||
};
|
||||
scaleInput.value = "1.5";
|
||||
buttons()[2].click();
|
||||
scaleInput.value = "2.5";
|
||||
buttons()[2].click();
|
||||
out.scale.applied = scales;
|
||||
out.scale.toasts = toasts.slice(toastStart).map((t) => t[0]);
|
||||
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
|
||||
@@ -119,7 +119,11 @@ def _project(tmp_path: Path, count: int) -> tuple[Path, Path]:
|
||||
|
||||
def test_layout_store_and_release_changed_confirmed(tmp_path: Path) -> None:
|
||||
root, longitudinal_path = _project(tmp_path, 20)
|
||||
assert read_cross_layout(root) == {"default": [4, 4], "sheets": {}} # 파일 없으면 기본
|
||||
assert read_cross_layout(root) == {
|
||||
"default": [4, 4],
|
||||
"sheets": {},
|
||||
"table_scale": 1.0,
|
||||
} # 파일 없으면 기본
|
||||
ids = [s["id"] for s in _cross_sheet_plan(root, longitudinal_path)]
|
||||
assert ids == ["cross_s00000m", "cross_s00320m"] # 16 + 4
|
||||
manifest = _read_manifest(root)
|
||||
|
||||
@@ -61,3 +61,12 @@ def test_장_아니면_이_장_꺼짐(out):
|
||||
assert not_sheet["disabled"] == [True, False]
|
||||
assert not_sheet["defaults"] == [[4, 4]]
|
||||
assert not_sheet["values"] == ["4", "4"]
|
||||
|
||||
|
||||
def test_표_배율_칸(out):
|
||||
scale = out["scale"]
|
||||
assert scale["labels"] == ["배치 열", "행", "표 배율"]
|
||||
assert scale["input"] == {"value": "1", "min": "0.5", "max": "2", "step": "0.1"}
|
||||
assert scale["buttons"] == ["이 장", "모든 장", "배율 적용"]
|
||||
assert scale["applied"] == [1.5]
|
||||
assert scale["toasts"] == ["error"] # 2.5 = 범위 밖
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""180-2 B07 횡단 장 — 표 · 표 위 글 배율(모든 장 한 값 · 0.5~2.0 · 기본 1).
|
||||
|
||||
못박는 것
|
||||
- 배율 1.5 = 표 폭 162 · 높이 60 · 글 줄 높이 × 1.5 · 표 아래선은 칸 아래 + 여백 그대로.
|
||||
- 그림 아래 끝 = 글(없으면 표) 위 8 mm(간격은 배율 안 탐) · 넓은 표가 칸을 넘으면 overflow_cells.
|
||||
- 배율 1 = 배율 칸 없는 배치와 같은 엔티티.
|
||||
- cross_layout.json 이 배율을 지킴 · 범위 밖 = 거절 · 배율만 바꾸면 확정 장 안 풂.
|
||||
- 지금과 다른 배율로 확정된 장 = 그대로 + stale_scale 안내.
|
||||
"""
|
||||
|
||||
import json
|
||||
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_Sheet import (
|
||||
CELL_TABLE_PAD_MM,
|
||||
build_cross_sheet,
|
||||
plan_cross_sheets,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Template import (
|
||||
entities_bbox,
|
||||
usable_area,
|
||||
use_project_templates,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support import (
|
||||
CROSS_SHEET_LAYOUT,
|
||||
_cross_sheet_plan,
|
||||
_read_drawing,
|
||||
_read_manifest,
|
||||
_write_manifest,
|
||||
read_cross_layout,
|
||||
write_cross_layout,
|
||||
)
|
||||
|
||||
NOTE = {"structure_note": "L형 측구\n집수정 보강"}
|
||||
|
||||
|
||||
def _source(chainage: float, lo: int = -5, hi: int = 5) -> dict:
|
||||
return {
|
||||
"chainage_m": chainage,
|
||||
"samples": [
|
||||
{"valid": True, "offset_m": float(x), "elevation_m": 100.0 + 0.05 * x}
|
||||
for x in range(lo, hi + 1)
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _drawing(layout: dict, designs: list[dict | None]) -> tuple[dict, dict]:
|
||||
use_project_templates(None)
|
||||
sources = [_source(i * 20.0) for i in range(len(designs))]
|
||||
sheet = plan_cross_sheets([s["chainage_m"] for s in sources], layout)[0]
|
||||
sections = [
|
||||
{
|
||||
"chainage": s["chainage_m"],
|
||||
"source": s,
|
||||
"design": design,
|
||||
"quantity_table": {"ground": 1.0, "planned": 0.5},
|
||||
"title": "No.0",
|
||||
}
|
||||
for s, design in zip(sources, designs, strict=True)
|
||||
]
|
||||
return sheet, build_cross_sheet(sheet, sections)
|
||||
|
||||
|
||||
def _tables(drawing: dict) -> list[dict]:
|
||||
return [e for e in drawing["entities"] if e["type"] == "Table"]
|
||||
|
||||
|
||||
def test_배율_15_표_크기와_쌓기() -> None:
|
||||
sheet, drawing = _drawing({"default": [2, 2], "table_scale": 1.5}, [NOTE, None])
|
||||
assert sheet["table_scale"] == 1.5
|
||||
_w, h = usable_area()
|
||||
tables = _tables(drawing)
|
||||
for table, slot in zip(tables, sheet["slots"], strict=True):
|
||||
shape = table["shapeData"]
|
||||
assert sum(shape["columnWidths"]) == pytest.approx(162.0)
|
||||
assert sum(shape["rowHeights"]) == pytest.approx(60.0)
|
||||
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
|
||||
]
|
||||
# 글 2 줄 × 3.0 × 1.5 = 9 mm · 간격 8 mm 는 배율 안 탐
|
||||
tops = [t["shapeData"]["origin"]["y"] for t in tables]
|
||||
assert grounds[0][1] == pytest.approx(tops[0] + 9.0 + 8.0)
|
||||
assert grounds[1][1] == pytest.approx(tops[1] + 8.0)
|
||||
|
||||
|
||||
def test_배율_1_은_지금과_같음() -> None:
|
||||
_s, plain = _drawing({"default": [2, 2]}, [NOTE, None])
|
||||
_s, one = _drawing({"default": [2, 2], "table_scale": 1.0}, [NOTE, None])
|
||||
assert one["entities"] == plain["entities"]
|
||||
assert sum(_tables(one)[0]["shapeData"]["columnWidths"]) == pytest.approx(108.0)
|
||||
|
||||
|
||||
def test_넓은_표_넘침() -> None:
|
||||
w, _h = usable_area()
|
||||
# 4 × 4 칸 폭 192.5 — 표 108 × 2.0 = 216 이 칸을 넘음
|
||||
_s, drawing = _drawing({"default": [4, 4], "table_scale": 2.0}, [None])
|
||||
assert w / 4 < 216.0
|
||||
assert len(drawing["overflow_cells"]) == 1
|
||||
_s, drawing = _drawing({"default": [4, 4], "table_scale": 1.0}, [None])
|
||||
assert drawing["overflow_cells"] == []
|
||||
|
||||
|
||||
def _project(tmp_path: Path, count: int) -> tuple[Path, Path]:
|
||||
root = tmp_path / "project"
|
||||
cross_dir = root / "B06_Section" / "cross_sections"
|
||||
cross_dir.mkdir(parents=True)
|
||||
longitudinal_path = root / "B06_Section" / "longitudinal" / "longitudinal.json"
|
||||
longitudinal_path.parent.mkdir(parents=True)
|
||||
stations = []
|
||||
for i in range(count):
|
||||
chainage = i * 20
|
||||
stations.append({"chainage_m": float(chainage), "center_x": 0.0, "center_y": 0.0})
|
||||
(cross_dir / f"cross_{chainage:05d}m.json").write_text(
|
||||
json.dumps(_source(float(chainage))), "utf-8"
|
||||
)
|
||||
longitudinal_path.write_text(json.dumps({"stations": stations}), "utf-8")
|
||||
return root, longitudinal_path
|
||||
|
||||
|
||||
def test_배치_파일_배율_저장_범위(tmp_path: Path) -> None:
|
||||
root, longitudinal_path = _project(tmp_path, 5)
|
||||
ids = [s["id"] for s in _cross_sheet_plan(root, longitudinal_path)]
|
||||
manifest = _read_manifest(root)
|
||||
manifest["drawings"] = {i: {"confirmed": True} for i in ids}
|
||||
_write_manifest(root, manifest)
|
||||
# 배율만 바꿈 — 담긴 측점 그대로라 확정 장 안 풂
|
||||
assert (
|
||||
write_cross_layout(root, longitudinal_path, {"default": [4, 4], "table_scale": 1.3}) == []
|
||||
)
|
||||
assert read_cross_layout(root)["table_scale"] == 1.3
|
||||
assert _read_manifest(root)["drawings"][ids[0]]["confirmed"] is True
|
||||
assert _cross_sheet_plan(root, longitudinal_path)[0]["table_scale"] == 1.3
|
||||
for bad in (0.4, 2.1, "1.2", True):
|
||||
with pytest.raises(ValueError):
|
||||
write_cross_layout(root, longitudinal_path, {"default": [4, 4], "table_scale": bad})
|
||||
assert read_cross_layout(root)["table_scale"] == 1.3
|
||||
|
||||
|
||||
def test_옛_배율_확정_장_안내(tmp_path: Path) -> None:
|
||||
stage = tmp_path / "B07_DesignDetail"
|
||||
(stage / "drawings").mkdir(parents=True)
|
||||
saved = {"format": DRAWING_FORMAT, "entities": [], "layers": []}
|
||||
(stage / "drawings" / "cross_s00000m.json").write_text(json.dumps(saved), encoding="utf-8")
|
||||
entry = {"kind": "cross", "label": "1장", "confirmed": True, "layout": CROSS_SHEET_LAYOUT}
|
||||
manifest = {"drawings": {"cross_s00000m": entry}}
|
||||
(stage / "manifest.json").write_text(json.dumps(manifest), 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
|
||||
|
||||
assert "stale_scale" not in read() # 배율 적힘 없음 = 1 · 지금도 1
|
||||
(stage / "cross_layout.json").write_text(
|
||||
json.dumps({"default": [4, 4], "sheets": {}, "table_scale": 1.5}), encoding="utf-8"
|
||||
)
|
||||
assert read().get("stale_scale") is True
|
||||
entry["table_scale"] = 1.5
|
||||
(stage / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
|
||||
assert "stale_scale" not in read()
|
||||
@@ -0,0 +1,85 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""180-1 B07 횡단 장 — 원지반선 고르기 · 측점 한 벌 groupId.
|
||||
|
||||
못박는 것
|
||||
- 횡단 장 · 측점 한 장의 원지반 층은 안 잠김(종단은 그대로 잠김).
|
||||
- 측점마다 groupId 하나(원지반 · 계획선 · 표 · 글 전부) · 측점끼리 다름.
|
||||
- 칸 격자선 · 도각엔 groupId 없음.
|
||||
- cross_placements 의 group_id = 그 측점 엔티티 값(화면 덧그림이 같은 묶음).
|
||||
"""
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
||||
FRAME_LAYER_ID,
|
||||
GROUND_LAYER_ID,
|
||||
build_cross_drawing,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Sheet import (
|
||||
build_cross_sheet,
|
||||
plan_cross_sheets,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Template import use_project_templates
|
||||
|
||||
|
||||
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 _design_line() -> list[dict]:
|
||||
return [{"offset_m": x, "elevation_m": 100.0} for x in (-3.0, 0.0, 3.0)]
|
||||
|
||||
|
||||
def _drawing() -> 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_line": _design_line(),
|
||||
"design": {"structure_note": "L형 측구"},
|
||||
"quantity_table": {"ground": 1.0, "planned": 0.5},
|
||||
"title": f"No.{int(c // 20)}",
|
||||
}
|
||||
for c in chainages
|
||||
]
|
||||
return build_cross_sheet(sheet, sections)
|
||||
|
||||
|
||||
def _ground_locked(drawing: dict) -> bool:
|
||||
return next(layer for layer in drawing["layers"] if layer["id"] == GROUND_LAYER_ID)["isLocked"]
|
||||
|
||||
|
||||
def test_원지반_층_안_잠김() -> None:
|
||||
assert _ground_locked(_drawing()) is False
|
||||
single = build_cross_drawing(_source(0.0), "cross_00000m", _design_line(), None, None, "")
|
||||
assert _ground_locked(single) is False
|
||||
|
||||
|
||||
def test_측점마다_groupId_하나() -> None:
|
||||
drawing = _drawing()
|
||||
placements = drawing["cross_placements"]
|
||||
assert len(placements) == 4
|
||||
group_ids = [p["group_id"] for p in placements]
|
||||
assert len(set(group_ids)) == 4 # 측점끼리 다름
|
||||
grouped = [e for e in drawing["entities"] if "groupId" in e]
|
||||
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
|
||||
# 격자선 · 도각은 묶지 않음
|
||||
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)
|
||||
|
||||
|
||||
def test_groupId_결정적() -> None:
|
||||
first = [p["group_id"] for p in _drawing()["cross_placements"]]
|
||||
assert first == [p["group_id"] for p in _drawing()["cross_placements"]]
|
||||
Reference in New Issue
Block a user