Skip to content

Commit aa20648

Browse files
authored
Merge pull request #4 from yoichironambu/main
【AL-1】コマンドIDの自動生成方式を設計・実装しました
2 parents 625b166 + db88e3f commit aa20648

4 files changed

Lines changed: 473 additions & 8 deletions

File tree

Designer/Data/CommandsConfig.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,6 @@
229229
<command id="model.slot"
230230
label="Slot"
231231
tooltip="Create slot features"
232-
<command id="model.chamfer"
233232
category="modeling"
234233
icon="icons/model/slot.svg"
235234
environments="part,sheet"/>
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
#Author: Yoichiro Nambu
4+
#Date: 2025.12.12
5+
"""
6+
Generate SolidDesignerCommands.h from CommandsConfig.xml
7+
8+
Usage:
9+
python generate_solid_designer_commands.py \
10+
--xml path/to/CommandsConfig.xml \
11+
--out path/to/SolidDesignerCommands.h
12+
"""
13+
14+
import argparse
15+
import sys
16+
import textwrap
17+
import xml.etree.ElementTree as ET
18+
from dataclasses import dataclass
19+
from pathlib import Path
20+
from typing import List, Dict, Set
21+
22+
print("[DEBUG] argv:", sys.argv)
23+
24+
@dataclass
25+
class CommandSpec:
26+
id: str
27+
label: str
28+
category: str
29+
30+
31+
def parse_commands(xml_path: Path) -> List[CommandSpec]:
32+
"""
33+
Parse CommandsConfig.xml and return a flat list of CommandSpec.
34+
"""
35+
try:
36+
tree = ET.parse(str(xml_path))
37+
except ET.ParseError as e:
38+
print(f"[ERROR] Failed to parse XML '{xml_path}': {e}", file=sys.stderr)
39+
sys.exit(1)
40+
41+
root = tree.getroot()
42+
43+
# 兼容两种写法:<CommandsConfig> 或 <commands>
44+
commands_nodes = []
45+
if root.tag.lower().endswith("commandsconfig"):
46+
commands_nodes = root.findall(".//commands/command")
47+
else:
48+
commands_nodes = root.findall(".//command")
49+
50+
specs: List[CommandSpec] = []
51+
52+
for elem in commands_nodes:
53+
cid = elem.get("id")
54+
if not cid:
55+
continue
56+
label = elem.get("label", "")
57+
category = elem.get("category", "")
58+
59+
specs.append(CommandSpec(id=cid.strip(),
60+
label=label.strip(),
61+
category=category.strip()))
62+
63+
if not specs:
64+
print(f"[WARN] No <command> elements found in '{xml_path}'", file=sys.stderr)
65+
66+
# 检查重复 id
67+
seen_ids: Set[str] = set()
68+
duplicates: Set[str] = set()
69+
for c in specs:
70+
if c.id in seen_ids:
71+
duplicates.add(c.id)
72+
seen_ids.add(c.id)
73+
if duplicates:
74+
print("[ERROR] Duplicate command id(s) detected:", file=sys.stderr)
75+
for d in sorted(duplicates):
76+
print(f" - {d}", file=sys.stderr)
77+
sys.exit(1)
78+
79+
# 排序:按 id 排一下,保证生成文件稳定
80+
specs.sort(key=lambda c: c.id)
81+
return specs
82+
83+
84+
def id_to_const_name(cmd_id: str) -> str:
85+
"""
86+
Convert 'file.save' -> 'FILE_SAVE'
87+
"""
88+
result_chars = []
89+
for ch in cmd_id:
90+
if ch.isalnum():
91+
result_chars.append(ch.upper())
92+
else:
93+
# '.' '-' ' ' 等全部归一成 '_'
94+
result_chars.append('_')
95+
name = "".join(result_chars)
96+
97+
# 去掉重复的 '_'(比如 "..")
98+
# 連続する '_' を除去(例:"..")
99+
while "__" in name:
100+
name = name.replace("__", "_")
101+
102+
# 去掉头尾 '_'
103+
# 先頭・末尾の '_' を削除
104+
name = name.strip("_")
105+
106+
# 如果第一个字符是数字,前面加前缀
107+
# 先頭が数字の場合は接頭辞を付与
108+
if name and name[0].isdigit():
109+
name = "CMD_" + name
110+
111+
if not name:
112+
raise ValueError(f"Cannot convert command id '{cmd_id}' to a valid C++ name")
113+
114+
return name
115+
116+
117+
def generate_header(specs: List[CommandSpec],
118+
out_path: Path,
119+
namespace_root: str = "sdr",
120+
namespace_leaf: str = "Cmd") -> None:
121+
"""
122+
Generate SolidDesignerCommands.h into out_path.
123+
"""
124+
# 检查常量名是否冲突
125+
# 定数名の衝突をチェック
126+
const_map: Dict[str, CommandSpec] = {}
127+
collisions: Dict[str, List[str]] = {}
128+
129+
for c in specs:
130+
const_name = id_to_const_name(c.id)
131+
if const_name in const_map:
132+
collisions.setdefault(const_name, []).append(c.id)
133+
else:
134+
const_map[const_name] = c
135+
136+
if collisions:
137+
print("[ERROR] Different command ids mapped to the same C++ name:", file=sys.stderr)
138+
for cname, ids in collisions.items():
139+
all_ids = [const_map[cname].id] + ids
140+
print(f" {cname}: {', '.join(all_ids)}", file=sys.stderr)
141+
sys.exit(1)
142+
143+
lines: List[str] = []
144+
145+
lines.extend(
146+
[
147+
"// -----------------------------------------------------------------------------",
148+
"// SolidDesignerCommands.h",
149+
"//",
150+
"// AUTO-GENERATED FILE. DO NOT EDIT BY HAND.",
151+
"//",
152+
"// Generated from: CommandsConfig.xml",
153+
"// -----------------------------------------------------------------------------",
154+
"",
155+
"#pragma once",
156+
"#include <string_view>",
157+
"",
158+
f"namespace {namespace_root}",
159+
"{",
160+
f" namespace {namespace_leaf}",
161+
" {",
162+
" // NOTE:",
163+
" // - Each constant is a std::string_view to the command id as defined",
164+
" // in CommandsConfig.xml.",
165+
" // - Use these constants in ICommand::Id() implementations, Ribbon/Menu",
166+
" // bindings, etc. Never hard-code string literals like \"file.save\".",
167+
"",
168+
]
169+
)
170+
171+
# 按 category 分组输出,便于阅读
172+
# category ごとにグループ化して出力(可読性向上)
173+
by_category: Dict[str, List[CommandSpec]] = {}
174+
for c in specs:
175+
cat = c.category or "uncategorized"
176+
by_category.setdefault(cat, []).append(c)
177+
178+
for cat in sorted(by_category.keys()):
179+
lines.append(f" // === Category: {cat} ===")
180+
for c in by_category[cat]:
181+
const_name = id_to_const_name(c.id)
182+
183+
# label 作为注释
184+
# label をコメントとして付与
185+
label_comment = f" // {c.label}" if c.label else ""
186+
lines.append(
187+
f' inline constexpr std::string_view {const_name:<30} = "{c.id}";{label_comment}'
188+
)
189+
lines.append("")
190+
191+
lines.extend(
192+
[
193+
" }} // namespace {0}".format(namespace_leaf),
194+
"}} // namespace {0}".format(namespace_root),
195+
"",
196+
]
197+
)
198+
199+
out_path.write_text("\n".join(lines), encoding="utf-8")
200+
print(f"[INFO] Generated {out_path} ({len(specs)} commands).")
201+
202+
203+
def main(argv: List[str]) -> int:
204+
parser = argparse.ArgumentParser(
205+
description="Generate SolidDesignerCommands.h from CommandsConfig.xml"
206+
)
207+
parser.add_argument("--xml", required=True, help="Path to CommandsConfig.xml")
208+
parser.add_argument("--out", required=True, help="Path to SolidDesignerCommands.h")
209+
parser.add_argument(
210+
"--namespace-root", default="sdr", help="Root namespace (default: sdr)"
211+
)
212+
parser.add_argument(
213+
"--namespace-leaf", default="Cmd", help="Leaf namespace (default: Cmd)"
214+
)
215+
216+
args = parser.parse_args(argv)
217+
218+
xml_path = Path(args.xml).resolve()
219+
out_path = Path(args.out).resolve()
220+
221+
if not xml_path.is_file():
222+
print(f"[ERROR] XML file not found: {xml_path}", file=sys.stderr)
223+
return 1
224+
225+
specs = parse_commands(xml_path)
226+
generate_header(
227+
specs,
228+
out_path,
229+
namespace_root=args.namespace_root,
230+
namespace_leaf=args.namespace_leaf,
231+
)
232+
return 0
233+
234+
235+
if __name__ == "__main__":
236+
sys.exit(main(sys.argv[1:]))

Designer/UI/SolidDesignerCommand/CMakeLists.txt

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ message("*--*--*--*--*--*--*--*--*--*--*--*--*--Start SolidDesignerCommand--*--*
88
add_definitions(-DSOLID_DESIGNER_UI_HOME)
99
add_definitions(-DSOLID_DESIGNER_COMMAND_HOME)
1010

11+
message("CMAKE_SOURCE_DIR = ${CMAKE_SOURCE_DIR}")
12+
# 先指定你项目内置的 Python 解释器
13+
set(_SD_PY_ROOT "${CMAKE_SOURCE_DIR}/ToolChain/Python310")
14+
set(Python3_EXECUTABLE "${_SD_PY_ROOT}/python.exe" CACHE FILEPATH "SolidDesigner bundled Python" FORCE)
15+
16+
# 然后再找 Python3
17+
find_package(Python3 REQUIRED COMPONENTS Interpreter)
18+
19+
message(STATUS "Using Python3_EXECUTABLE = ${Python3_EXECUTABLE}")
20+
1121
message("${PROJECT_SOURCE_DIR}")
1222

1323
set(ABS_SDC "${PROJECT_SOURCE_DIR}/Designer/UI/SolidDesignerCommand")
@@ -99,14 +109,15 @@ endif()
99109
#-----------------------------------------------------------------------------
100110
# Automatically generate SolidDesignerCommands.h
101111
#-----------------------------------------------------------------------------
102-
set(COMMANDS_XML ${CMAKE_SOURCE_DIR}/config/CommandsConfig.xml)
103-
set(COMMANDS_GEN_DIR ${CMAKE_BINARY_DIR}/generated/$<CONFIG>)
104-
set(COMMANDS_HDR ${COMMANDS_GEN_DIR}/SolidDesignerCommands.h)
105-
set(COMMANDS_PY ${CMAKE_SOURCE_DIR}/tools/generate_solid_designer_commands.py)
112+
set(COMMANDS_XML ${PROJECT_SOURCE_DIR}/Designer/Data/CommandsConfig.xml)
113+
set(COMMANDS_HDR ${PROJECT_SOURCE_DIR}/Designer/UI/SolidDesignerCommand/SolidDesignerCommands.h)
114+
set(COMMANDS_PY ${CMAKE_SOURCE_DIR}/Designer/Data/Scripts/generate_solid_designer_commands.py)
115+
116+
message(STATUS "COMMANDS_XML = ${COMMANDS_XML}")
117+
message(STATUS "COMMANDS_PY = ${COMMANDS_PY}")
106118

107119
add_custom_command(
108120
OUTPUT ${COMMANDS_HDR}
109-
COMMAND ${CMAKE_COMMAND} -E make_directory ${COMMANDS_GEN_DIR}
110121
COMMAND ${Python3_EXECUTABLE} ${COMMANDS_PY}
111122
--xml ${COMMANDS_XML}
112123
--out ${COMMANDS_HDR}
@@ -116,8 +127,6 @@ add_custom_command(
116127
)
117128

118129
add_custom_target(GenerateSolidDesignerCommands DEPENDS ${COMMANDS_HDR})
119-
120-
target_include_directories(SolidDesignerCommand PRIVATE ${COMMANDS_GEN_DIR})
121130
add_dependencies(SolidDesignerCommand GenerateSolidDesignerCommands)
122131

123132
# 可选:让 IDE 看到生成文件

0 commit comments

Comments
 (0)