Files
pythonocc-step-editor/step_editor/relation_formulas.py
T

314 lines
11 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
import ast
import math
import re
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):
pass
@dataclass(frozen=True)
class ObjectParameterRef:
kind: str
object_id: int
parameter: str
@property
def token(self) -> str:
return f"{self.kind}{self.object_id}.{self.parameter}"
@dataclass(frozen=True)
class RelationFormula:
text: str
target: ObjectParameterRef
expression: str
safe_expression: str
references: tuple[ObjectParameterRef, ...]
class Vector3:
__slots__ = ("values",)
def __init__(self, values: Iterable[object]) -> None:
items = tuple(values)
if len(items) != 3:
raise TypeError("Vector expression must contain exactly 3 values.")
try:
self.values = (float(items[0]), float(items[1]), float(items[2]))
except (TypeError, ValueError) as exc:
raise TypeError("Vector expression values must be numbers.") from exc
def __iter__(self):
return iter(self.values)
def __len__(self) -> int:
return 3
def __getitem__(self, index: int) -> float:
return self.values[index]
def __repr__(self) -> str:
return f"Vector3({self.values!r})"
def __add__(self, other: object) -> "Vector3":
right = _coerce_vector(other)
return Vector3((self.values[0] + right[0], self.values[1] + right[1], self.values[2] + right[2]))
def __radd__(self, other: object) -> "Vector3":
return self.__add__(other)
def __sub__(self, other: object) -> "Vector3":
right = _coerce_vector(other)
return Vector3((self.values[0] - right[0], self.values[1] - right[1], self.values[2] - right[2]))
def __rsub__(self, other: object) -> "Vector3":
left = _coerce_vector(other)
return Vector3((left[0] - self.values[0], left[1] - self.values[1], left[2] - self.values[2]))
def __mul__(self, other: object) -> "Vector3":
scalar = _coerce_number(other)
return Vector3((self.values[0] * scalar, self.values[1] * scalar, self.values[2] * scalar))
def __rmul__(self, other: object) -> "Vector3":
return self.__mul__(other)
def __truediv__(self, other: object) -> "Vector3":
scalar = _coerce_number(other)
if abs(scalar) <= 1e-15:
raise ZeroDivisionError("Vector division by zero.")
return Vector3((self.values[0] / scalar, self.values[1] / scalar, self.values[2] / scalar))
def __neg__(self) -> "Vector3":
return Vector3((-self.values[0], -self.values[1], -self.values[2]))
def parse_relation_formula(text: str) -> RelationFormula:
normalized = " ".join(str(text or "").strip().split())
if not normalized:
raise RelationFormulaError("请输入关系式。")
if normalized.count("=") != 1:
raise RelationFormulaError("关系式必须且只能包含一个等号,例如 Face87.直径 = Face85.直径。")
left, expression = (part.strip() for part in normalized.split("=", 1))
if not left or not expression:
raise RelationFormulaError("关系式左侧和右侧都不能为空。")
target_match = RELATION_REF_PATTERN.fullmatch(left)
if target_match is None:
raise RelationFormulaError("关系式左侧必须是 FaceID.参数 或 EdgeID.参数,例如 Face87.直径。")
target = _ref_from_match(target_match)
references: list[ObjectParameterRef] = []
def replace_ref(match: re.Match[str]) -> str:
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:
raise RelationFormulaError(f"关系式右侧语法错误:{exc.msg}") from exc
_validate_expression_tree(tree, len(references))
return RelationFormula(
text=f"{target.token} = {expression}",
target=target,
expression=expression,
safe_expression=safe_expression,
references=tuple(references),
)
def evaluate_relation_formula(
formula: RelationFormula,
value_resolver: Callable[[ObjectParameterRef], object],
) -> float | Vector3:
namespace: dict[str, object] = {}
for index, ref in enumerate(formula.references):
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
except Exception as exc:
raise RelationFormulaError(f"关系式计算失败:{exc}") from exc
return _coerce_formula_value(value)
def validate_relation_formula_graph(formulas: Iterable[RelationFormula]) -> None:
target_to_formula: dict[str, RelationFormula] = {}
for formula in formulas:
target_token = formula.target.token
if target_token in target_to_formula:
raise RelationFormulaError(f"同一目标参数只能由一条关系式控制:{target_token}。")
target_to_formula[target_token] = formula
target_tokens = set(target_to_formula)
graph: dict[str, list[str]] = {}
for target_token, formula in target_to_formula.items():
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] = {}
stack: list[str] = []
def visit(token: str) -> None:
state = visit_state.get(token)
if state == "visiting":
start_index = stack.index(token) if token in stack else 0
cycle = [*stack[start_index:], token]
raise RelationFormulaError(f"关系式存在循环依赖:{' -> '.join(cycle)}。")
if state == "visited":
return
visit_state[token] = "visiting"
stack.append(token)
for dependency in graph.get(token, []):
visit(dependency)
stack.pop()
visit_state[token] = "visited"
for target_token in graph:
visit(target_token)
def relation_value_to_text(value: object) -> str:
value = _coerce_formula_value(value)
if isinstance(value, Vector3):
return ", ".join(_format_number(item) for item in value.values)
return _format_number(float(value))
def rewrite_relation_formula_ids(text: str, face_id_map: dict[int, int], edge_id_map: dict[int, int] | None = None) -> str:
edge_id_map = dict(edge_id_map or {})
def replace(match: re.Match[str]) -> str:
kind = str(match.group("kind"))
object_id = int(match.group("object_id"))
parameter = str(match.group("parameter"))
if kind == "Face" and object_id in face_id_map:
object_id = int(face_id_map[object_id])
elif kind == "Edge" and object_id in edge_id_map:
object_id = int(edge_id_map[object_id])
return f"{kind}{object_id}.{parameter}"
return RELATION_REF_PATTERN.sub(replace, text)
def _ref_from_match(match: re.Match[str]) -> ObjectParameterRef:
return ObjectParameterRef(
kind=str(match.group("kind")),
object_id=int(match.group("object_id")),
parameter=str(match.group("parameter")),
)
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,
ast.BinOp,
ast.UnaryOp,
ast.Name,
ast.Load,
ast.Constant,
ast.Tuple,
ast.Add,
ast.Sub,
ast.Mult,
ast.Div,
ast.UAdd,
ast.USub,
)
for node in ast.walk(tree):
if not isinstance(node, allowed):
raise RelationFormulaError("关系式只支持数字、对象参数、括号、向量和 + - * / 运算。")
if isinstance(node, ast.Name):
if not re.fullmatch(r"__ref\d+", node.id):
raise RelationFormulaError(f"未知参数引用:{node.id}")
index = int(node.id.replace("__ref", ""))
if index < 0 or index >= ref_count:
raise RelationFormulaError(f"未知参数引用:{node.id}")
elif isinstance(node, ast.Constant):
if not isinstance(node.value, (int, float)):
raise RelationFormulaError("关系式常量只支持数字。")
if isinstance(node.value, float) and not math.isfinite(node.value):
raise RelationFormulaError("关系式数字不能是 NaN 或无穷大。")
elif isinstance(node, ast.Tuple):
if len(node.elts) != 3:
raise RelationFormulaError("向量必须是 3 个数字,例如 (0, 0, -3.5)。")
def _coerce_formula_value(value: object) -> float | Vector3:
if isinstance(value, Vector3):
return value
if isinstance(value, (tuple, list)):
return Vector3(value)
return _coerce_number(value)
def _coerce_vector(value: object) -> tuple[float, float, float]:
if isinstance(value, Vector3):
return value.values
if isinstance(value, (tuple, list)):
return Vector3(value).values
raise TypeError("Vector operation requires another 3D vector.")
def _coerce_number(value: object) -> float:
if isinstance(value, bool):
raise TypeError("Boolean is not a valid numeric formula value.")
if isinstance(value, (int, float)):
number = float(value)
else:
raise TypeError(f"{value!r} is not a valid numeric formula value.")
if not math.isfinite(number):
raise TypeError("Formula value must be finite.")
return number
def _format_number(value: float) -> str:
if abs(value) < 5e-13:
value = 0.0
return f"{value:.12g}"