fix(526-1): B07 횡단 장 인쇄 칸선 지움 · 칸마다 안내선 · 처음 맞춤 작도 영역까지
- 횡단 장 칸 격자선(도각 층 인쇄 개체) 지움 — 칸은 화면 안내선 겹판(guide.cells · 단면마다 점선) · 안내선 숨기기에 함께 꺼짐 · 넘친 칸 붉은 틀은 그대로 - 옛 확정 · 저장본 장은 열 때 칸선만 걷고 칸 안내선은 지금 배치로(refresh_saved_sheet) - CAD 처음 맞춤 · 전체 맞춤 = 내용 + 작도 영역(가이드) — 표준 횡단도가 내용에만 확대돼 도면 한 장 안 자리가 안 보이던 흠 - 낡은 시험 고침: test_169 칸선 단언 → 칸선 없음 + 칸 안내선 · test_212_3 「남는 검은 선 = 인쇄 칸선 지우지 않음」(212-3 결정) → 요구 343-4 로 칸선 지움 · guide-area 시험 칸 그리기 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,19 +3,22 @@
|
||||
채우는 순서는 **좌하단부터 아래에서 위로, 그다음 오른쪽 열**이다
|
||||
(2026-08-29 사용자 확정). 칸 = 도각 그림칸(usable_area)을 똑같이 나눈 자리 · 단면은
|
||||
칸 가운데 · 작성 척도 1/100 그대로(큰 단면이 칸을 넘으면 겹침 — 사용자 배치로 고침).
|
||||
칸 = 인쇄되는 표 격자선 · 블록 테두리 없음 · 칸보다 큰 단면은 `overflow_cells`(169-2).
|
||||
칸 = 화면 안내선(`guide_cells` · 인쇄 · 저장 안 됨) · 칸선 · 블록 테두리 없음 · 칸보다 큰 단면은
|
||||
`overflow_cells`(169-2).
|
||||
칸 안 = 표를 칸 아래 가운데에 먼저 · 그 위 글 · 그림(도로 중심 = 칸 가운데)(174-1).
|
||||
측점마다 도면층 하나(「횡단 No.3」) = 원지반 · 계획선 · 구조물 · 암 그림 · 종류는 색 · 선 꼴
|
||||
· 표 · 글은 수량표 층(212-2).
|
||||
"""
|
||||
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
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,
|
||||
_POLY_NS,
|
||||
CROSS_BORDER_PAD_MM,
|
||||
CROSS_TABLE_LAYER_ID,
|
||||
DESIGN_LAYER_ID,
|
||||
@@ -24,14 +27,12 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
||||
GROUND_LAYER_ID,
|
||||
ROCK_LAYER_ID,
|
||||
STRUCTURE_LAYER_ID,
|
||||
TABLE_LINE_COLOR,
|
||||
_clip_polyline,
|
||||
_layer,
|
||||
_section_window,
|
||||
build_cross_drawing,
|
||||
cross_mm,
|
||||
points_from_samples,
|
||||
polyline_entity,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Table import (
|
||||
cross_note_block_height,
|
||||
@@ -162,28 +163,41 @@ def plan_cross_sheets(
|
||||
return sheets
|
||||
|
||||
|
||||
def _grid_lines(sheet_id: str, cols: int, rows: int) -> list[dict[str, Any]]:
|
||||
"""장 칸 격자선(169-2) — 세로 열 + 1 · 가로 행 + 1 · 도각 층(인쇄 · DXF 에 나감)."""
|
||||
def strip_grid_lines(sheet_id: str, entities: list[Any]) -> list[Any]:
|
||||
"""옛 저장본의 인쇄 칸선(169-2 · `:grid-v0` …) 걷음 — 칸은 이제 화면 안내선(`guide_cells`)."""
|
||||
seeds = (f"{sheet_id}:{FRAME_LAYER_ID}:grid-{axis}{i}" for axis in "vh" for i in range(33))
|
||||
ids = {str(uuid5(_POLY_NS, seed)) for seed in seeds}
|
||||
return [
|
||||
entity for entity in entities if not (isinstance(entity, dict) and entity.get("id") in ids)
|
||||
]
|
||||
|
||||
|
||||
def sheet_cells(sheet: dict[str, Any], count: int | None = None) -> list[list[float]]:
|
||||
"""장 칸 [x0, y0, x1, y1] — 앞 `count` 개(없으면 측점 수) · 칸 가운데 = `slots`."""
|
||||
cols, rows = sheet.get("grid") or (4, 4)
|
||||
usable_w, usable_h = usable_area()
|
||||
left, bottom = -usable_w / 2.0, -usable_h / 2.0
|
||||
lines = [
|
||||
((left + usable_w * i / cols, bottom), (left + usable_w * i / cols, -bottom), f"v{i}")
|
||||
for i in range(cols + 1)
|
||||
] + [
|
||||
((left, bottom + usable_h * j / rows), (-left, bottom + usable_h * j / rows), f"h{j}")
|
||||
for j in range(rows + 1)
|
||||
cell_w, cell_h = usable_w / cols, usable_h / rows
|
||||
slots = (sheet.get("slots") or [])[
|
||||
: len(sheet.get("chainages") or []) if count is None else count
|
||||
]
|
||||
return [
|
||||
entity
|
||||
for start, end, name in lines
|
||||
if (
|
||||
entity := polyline_entity(
|
||||
sheet_id, [start, end], FRAME_LAYER_ID, TABLE_LINE_COLOR, f":grid-{name}"
|
||||
)
|
||||
)
|
||||
[x - cell_w / 2.0, y - cell_h / 2.0, x + cell_w / 2.0, y + cell_h / 2.0] for x, y in slots
|
||||
]
|
||||
|
||||
|
||||
def refresh_saved_sheet(
|
||||
drawing_id: str, saved: dict[str, Any], plan: Callable[[], list[dict[str, Any]]]
|
||||
) -> None:
|
||||
"""확정 · 저장본 장 — 옛 인쇄 칸선 걷고 칸 안내선은 지금 배치로(526-1) · 원본 없으면 칸 없음."""
|
||||
saved["entities"] = strip_grid_lines(drawing_id, saved.get("entities") or [])
|
||||
try:
|
||||
sheets = plan()
|
||||
except FileNotFoundError:
|
||||
sheets = []
|
||||
sheet = next((item for item in sheets if item["id"] == drawing_id), None)
|
||||
saved["guide_cells"] = sheet_cells(sheet) if sheet else []
|
||||
|
||||
|
||||
def _move_to_layer(entity: dict[str, Any], layer_id: str) -> None:
|
||||
"""그림 종류 층 개체(자식 선까지)를 측점 층으로 — id 는 그대로(212-2)."""
|
||||
if entity.get("layerId") in _DRAWING_LAYER_IDS:
|
||||
@@ -198,22 +212,12 @@ def build_cross_sheet(sheet: dict[str, Any], sections: 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)
|
||||
cells = sheet_cells(sheet, len(sections))
|
||||
usable_w, usable_h = usable_area()
|
||||
cell_w, cell_h = usable_w / cols, usable_h / rows
|
||||
|
||||
for index, section in enumerate(sections):
|
||||
if index >= len(slots):
|
||||
break
|
||||
for section, cell in zip(sections, cells):
|
||||
seed_id = f"{sheet['id']}:{section['chainage']}"
|
||||
center_x, center_y = slots[index]
|
||||
cell = [
|
||||
center_x - cell_w / 2.0,
|
||||
center_y - cell_h / 2.0,
|
||||
center_x + cell_w / 2.0,
|
||||
center_y + cell_h / 2.0,
|
||||
]
|
||||
center_x = (cell[0] + cell[2]) / 2.0
|
||||
# 표 아래선 = 칸 아래 + 여백 · 도로 중심 = 칸 가운데 · 글 · 그림은 위로(174-1)
|
||||
placed = build_cross_drawing(
|
||||
section["source"],
|
||||
@@ -254,7 +258,6 @@ def build_cross_sheet(sheet: dict[str, Any], sections: list[dict[str, Any]]) ->
|
||||
placements.extend(placed.get("cross_placements") or [])
|
||||
|
||||
if entities:
|
||||
entities.extend(_grid_lines(sheet["id"], cols, rows))
|
||||
scale = drawing_scale("cross")
|
||||
# 가로 · 세로가 같으면 분모 하나 · 다르면 이름표(H · V)(485)
|
||||
scales = (
|
||||
@@ -262,7 +265,7 @@ def build_cross_sheet(sheet: dict[str, Any], sections: list[dict[str, Any]]) ->
|
||||
if scale["x"] == scale["y"]
|
||||
else (("H", scale["x"]), ("V", scale["y"]))
|
||||
)
|
||||
# 도각 = 고정 그림칸 — 내용 크기와 무관하게 칸 격자와 같은 자리(140-3).
|
||||
# 도각 = 고정 그림칸 — 내용 크기와 무관하게 칸과 같은 자리(140-3).
|
||||
entities.extend(
|
||||
frame_entities(
|
||||
sheet["id"],
|
||||
@@ -278,6 +281,8 @@ def build_cross_sheet(sheet: dict[str, Any], sections: list[dict[str, Any]]) ->
|
||||
"cross_placements": placements,
|
||||
# 칸보다 큰 단면의 칸 [x0, y0, x1, y1] — 라우터가 CAD 가이드 붉은 틀로 보냄(169-2)
|
||||
"overflow_cells": overflow,
|
||||
# 단면마다 칸 — 라우터가 CAD 가이드 칸 점선으로 보냄(인쇄 칸선 대신 · 526-1)
|
||||
"guide_cells": cells,
|
||||
"layers": [
|
||||
*station_layers,
|
||||
_layer(CROSS_TABLE_LAYER_ID, "수량 산출표"),
|
||||
|
||||
@@ -149,11 +149,14 @@ def _asset_data_url(relative_path: str | None) -> str:
|
||||
|
||||
|
||||
def _drawing_guide(drawing: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""CAD 가이드(169-1 · 169-2) — 작도 영역(도면 좌표) + 넘친 칸. 도각이 없으면 None."""
|
||||
"""CAD 가이드(169-1 · 169-2 · 526-1) — 작도 영역 + 넘친 칸 + 횡단 칸. 도각 없으면 None."""
|
||||
entities = drawing.get("entities") or []
|
||||
# 구조물 도면(376) = M02 얹는 도각 층
|
||||
area = frame_guide_area(entities) or frame_guide_area(entities, STRUCTURE_FRAME_LAYER)
|
||||
return {"area": area, "marks": drawing.get("overflow_cells") or []} if area else None
|
||||
if not area:
|
||||
return None
|
||||
marks = drawing.get("overflow_cells") or []
|
||||
return {"area": area, "marks": marks, "cells": drawing.get("guide_cells") or []}
|
||||
|
||||
|
||||
async def title_block_fields(project_id: UUID) -> dict[str, str]:
|
||||
|
||||
@@ -51,6 +51,7 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Sheet import (
|
||||
STATION_LAYER_PREFIX,
|
||||
build_cross_sheet,
|
||||
plan_cross_sheets,
|
||||
refresh_saved_sheet,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Standard import (
|
||||
STANDARD_LABEL,
|
||||
@@ -511,6 +512,9 @@ def _read_drawing(
|
||||
for layer in layers:
|
||||
if layer.get("id") == GROUND_LAYER_ID:
|
||||
layer["isLocked"] = False
|
||||
if CROSS_SHEET_ID.fullmatch(drawing_id):
|
||||
plan = lambda: _cross_sheet_plan(project_root, longitudinal_path) # noqa: E731
|
||||
refresh_saved_sheet(drawing_id, saved, plan)
|
||||
if kind == "cross" and manifest_entry.get("layout") != CROSS_SHEET_LAYOUT:
|
||||
saved["stale_layout"] = True # 화면이 [수정] → 새 배치로 다시 만들기를 안내
|
||||
elif kind == "cross" and drawing_id.startswith("cross_s"):
|
||||
|
||||
@@ -26,9 +26,22 @@ export const setFitMinHeight = (value: unknown): void => {
|
||||
fitMinHeight = typeof value === 'number' && value > 0 ? value : 0;
|
||||
};
|
||||
|
||||
/** 개체 범위를 화면 가운데에 놓는 배율 · 화면 원점(월드 좌표) */
|
||||
export function fitView(entities: Entity[], canvasSize: Point): { zoom: number; offset: Point } {
|
||||
/**
|
||||
* 개체 범위를 화면 가운데에 놓는 배율 · 화면 원점(월드 좌표).
|
||||
* `include` = 범위에 더할 [왼 x, 아래 y, 오른 x, 위 y] — 작도 영역(526-1 · 작은 그림이 칸 크기로 과확대되지 않게)
|
||||
*/
|
||||
export function fitView(
|
||||
entities: Entity[],
|
||||
canvasSize: Point,
|
||||
include?: readonly [number, number, number, number]
|
||||
): { zoom: number; offset: Point } {
|
||||
const boundingBox = { ...getBoundingBoxOfMultipleEntities(entities) };
|
||||
if (include) {
|
||||
boundingBox.minX = Math.min(boundingBox.minX, include[0]);
|
||||
boundingBox.minY = Math.min(boundingBox.minY, include[1]);
|
||||
boundingBox.maxX = Math.max(boundingBox.maxX, include[2]);
|
||||
boundingBox.maxY = Math.max(boundingBox.maxY, include[3]);
|
||||
}
|
||||
// 글 몇 줄뿐인 도면이 칸 높이로 과확대되지 않게 — 범위를 위 끝에서 아래로 최소 높이까지 늘림
|
||||
boundingBox.minY = Math.min(boundingBox.minY, boundingBox.maxY - fitMinHeight);
|
||||
const boundingWidth = boundingBox.maxX - boundingBox.minX;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getGridEnabled, triggerReactUpdate } from '../state.ts';
|
||||
import { paintColor, themeColor } from '../theme.ts';
|
||||
import { DEFAULT_TEXT_OPTIONS, type DrawController } from './DrawController';
|
||||
import { fitEntities, fitView } from './fit-view.ts';
|
||||
import { getGuide } from '../integration/guide-area.ts';
|
||||
|
||||
/** 토큰을 못 읽을 때만 쓰는 캔버스 배경 폴백. */
|
||||
const CANVAS_BACKGROUND_FALLBACK = '#111';
|
||||
@@ -157,7 +158,8 @@ export class ScreenCanvasDrawController implements DrawController {
|
||||
this.viewFitted = true;
|
||||
return;
|
||||
}
|
||||
const { zoom, offset } = fitView(entities, this.getCanvasSize());
|
||||
// 작도 영역(가이드)까지 — 그림이 작아도 도면 한 장 안 자리가 보이게(526-1)
|
||||
const { zoom, offset } = fitView(entities, this.getCanvasSize(), getGuide()?.area);
|
||||
this.setScreenScale(zoom);
|
||||
this.setScreenOffset(offset);
|
||||
this.viewFitted = true;
|
||||
|
||||
@@ -21,8 +21,21 @@ describe('작도 영역 가이드(169-1 · 169-3)', () => {
|
||||
expect(getGuide()).toBeNull();
|
||||
setGuideArea(AREA);
|
||||
expect(getGuide()).toBeNull();
|
||||
setGuide({ area: AREA, marks: [[0, 0, 1, 1], 'x'], editable: true });
|
||||
expect(getGuide()).toEqual({ area: AREA, marks: [[0, 0, 1, 1]], editable: true });
|
||||
setGuide({
|
||||
area: AREA,
|
||||
marks: [[0, 0, 1, 1], 'x'],
|
||||
cells: [
|
||||
[0, 0, 2, 2],
|
||||
[3, 3, 1, 1],
|
||||
],
|
||||
editable: true,
|
||||
});
|
||||
expect(getGuide()).toEqual({
|
||||
area: AREA,
|
||||
marks: [[0, 0, 1, 1]],
|
||||
cells: [[0, 0, 2, 2]],
|
||||
editable: true,
|
||||
});
|
||||
setGuideArea([0, 0, 100, 50]);
|
||||
expect(getGuide()?.area).toEqual([0, 0, 100, 50]);
|
||||
setGuide(null);
|
||||
@@ -54,12 +67,14 @@ describe('작도 영역 가이드(169-1 · 169-3)', () => {
|
||||
drawLineScreen: lines,
|
||||
fillRectScreen: vi.fn(),
|
||||
} as unknown as ScreenCanvasDrawController;
|
||||
const guide = { area: AREA, marks: [[0, 0, 5, 5] as GuideArea], editable: false };
|
||||
// 횡단 칸(526-1) 둘 — 단면마다 점선 4 변
|
||||
const cells = [[0, 0, 5, 5] as GuideArea, [5, 0, 10, 5] as GuideArea];
|
||||
const guide = { area: AREA, marks: [[0, 0, 5, 5] as GuideArea], cells, editable: false };
|
||||
|
||||
setGuide(guide);
|
||||
expect(isGuideVisible()).toBe(true); // 기본 = 보임
|
||||
drawGuide(controller);
|
||||
expect(lines).toHaveBeenCalledTimes(8); // 점선 영역 4 변 + 붉은 틀 4 변
|
||||
expect(lines).toHaveBeenCalledTimes(16); // 점선 영역 4 변 + 칸 2 × 4 변 + 붉은 틀 4 변
|
||||
|
||||
setGuideVisible(false);
|
||||
lines.mockClear();
|
||||
@@ -74,7 +89,7 @@ describe('작도 영역 가이드(169-1 · 169-3)', () => {
|
||||
|
||||
setGuideVisible(true);
|
||||
drawGuide(controller);
|
||||
expect(lines).toHaveBeenCalledTimes(8);
|
||||
expect(lines).toHaveBeenCalledTimes(16);
|
||||
setGuideVisible('x'); // 어긋난 값 = 보임
|
||||
expect(isGuideVisible()).toBe(true);
|
||||
setGuide(null);
|
||||
|
||||
@@ -18,6 +18,8 @@ export interface Guide {
|
||||
area: GuideArea;
|
||||
/** 넘친 칸(169-2) — 붉은 사각형으로 짚음 */
|
||||
marks?: GuideArea[];
|
||||
/** 횡단 장 칸(526-1) — 단면마다 점선 사각형(인쇄 칸선 대신) */
|
||||
cells?: GuideArea[];
|
||||
/** 끌어서 고칠 수 있나 — 도각 편집 · M02 양식만 */
|
||||
editable?: boolean;
|
||||
}
|
||||
@@ -60,6 +62,7 @@ export function setGuide(next: unknown): void {
|
||||
? {
|
||||
area: [...candidate.area] as GuideArea,
|
||||
marks: (candidate.marks ?? []).filter(isArea),
|
||||
cells: (candidate.cells ?? []).filter(isArea),
|
||||
editable: candidate.editable === true,
|
||||
}
|
||||
: null;
|
||||
@@ -129,6 +132,8 @@ export function drawGuide(drawController: ScreenCanvasDrawController): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
drawController.setLineStyles(false, false, GUIDE_COLOR, 1, [4, 4]);
|
||||
for (const cell of guide.cells ?? []) strokeRect(drawController, cell);
|
||||
drawController.setLineStyles(false, false, MARK_COLOR, 2, []);
|
||||
for (const mark of guide.marks ?? []) strokeRect(drawController, mark);
|
||||
drawController.setOpacity(1);
|
||||
|
||||
Reference in New Issue
Block a user