feat: 完善 STEP 一级参数化编辑识别与关系式建模
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
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"
|
||||
)
|
||||
|
||||
|
||||
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}"
|
||||
|
||||
safe_expression = RELATION_REF_PATTERN.sub(replace_ref, 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:
|
||||
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 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 _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}"
|
||||
Reference in New Issue
Block a user