6f8b599629
- execution_parser.py: 解析关卡执行案Markdown提取区域/结构体/布景/边界/光照 - asset_expander.py: BSP资产建议展开+绝对坐标计算+旋转推断 - guide_writer.py: 生成精确匹配build_guide_parser格式的搭建指南 - composite_writer.py: 组合资产清单+JSON模板生成 - converter.py: 主入口CLI工具 - 更新level-execution-template.md: 移除交互物/收集品章节,新增组合资产标记 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
663 lines
25 KiB
Python
663 lines
25 KiB
Python
"""关卡执行案 Markdown 解析器。
|
||
|
||
解析 docs/design/关卡-vXX-<名称>-执行案.md,提取结构化数据供后续转换为搭建指南。
|
||
|
||
执行案的表格格式(简化版,无交互物/收集品):
|
||
- 区域总览表: 区域编号 / 区域名称 / 绝对坐标范围 (X,Y) / 尺寸 / 地面材质建议 / 标识色
|
||
- 交通流线表: 流线路径 / 宽度 / 说明
|
||
- 结构体表: 编号 / 构件名称 / 尺寸 / 相对原点偏移 / BSP 资产建议 / 标识色
|
||
- 布景表: 编号 / 物件名称 / 尺寸 / 相对原点偏移 / BSP 资产建议 / 说明
|
||
- 组合资产表: 编号 / 物件名称 / 期望尺寸 / 推荐拼搭方案 / 说明
|
||
- 全局物件表: 编号 / 物件名称 / 数量 / 放置位置 / BSP 资产建议 / 说明
|
||
- 边界表: 边界段 / 位置范围 / 遮挡方式 / BSP 资产建议 / 标识色
|
||
- 光照表: 光源类型 / 数量 / 位置/方向 / 参数建议 / 说明
|
||
"""
|
||
|
||
import re
|
||
import logging
|
||
from pathlib import Path
|
||
from dataclasses import dataclass, field
|
||
from typing import Optional
|
||
|
||
# 允许从包内或直接运行
|
||
try:
|
||
from parsers.markdown_table import parse_table, clean_cell
|
||
except ImportError:
|
||
import sys
|
||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "level-builder"))
|
||
from parsers.markdown_table import parse_table, clean_cell
|
||
|
||
logger = logging.getLogger("execution_to_guide.parser")
|
||
|
||
|
||
# ============================================================
|
||
# 数据结构
|
||
# ============================================================
|
||
|
||
@dataclass
|
||
class ZoneInfo:
|
||
"""区域信息。"""
|
||
zone_id: str # Z1, Z2, ...
|
||
zone_name: str # 家、小卖部、...
|
||
origin: tuple # (X, Y, Z) 区域西南角绝对坐标
|
||
bounds_x: tuple # (X_min, X_max)
|
||
bounds_y: tuple # (Y_min, Y_max)
|
||
size_m: tuple # (宽m, 深m)
|
||
ground_material: str = "" # 地面材质建议
|
||
tag_color: str = "" # 标识色 MI_DevXxx
|
||
height: float = 0 # 建筑高度(cm)
|
||
wall_height: float = 250 # 围墙高度
|
||
door_width: float = 120 # 门洞宽度
|
||
door_height: float = 220 # 门洞高度
|
||
indoor_clear_height: float = 280 # 室内净高
|
||
eaves_height: float = 300 # 屋檐高度
|
||
|
||
|
||
@dataclass
|
||
class StructureItem:
|
||
"""结构体条目(墙/地/顶/门/窗等建筑构件)。"""
|
||
item_id: str # Z1-S01
|
||
name: str # 南围墙
|
||
size: tuple # (宽, 深, 高) cm
|
||
offset: tuple # (ΔX, ΔY, ΔZ) 相对区域原点
|
||
bsp_suggestion: str # Wall_400x300 x 2
|
||
tag_color: str = "" # MI_DevRed
|
||
|
||
|
||
@dataclass
|
||
class PropItem:
|
||
"""布景物条目(家具/道具/装饰)。"""
|
||
item_id: str # Z1-P01
|
||
name: str # 旧自行车
|
||
size: tuple # (宽, 深, 高) cm(约)
|
||
offset: tuple # (ΔX, ΔY, ΔZ) 相对区域原点
|
||
bsp_suggestion: str # Car01 缩放50%
|
||
note: str = "" # 靠墙停放
|
||
|
||
|
||
@dataclass
|
||
class CompositeItem:
|
||
"""组合资产条目(需 Primitives 拼搭)。"""
|
||
item_id: str # Z1-C01
|
||
name: str # 旧收音机
|
||
target_size: tuple # (宽, 深, 高) cm(约)
|
||
build_plan: str # Cube(0.2,0.15,0.12) + Cylinder天线
|
||
note: str = "" # 放在柜台上
|
||
|
||
|
||
@dataclass
|
||
class GlobalObject:
|
||
"""全局物件。"""
|
||
item_id: str # G-01 / G-E01
|
||
name: str
|
||
count: int
|
||
location: tuple # (X, Y, Z) 绝对坐标
|
||
bsp_suggestion: str
|
||
note: str = ""
|
||
|
||
|
||
@dataclass
|
||
class BoundaryItem:
|
||
"""场景边界。"""
|
||
segment: str # 西边界/南边界/东边界/北边界
|
||
location_range: str
|
||
block_method: str # 围墙/自然遮挡
|
||
bsp_suggestion: str
|
||
tag_color: str = ""
|
||
|
||
|
||
@dataclass
|
||
class LightItem:
|
||
"""光照建议。"""
|
||
light_type: str # 定向光/天空光/路灯
|
||
count: int
|
||
position: str # 位置/方向描述
|
||
params: str # 参数建议
|
||
note: str = ""
|
||
|
||
|
||
@dataclass
|
||
class TrafficFlow:
|
||
"""交通流线。"""
|
||
path: str # Z1 → Z3 家门 → 街道
|
||
width: float # cm
|
||
note: str = ""
|
||
|
||
|
||
@dataclass
|
||
class ExecutionPlanData:
|
||
"""执行案解析后的完整数据。"""
|
||
version: str = ""
|
||
scene_name: str = ""
|
||
scene_size: tuple = (0, 0) # (宽cm, 深cm)
|
||
ground_z: float = 0
|
||
character_height: float = 120
|
||
camera_height: float = 100
|
||
time_setting: str = ""
|
||
zones: list = field(default_factory=list) # list[ZoneInfo]
|
||
flows: list = field(default_factory=list) # list[TrafficFlow]
|
||
zone_structures: dict = field(default_factory=dict) # zone_id -> list[StructureItem]
|
||
zone_props: dict = field(default_factory=dict) # zone_id -> list[PropItem]
|
||
zone_composites: dict = field(default_factory=dict) # zone_id -> list[CompositeItem]
|
||
global_objects: list = field(default_factory=list) # list[GlobalObject]
|
||
boundaries: list = field(default_factory=list) # list[BoundaryItem]
|
||
lights: list = field(default_factory=list) # list[LightItem]
|
||
|
||
|
||
# ============================================================
|
||
# 解析逻辑
|
||
# ============================================================
|
||
|
||
class ExecutionPlanParser:
|
||
"""关卡执行案解析器。"""
|
||
|
||
def __init__(self):
|
||
self.data = ExecutionPlanData()
|
||
|
||
def parse_file(self, filepath: str) -> ExecutionPlanData:
|
||
"""解析执行案文件。"""
|
||
path = Path(filepath)
|
||
if not path.exists():
|
||
raise FileNotFoundError(f"执行案文件不存在: {filepath}")
|
||
text = path.read_text(encoding="utf-8")
|
||
return self.parse_text(text)
|
||
|
||
def parse_text(self, text: str) -> ExecutionPlanData:
|
||
"""解析执行案文本。"""
|
||
lines = text.split("\n")
|
||
|
||
# 解析标题 → 版本和场景名
|
||
self._parse_title(lines)
|
||
|
||
# 一、场景参数
|
||
self._parse_scene_params(lines)
|
||
|
||
# 二、区域坐标布局
|
||
self._parse_zone_layout(lines)
|
||
|
||
# 三、逐区域搭建指南
|
||
self._parse_zone_details(lines)
|
||
|
||
# 四、全局物件清单
|
||
self._parse_global_objects(lines)
|
||
|
||
# 五、场景边界与遮挡
|
||
self._parse_boundaries(lines)
|
||
|
||
# 六、光照建议
|
||
self._parse_lighting(lines)
|
||
|
||
return self.data
|
||
|
||
# ----------------------------------------------------------
|
||
# 标题
|
||
# ----------------------------------------------------------
|
||
def _parse_title(self, lines: list[str]):
|
||
"""从标题提取版本号和场景名。如:# v0.1 关卡执行案 — 城郊小街"""
|
||
for line in lines:
|
||
m = re.match(r'#\s+v([\d.]+)\s+关卡执行案\s*[—\-–]\s*(.+)', line)
|
||
if m:
|
||
self.data.version = m.group(1)
|
||
self.data.scene_name = m.group(2).strip()
|
||
break
|
||
|
||
# ----------------------------------------------------------
|
||
# 一、场景参数
|
||
# ----------------------------------------------------------
|
||
def _parse_scene_params(self, lines: list[str]):
|
||
"""解析场景参数表。"""
|
||
section_start = self._find_section(lines, "一、场景参数")
|
||
section_end = self._find_section(lines, "二、区域坐标布局")
|
||
if section_start < 0:
|
||
return
|
||
if section_end < 0:
|
||
section_end = len(lines)
|
||
|
||
section_lines = lines[section_start:section_end]
|
||
tables = self._extract_tables(section_lines)
|
||
if not tables:
|
||
return
|
||
|
||
for row in tables[0]:
|
||
param = clean_cell(row.get("参数", ""))
|
||
value = clean_cell(row.get("值", ""))
|
||
if not param:
|
||
continue
|
||
|
||
if "场景总尺寸" in param:
|
||
size_match = re.search(r'(\d+)\s*cm\s*[x×]\s*(\d+)\s*cm', value)
|
||
if size_match:
|
||
self.data.scene_size = (int(size_match.group(1)), int(size_match.group(2)))
|
||
elif "地面高度基准" in param:
|
||
z_match = re.search(r'Z?\s*=\s*(-?\d+)', value)
|
||
if z_match:
|
||
self.data.ground_z = float(z_match.group(1))
|
||
elif "主角身高" in param:
|
||
h_match = re.search(r'~?(\d+)', value)
|
||
if h_match:
|
||
self.data.character_height = float(h_match.group(1))
|
||
elif "视角高度" in param:
|
||
h_match = re.search(r'~?(\d+)', value)
|
||
if h_match:
|
||
self.data.camera_height = float(h_match.group(1))
|
||
elif "时间设定" in param:
|
||
self.data.time_setting = value
|
||
|
||
# ----------------------------------------------------------
|
||
# 二、区域坐标布局
|
||
# ----------------------------------------------------------
|
||
def _parse_zone_layout(self, lines: list[str]):
|
||
"""解析区域总览表和交通流线表。"""
|
||
section_start = self._find_section(lines, "二、区域坐标布局")
|
||
section_end = self._find_section(lines, "三、逐区域搭建指南")
|
||
if section_start < 0:
|
||
return
|
||
if section_end < 0:
|
||
section_end = len(lines)
|
||
|
||
section_lines = lines[section_start:section_end]
|
||
tables = self._extract_tables(section_lines)
|
||
|
||
# 第一个表格 = 区域总览表
|
||
if tables:
|
||
for row in tables[0]:
|
||
zone = self._parse_zone_row(row)
|
||
if zone:
|
||
self.data.zones.append(zone)
|
||
|
||
# 第二个表格 = 交通流线(如果有)
|
||
if len(tables) > 1:
|
||
for row in tables[1]:
|
||
path = clean_cell(row.get("流线路径", ""))
|
||
if not path:
|
||
continue
|
||
width_str = clean_cell(row.get("宽度", "200"))
|
||
w_match = re.search(r'(\d+)', width_str)
|
||
width = float(w_match.group(1)) if w_match else 200.0
|
||
note = clean_cell(row.get("说明", ""))
|
||
self.data.flows.append(TrafficFlow(
|
||
path=path, width=width, note=note
|
||
))
|
||
|
||
def _parse_zone_row(self, row: dict) -> Optional[ZoneInfo]:
|
||
"""解析区域总览表的一行。"""
|
||
zone_id = clean_cell(row.get("区域编号", ""))
|
||
zone_name = clean_cell(row.get("区域名称", ""))
|
||
coord_str = clean_cell(row.get("绝对坐标范围 (X,Y)", row.get("绝对坐标范围", "")))
|
||
|
||
if not zone_id or not zone_name:
|
||
return None
|
||
if not re.match(r'Z\d+', zone_id):
|
||
return None
|
||
|
||
# 解析坐标范围:(X1,Y1)→(X2,Y2)
|
||
coord_match = re.match(
|
||
r'\((\d+)\s*,\s*(\d+)\)\s*[→\-–]+\s*\((\d+)\s*,\s*(\d+)\)',
|
||
coord_str
|
||
)
|
||
if not coord_match:
|
||
return None
|
||
|
||
x_min, y_min = int(coord_match.group(1)), int(coord_match.group(2))
|
||
x_max, y_max = int(coord_match.group(3)), int(coord_match.group(4))
|
||
|
||
# 解析尺寸
|
||
size_str = clean_cell(row.get("尺寸 (宽x深)", row.get("尺寸", "")))
|
||
size_match = re.search(r'(\d+)\s*m?\s*[x×]\s*(\d+)\s*m?', size_str)
|
||
size_m = (float(size_match.group(1)), float(size_match.group(2))) if size_match else (0, 0)
|
||
|
||
ground_mat = clean_cell(row.get("地面材质建议", ""))
|
||
tag_color = clean_cell(row.get("标识色", ""))
|
||
|
||
return ZoneInfo(
|
||
zone_id=zone_id,
|
||
zone_name=zone_name,
|
||
origin=(x_min, y_min, 0),
|
||
bounds_x=(x_min, x_max),
|
||
bounds_y=(y_min, y_max),
|
||
size_m=size_m,
|
||
ground_material=ground_mat,
|
||
tag_color=tag_color,
|
||
)
|
||
|
||
# ----------------------------------------------------------
|
||
# 三、逐区域搭建指南
|
||
# ----------------------------------------------------------
|
||
def _parse_zone_details(self, lines: list[str]):
|
||
"""解析逐区域搭建指南(结构体/布景/组合资产)。"""
|
||
section_start = self._find_section(lines, "三、逐区域搭建指南")
|
||
section_end = self._find_section(lines, "四、全局物件清单")
|
||
if section_start < 0:
|
||
return
|
||
if section_end < 0:
|
||
section_end = len(lines)
|
||
|
||
section_lines = lines[section_start:section_end]
|
||
current_zone = ""
|
||
current_zone_info = None
|
||
|
||
i = 0
|
||
while i < len(section_lines):
|
||
line = section_lines[i].strip()
|
||
|
||
# 检测区域标题:### Z1:家(Home)或 ### Z1: 家 或 ### Z1:家
|
||
zm = re.match(r'###\s+(Z\d+)\s*[::]\s*(.+?)(?:\s*[((].+[)])?\s*$', line)
|
||
if zm:
|
||
current_zone = zm.group(1)
|
||
current_zone_info = self._find_zone(current_zone)
|
||
i += 1
|
||
continue
|
||
|
||
# 检测区域绝对原点行:**区域绝对原点**:(X, Y, Z)
|
||
if "区域绝对原点" in line and current_zone:
|
||
origin_match = re.search(r'\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)\)', line)
|
||
if origin_match and current_zone_info:
|
||
current_zone_info.origin = (
|
||
int(origin_match.group(1)),
|
||
int(origin_match.group(2)),
|
||
int(origin_match.group(3)),
|
||
)
|
||
|
||
# 检测区域参数子标题
|
||
if "区域参数" in line:
|
||
i += 1
|
||
i = self._skip_blank(section_lines, i)
|
||
rows = self._read_table(section_lines, i)
|
||
if rows:
|
||
i = self._advance_past_table(section_lines, i)
|
||
if current_zone_info:
|
||
self._parse_zone_params(rows, current_zone_info)
|
||
continue
|
||
|
||
# 检测结构体子标题
|
||
if "结构体清单" in line:
|
||
i += 1
|
||
i = self._skip_blank(section_lines, i)
|
||
rows = self._read_table(section_lines, i)
|
||
if rows and current_zone:
|
||
items = [self._parse_structure_row(r) for r in rows]
|
||
items = [it for it in items if it]
|
||
self.data.zone_structures[current_zone] = items
|
||
i = self._advance_past_table(section_lines, i)
|
||
continue
|
||
|
||
# 检测布景物子标题
|
||
if "布景物清单" in line:
|
||
i += 1
|
||
i = self._skip_blank(section_lines, i)
|
||
rows = self._read_table(section_lines, i)
|
||
if rows and current_zone:
|
||
items = [self._parse_prop_row(r) for r in rows]
|
||
items = [it for it in items if it]
|
||
self.data.zone_props[current_zone] = items
|
||
i = self._advance_past_table(section_lines, i)
|
||
continue
|
||
|
||
# 检测组合资产子标题
|
||
if "组合资产标记" in line:
|
||
i += 1
|
||
i = self._skip_blank(section_lines, i)
|
||
rows = self._read_table(section_lines, i)
|
||
if rows and current_zone:
|
||
items = [self._parse_composite_row(r) for r in rows]
|
||
items = [it for it in items if it]
|
||
self.data.zone_composites[current_zone] = items
|
||
i = self._advance_past_table(section_lines, i)
|
||
continue
|
||
|
||
i += 1
|
||
|
||
def _parse_zone_params(self, rows: list[dict], zone: ZoneInfo):
|
||
"""解析区域参数表。"""
|
||
for row in rows:
|
||
param = clean_cell(row.get("参数", ""))
|
||
value = clean_cell(row.get("值", ""))
|
||
if "围墙" in param or "外墙高度" in param:
|
||
m = re.search(r'(\d+)', value)
|
||
if m:
|
||
zone.wall_height = float(m.group(1))
|
||
elif "门洞宽度" in param:
|
||
m = re.search(r'(\d+)', value)
|
||
if m:
|
||
zone.door_width = float(m.group(1))
|
||
elif "门洞高度" in param:
|
||
m = re.search(r'(\d+)', value)
|
||
if m:
|
||
zone.door_height = float(m.group(1))
|
||
elif "室内净高" in param:
|
||
m = re.search(r'(\d+)', value)
|
||
if m:
|
||
zone.indoor_clear_height = float(m.group(1))
|
||
elif "屋檐高度" in param:
|
||
m = re.search(r'(\d+)', value)
|
||
if m:
|
||
zone.eaves_height = float(m.group(1))
|
||
|
||
def _parse_structure_row(self, row: dict) -> Optional[StructureItem]:
|
||
"""解析结构体表行。"""
|
||
item_id = clean_cell(row.get("编号", ""))
|
||
name = clean_cell(row.get("构件名称", ""))
|
||
if not item_id or not name:
|
||
return None
|
||
|
||
size_str = clean_cell(row.get("尺寸 (宽x深x高, cm)", row.get("尺寸", "")))
|
||
size = self._parse_size(size_str)
|
||
|
||
offset_str = clean_cell(row.get("相对原点偏移 (ΔX,ΔY,ΔZ)", row.get("相对原点偏移", "")))
|
||
offset = self._parse_xyz(offset_str)
|
||
|
||
bsp = clean_cell(row.get("BSP 资产建议", ""))
|
||
tag = clean_cell(row.get("标识色", ""))
|
||
|
||
return StructureItem(
|
||
item_id=item_id, name=name, size=size,
|
||
offset=offset, bsp_suggestion=bsp, tag_color=tag
|
||
)
|
||
|
||
def _parse_prop_row(self, row: dict) -> Optional[PropItem]:
|
||
"""解析布景表行。"""
|
||
item_id = clean_cell(row.get("编号", ""))
|
||
name = clean_cell(row.get("物件名称", ""))
|
||
if not item_id or not name:
|
||
return None
|
||
|
||
size_str = clean_cell(row.get("尺寸 (约, cm)", row.get("尺寸", "")))
|
||
size = self._parse_size(size_str)
|
||
|
||
offset_str = clean_cell(row.get("相对原点偏移 (ΔX,ΔY,ΔZ)", row.get("相对原点偏移", "")))
|
||
offset = self._parse_xyz(offset_str)
|
||
|
||
bsp = clean_cell(row.get("BSP 资产建议", ""))
|
||
note = clean_cell(row.get("说明", ""))
|
||
|
||
return PropItem(
|
||
item_id=item_id, name=name, size=size,
|
||
offset=offset, bsp_suggestion=bsp, note=note
|
||
)
|
||
|
||
def _parse_composite_row(self, row: dict) -> Optional[CompositeItem]:
|
||
"""解析组合资产表行。"""
|
||
item_id = clean_cell(row.get("编号", ""))
|
||
name = clean_cell(row.get("物件名称", ""))
|
||
if not item_id or not name:
|
||
return None
|
||
|
||
size_str = clean_cell(row.get("期望尺寸 (约, cm)", row.get("期望尺寸", "")))
|
||
size = self._parse_size(size_str)
|
||
|
||
plan = clean_cell(row.get("推荐拼搭方案", ""))
|
||
note = clean_cell(row.get("说明", ""))
|
||
|
||
return CompositeItem(
|
||
item_id=item_id, name=name, target_size=size,
|
||
build_plan=plan, note=note
|
||
)
|
||
|
||
# ----------------------------------------------------------
|
||
# 四、全局物件清单
|
||
# ----------------------------------------------------------
|
||
def _parse_global_objects(self, lines: list[str]):
|
||
"""解析全局物件清单。"""
|
||
section_start = self._find_section(lines, "四、全局物件清单")
|
||
section_end = self._find_section(lines, "五、场景边界")
|
||
if section_start < 0:
|
||
return
|
||
if section_end < 0:
|
||
section_end = len(lines)
|
||
|
||
section_lines = lines[section_start:section_end]
|
||
tables = self._extract_tables(section_lines)
|
||
|
||
for table in tables:
|
||
for row in table:
|
||
item_id = clean_cell(row.get("编号", ""))
|
||
name = clean_cell(row.get("物件名称", ""))
|
||
if not item_id or not name:
|
||
continue
|
||
|
||
count_str = clean_cell(row.get("数量", "1"))
|
||
c_match = re.search(r'(\d+)', count_str)
|
||
count = int(c_match.group(1)) if c_match else 1
|
||
|
||
loc_str = clean_cell(row.get("放置位置 (绝对坐标 X,Y,Z)", row.get("放置位置", "")))
|
||
location = self._parse_xyz(loc_str)
|
||
|
||
bsp = clean_cell(row.get("BSP 资产建议", ""))
|
||
note = clean_cell(row.get("说明", ""))
|
||
|
||
self.data.global_objects.append(GlobalObject(
|
||
item_id=item_id, name=name, count=count,
|
||
location=location, bsp_suggestion=bsp, note=note
|
||
))
|
||
|
||
# ----------------------------------------------------------
|
||
# 五、场景边界与遮挡
|
||
# ----------------------------------------------------------
|
||
def _parse_boundaries(self, lines: list[str]):
|
||
"""解析场景边界表。"""
|
||
section_start = self._find_section(lines, "五、场景边界")
|
||
section_end = self._find_section(lines, "六、光照建议")
|
||
if section_start < 0:
|
||
return
|
||
if section_end < 0:
|
||
section_end = len(lines)
|
||
|
||
section_lines = lines[section_start:section_end]
|
||
tables = self._extract_tables(section_lines)
|
||
|
||
if tables:
|
||
for row in tables[0]:
|
||
segment = clean_cell(row.get("边界段", ""))
|
||
if not segment:
|
||
continue
|
||
loc_range = clean_cell(row.get("位置范围", ""))
|
||
method = clean_cell(row.get("遮挡方式", ""))
|
||
bsp = clean_cell(row.get("BSP 资产建议", ""))
|
||
tag = clean_cell(row.get("标识色", ""))
|
||
self.data.boundaries.append(BoundaryItem(
|
||
segment=segment, location_range=loc_range,
|
||
block_method=method, bsp_suggestion=bsp, tag_color=tag
|
||
))
|
||
|
||
# ----------------------------------------------------------
|
||
# 六、光照建议
|
||
# ----------------------------------------------------------
|
||
def _parse_lighting(self, lines: list[str]):
|
||
"""解析光照建议表。"""
|
||
section_start = self._find_section(lines, "六、光照建议")
|
||
if section_start < 0:
|
||
return
|
||
|
||
section_lines = lines[section_start:]
|
||
tables = self._extract_tables(section_lines)
|
||
|
||
if tables:
|
||
for row in tables[0]:
|
||
light_type = clean_cell(row.get("光源类型", ""))
|
||
if not light_type:
|
||
continue
|
||
count_str = clean_cell(row.get("数量", "1"))
|
||
c_match = re.search(r'(\d+)', count_str)
|
||
count = int(c_match.group(1)) if c_match else 1
|
||
position = clean_cell(row.get("位置/方向", ""))
|
||
params = clean_cell(row.get("参数建议", ""))
|
||
note = clean_cell(row.get("说明", ""))
|
||
self.data.lights.append(LightItem(
|
||
light_type=light_type, count=count,
|
||
position=position, params=params, note=note
|
||
))
|
||
|
||
# ----------------------------------------------------------
|
||
# 辅助方法
|
||
# ----------------------------------------------------------
|
||
def _find_section(self, lines: list[str], keyword: str) -> int:
|
||
"""找到包含关键词的章节标题行。"""
|
||
for i, line in enumerate(lines):
|
||
if keyword in line:
|
||
return i
|
||
return -1
|
||
|
||
def _find_zone(self, zone_id: str) -> Optional[ZoneInfo]:
|
||
"""查找区域信息。"""
|
||
for z in self.data.zones:
|
||
if z.zone_id == zone_id:
|
||
return z
|
||
return None
|
||
|
||
def _extract_tables(self, lines: list[str]) -> list[list[dict]]:
|
||
"""从行列表中提取所有表格。"""
|
||
tables = []
|
||
i = 0
|
||
while i < len(lines):
|
||
if '|' in lines[i]:
|
||
table_lines = []
|
||
while i < len(lines) and '|' in lines[i]:
|
||
table_lines.append(lines[i])
|
||
i += 1
|
||
rows = parse_table(table_lines)
|
||
if rows:
|
||
tables.append(rows)
|
||
continue
|
||
i += 1
|
||
return tables
|
||
|
||
def _skip_blank(self, lines: list[str], i: int) -> int:
|
||
"""跳过空行,返回第一个非空行的索引。"""
|
||
while i < len(lines) and not lines[i].strip():
|
||
i += 1
|
||
return i
|
||
|
||
def _read_table(self, lines: list[str], i: int) -> list[dict]:
|
||
"""从第 i 行开始读取一个 Markdown 表格。"""
|
||
table_lines = []
|
||
while i < len(lines) and '|' in lines[i]:
|
||
table_lines.append(lines[i])
|
||
i += 1
|
||
if not table_lines:
|
||
return []
|
||
return parse_table(table_lines)
|
||
|
||
def _advance_past_table(self, lines: list[str], i: int) -> int:
|
||
"""将索引推进到表格之后。"""
|
||
while i < len(lines) and '|' in lines[i]:
|
||
i += 1
|
||
return i
|
||
|
||
def _parse_xyz(self, text: str) -> tuple:
|
||
"""解析 (X,Y,Z) 或 (ΔX,ΔY,ΔZ) 格式。"""
|
||
if not text:
|
||
return (0, 0, 0)
|
||
m = re.search(r'\(([-\d.]+)\s*[,,]\s*([-\d.]+)\s*[,,]\s*([-\d.]+)\)', text)
|
||
if m:
|
||
return (float(m.group(1)), float(m.group(2)), float(m.group(3)))
|
||
return (0, 0, 0)
|
||
|
||
def _parse_size(self, text: str) -> tuple:
|
||
"""解析 尺寸格式:宽x深x高。支持 '800x20x250' 或 '800×20×250'。"""
|
||
if not text:
|
||
return (0, 0, 0)
|
||
m = re.search(r'(\d+(?:\.\d+)?)\s*[x×]\s*(\d+(?:\.\d+)?)\s*[x×]\s*(\d+(?:\.\d+)?)', text)
|
||
if m:
|
||
return (float(m.group(1)), float(m.group(2)), float(m.group(3)))
|
||
return (0, 0, 0)
|