feat: 完善 SCDM-first 参数化编辑交付版

This commit is contained in:
2026-08-20 17:12:01 +08:00
parent 4e7877e05c
commit b4feab24d2
21 changed files with 3015 additions and 1729 deletions
+31
View File
@@ -10,6 +10,18 @@ from typing import Callable, Iterable
RELATION_REF_PATTERN = re.compile(
r"\b(?P<kind>Face|Edge)(?P<object_id>\d+)\.(?P<parameter>[A-Za-z0-9_\u4e00-\u9fff]+)\b"
)
RELATION_UNIT_LITERAL_PATTERN = re.compile(
r"(?<![A-Za-z0-9_.])(?P<number>(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)\s*(?P<unit>mm|毫米|cm|厘米|m|米)\b",
re.IGNORECASE,
)
RELATION_UNIT_MULTIPLIERS = {
"mm": 1.0,
"毫米": 1.0,
"cm": 10.0,
"厘米": 10.0,
"m": 1000.0,
"": 1000.0,
}
class RelationFormulaError(ValueError):
@@ -112,7 +124,10 @@ def parse_relation_formula(text: str) -> RelationFormula:
references.append(_ref_from_match(match))
return f"__ref{len(references) - 1}"
# 用户输入的 Face85.直径 不能直接丢给 eval;先替换成内部占位符,
# 后面只允许这些占位符和白名单 AST 节点参与计算。
safe_expression = RELATION_REF_PATTERN.sub(replace_ref, expression)
safe_expression = _replace_unit_literals(safe_expression)
try:
tree = ast.parse(safe_expression, mode="eval")
except SyntaxError as exc:
@@ -136,6 +151,8 @@ def evaluate_relation_formula(
namespace[f"__ref{index}"] = _coerce_formula_value(value_resolver(ref))
code = compile(formula.safe_expression, "<relation-formula>", "eval")
try:
# 这里仍然使用 Python 表达式能力,但 builtins 为空,AST 也已校验过。
# 关系式只承担参数求值,不允许调用函数、访问属性或执行任意代码。
value = eval(code, {"__builtins__": {}}, namespace)
except ZeroDivisionError as exc:
raise RelationFormulaError("关系式中出现除以 0。") from exc
@@ -158,6 +175,8 @@ def validate_relation_formula_graph(formulas: Iterable[RelationFormula]) -> None
reference_tokens = [ref.token for ref in formula.references]
if target_token in reference_tokens:
raise RelationFormulaError(f"关系式不能引用自身:{target_token}")
# 只把“由其它公式控制的参数”纳入依赖图;普通测量值由模型/cache 提供,
# 不参与循环依赖判断。
graph[target_token] = [ref_token for ref_token in reference_tokens if ref_token in target_tokens]
visit_state: dict[str, str] = {}
@@ -213,6 +232,18 @@ def _ref_from_match(match: re.Match[str]) -> ObjectParameterRef:
)
def _replace_unit_literals(expression: str) -> str:
def replace(match: re.Match[str]) -> str:
number_text = str(match.group("number"))
unit = str(match.group("unit"))
multiplier = RELATION_UNIT_MULTIPLIERS.get(unit) or RELATION_UNIT_MULTIPLIERS.get(unit.lower())
if multiplier is None:
raise RelationFormulaError(f"不支持的单位:{unit}")
return f"({number_text}*{multiplier:.12g})"
return RELATION_UNIT_LITERAL_PATTERN.sub(replace, expression)
def _validate_expression_tree(tree: ast.AST, ref_count: int) -> None:
allowed = (
ast.Expression,