2026-08-19 10:28:09 +08:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
from collections.abc import Iterable, Mapping
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def property_specs_from_scdm_cache(
|
|
|
|
|
|
cache: Mapping[str, object],
|
|
|
|
|
|
*,
|
|
|
|
|
|
selected_face_ids: Iterable[int] = (),
|
|
|
|
|
|
selected_edge_ids: Iterable[int] = (),
|
|
|
|
|
|
execution_ready: bool | Iterable[str] = False,
|
|
|
|
|
|
) -> list[dict[str, object]]:
|
|
|
|
|
|
face_ids = {int(item) for item in selected_face_ids}
|
|
|
|
|
|
edge_ids = {int(item) for item in selected_edge_ids}
|
|
|
|
|
|
if not face_ids and not edge_ids:
|
|
|
|
|
|
return []
|
|
|
|
|
|
objects = cache.get("objects")
|
|
|
|
|
|
if not isinstance(objects, list):
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
specs: list[dict[str, object]] = []
|
|
|
|
|
|
for item in objects:
|
|
|
|
|
|
if not isinstance(item, Mapping) or not _object_matches(item, face_ids=face_ids, edge_ids=edge_ids):
|
|
|
|
|
|
continue
|
|
|
|
|
|
capabilities = item.get("capabilities")
|
|
|
|
|
|
if not isinstance(capabilities, list):
|
|
|
|
|
|
continue
|
|
|
|
|
|
for capability in capabilities:
|
|
|
|
|
|
if isinstance(capability, Mapping):
|
2026-08-19 18:02:47 +08:00
|
|
|
|
if str(capability.get("key") or "") == "pattern.segment_spacing":
|
|
|
|
|
|
continue
|
2026-08-19 10:28:09 +08:00
|
|
|
|
spec = _capability_spec(item, capability, execution_ready=execution_ready)
|
|
|
|
|
|
if spec is not None:
|
|
|
|
|
|
specs.append(spec)
|
2026-08-19 18:02:47 +08:00
|
|
|
|
specs.extend(_pattern_segment_spacing_specs(item, execution_ready=execution_ready))
|
2026-08-19 10:28:09 +08:00
|
|
|
|
return specs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _object_matches(raw_object: Mapping[str, object], *, face_ids: set[int], edge_ids: set[int]) -> bool:
|
|
|
|
|
|
signature = raw_object.get("geometrySignature")
|
|
|
|
|
|
if not isinstance(signature, Mapping):
|
|
|
|
|
|
return False
|
|
|
|
|
|
object_faces = set(_int_values(signature.get("faceIds")))
|
2026-08-19 18:02:47 +08:00
|
|
|
|
object_faces.update(_int_values(signature.get("supportFaceIds")))
|
2026-08-19 10:28:09 +08:00
|
|
|
|
object_edges = set(_int_values(signature.get("edgeIds")))
|
|
|
|
|
|
return bool((face_ids and object_faces & face_ids) or (edge_ids and object_edges & edge_ids))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _capability_spec(
|
|
|
|
|
|
raw_object: Mapping[str, object],
|
|
|
|
|
|
capability: Mapping[str, object],
|
|
|
|
|
|
*,
|
|
|
|
|
|
execution_ready: bool | Iterable[str],
|
|
|
|
|
|
) -> dict[str, object] | None:
|
|
|
|
|
|
key = str(capability.get("key") or "").strip()
|
|
|
|
|
|
label = str(capability.get("displayName") or key).strip()
|
|
|
|
|
|
if not key or not label:
|
|
|
|
|
|
return None
|
|
|
|
|
|
value_kind = str(capability.get("valueKind") or "number")
|
|
|
|
|
|
current = capability.get("currentValue")
|
|
|
|
|
|
value_type = _value_type(value_kind, key)
|
2026-08-19 18:02:47 +08:00
|
|
|
|
signature = raw_object.get("geometrySignature") if isinstance(raw_object.get("geometrySignature"), Mapping) else {}
|
|
|
|
|
|
unit_scale = _unit_scale(signature if isinstance(signature, Mapping) else {})
|
|
|
|
|
|
current_display = _display_value(current, key=key, value_type=value_type, unit_scale=unit_scale)
|
2026-08-19 10:28:09 +08:00
|
|
|
|
command_value = value_type == "command"
|
2026-08-19 18:02:47 +08:00
|
|
|
|
current_text = "可执行" if command_value else _format_value(current_display, value_type=value_type)
|
|
|
|
|
|
target_text = "执行" if command_value else _format_value(current_display, value_type=value_type)
|
2026-08-19 10:28:09 +08:00
|
|
|
|
capability_block = str(capability.get("blockReason") or "").strip()
|
|
|
|
|
|
object_block = str(raw_object.get("blockReason") or "").strip()
|
|
|
|
|
|
block_reason = capability_block or object_block
|
2026-08-19 18:02:47 +08:00
|
|
|
|
if not command_value and not _current_value_available(current_display, value_type=value_type):
|
|
|
|
|
|
block_reason = block_reason or f"SCDM 已识别“{label}”,但没有返回可用于编辑的当前值。"
|
2026-08-19 10:28:09 +08:00
|
|
|
|
backend_operation = str(capability.get("backendOperation") or "")
|
|
|
|
|
|
post_check = str(capability.get("postCheck") or "")
|
2026-08-19 18:02:47 +08:00
|
|
|
|
max_value = _display_max_value(key=key, signature=signature if isinstance(signature, Mapping) else {}, unit_scale=unit_scale)
|
|
|
|
|
|
range_hint = "来源:SCDM 结构化识别结果。执行前仍需生成 edit job,并在结果 STEP 上做 OCCT 校验和目标值回测。"
|
|
|
|
|
|
if key == "pattern.spacing" and max_value is not None:
|
|
|
|
|
|
range_hint = f"该阵列受承载面范围限制,保持阵列中心不变时最大间距约 {max_value:g};超过后会跑出承载面。"
|
2026-08-19 10:28:09 +08:00
|
|
|
|
can_execute = bool(_capability_execution_ready(key, execution_ready) and capability.get("editable", True) and not block_reason)
|
|
|
|
|
|
if can_execute:
|
|
|
|
|
|
disabled_tip = ""
|
|
|
|
|
|
enabled_tip = (
|
|
|
|
|
|
f"SCDM 已识别“{label}”可由 {backend_operation or '后端命令'} 修改;"
|
|
|
|
|
|
f"执行后会用 {post_check or '结果回测'} 校验。"
|
|
|
|
|
|
)
|
|
|
|
|
|
elif block_reason:
|
|
|
|
|
|
enabled_tip = ""
|
|
|
|
|
|
disabled_tip = f"SCDM 已识别该对象,但当前能力被阻止:{block_reason}"
|
|
|
|
|
|
else:
|
|
|
|
|
|
enabled_tip = ""
|
|
|
|
|
|
disabled_tip = "SCDM 已识别该参数,但 S5 修改执行器还没有接入;当前只作为后端识别结果缓存,不开放执行。"
|
|
|
|
|
|
return {
|
|
|
|
|
|
"key": f"scdm:{key}",
|
|
|
|
|
|
"label": label,
|
2026-08-19 18:02:47 +08:00
|
|
|
|
"current_raw": current_display if current_display is not None else "",
|
|
|
|
|
|
"scdm_current_raw": current if current is not None else "",
|
|
|
|
|
|
"scdm_unit_scale": unit_scale,
|
2026-08-19 10:28:09 +08:00
|
|
|
|
"current_text": current_text,
|
|
|
|
|
|
"target_text": target_text,
|
|
|
|
|
|
"editable": True,
|
|
|
|
|
|
"enabled": can_execute,
|
|
|
|
|
|
"status_text": "可修改" if can_execute else "暂未接入",
|
|
|
|
|
|
"scope_text": str(capability.get("defaultIntent") or "SCDM"),
|
|
|
|
|
|
"action": "apply_scdm_property_edit",
|
|
|
|
|
|
"value_type": value_type,
|
|
|
|
|
|
"enabled_tip": enabled_tip,
|
|
|
|
|
|
"disabled_tip": disabled_tip,
|
2026-08-19 18:02:47 +08:00
|
|
|
|
"range_hint": range_hint,
|
2026-08-19 10:28:09 +08:00
|
|
|
|
"min_value": 0.0 if value_type == "positive" else None,
|
|
|
|
|
|
"min_exclusive": True if value_type == "positive" else False,
|
2026-08-19 18:02:47 +08:00
|
|
|
|
"max_value": max_value,
|
2026-08-19 10:28:09 +08:00
|
|
|
|
"scdm_object_id": raw_object.get("objectId"),
|
|
|
|
|
|
"scdm_source_backend_id": raw_object.get("sourceBackendId"),
|
|
|
|
|
|
"scdm_capability_key": key,
|
|
|
|
|
|
"scdm_backend_operation": backend_operation,
|
|
|
|
|
|
"scdm_post_check": post_check,
|
2026-08-19 18:02:47 +08:00
|
|
|
|
"scdm_geometry_signature": signature if isinstance(signature, Mapping) else {},
|
2026-08-19 10:28:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-19 18:02:47 +08:00
|
|
|
|
def _unit_scale(signature: Mapping[str, object]) -> float:
|
|
|
|
|
|
try:
|
|
|
|
|
|
value = float(str(signature.get("localUnitScale")).strip())
|
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
|
return 1.0
|
|
|
|
|
|
return value if value > 0 else 1.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _display_value(value: object, *, key: str, value_type: str, unit_scale: float) -> object:
|
|
|
|
|
|
if unit_scale <= 0 or abs(unit_scale - 1.0) <= 1.0e-12 or not _uses_length_units(key, value_type):
|
|
|
|
|
|
return value
|
|
|
|
|
|
if value_type == "vector3":
|
|
|
|
|
|
values = _float_values(value)
|
|
|
|
|
|
if len(values) == 3:
|
|
|
|
|
|
return [item / unit_scale for item in values]
|
|
|
|
|
|
return value
|
|
|
|
|
|
try:
|
|
|
|
|
|
return float(str(value).strip()) / unit_scale
|
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _uses_length_units(key: str, value_type: str) -> bool:
|
|
|
|
|
|
if value_type == "vector3":
|
|
|
|
|
|
return True
|
|
|
|
|
|
suffixes = (
|
|
|
|
|
|
".diameter",
|
|
|
|
|
|
".radius",
|
|
|
|
|
|
".offset",
|
|
|
|
|
|
".width",
|
|
|
|
|
|
".depth",
|
|
|
|
|
|
".height",
|
|
|
|
|
|
".distance",
|
|
|
|
|
|
".thickness",
|
|
|
|
|
|
".spacing",
|
|
|
|
|
|
".segment_spacing",
|
|
|
|
|
|
".position",
|
|
|
|
|
|
)
|
|
|
|
|
|
return key.endswith(suffixes)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _display_max_value(*, key: str, signature: Mapping[str, object], unit_scale: float) -> float | None:
|
|
|
|
|
|
if key != "pattern.spacing":
|
|
|
|
|
|
return None
|
|
|
|
|
|
fit = signature.get("supportPatternFit")
|
|
|
|
|
|
if not isinstance(fit, Mapping):
|
|
|
|
|
|
return None
|
|
|
|
|
|
value = fit.get("maxSpacingLocal")
|
|
|
|
|
|
try:
|
|
|
|
|
|
result = float(str(value).strip())
|
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
|
backend_value = fit.get("maxSpacing")
|
|
|
|
|
|
try:
|
|
|
|
|
|
return float(str(backend_value).strip()) / unit_scale if unit_scale > 0 else None
|
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
|
return None
|
|
|
|
|
|
return result if result > 0 else None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _pattern_segment_spacing_specs(
|
|
|
|
|
|
raw_object: Mapping[str, object],
|
|
|
|
|
|
*,
|
|
|
|
|
|
execution_ready: bool | Iterable[str],
|
|
|
|
|
|
) -> list[dict[str, object]]:
|
|
|
|
|
|
if str(raw_object.get("objectType") or "").strip().lower() != "linear_pattern":
|
|
|
|
|
|
return []
|
|
|
|
|
|
signature = raw_object.get("geometrySignature")
|
|
|
|
|
|
if not isinstance(signature, Mapping):
|
|
|
|
|
|
return []
|
|
|
|
|
|
axis = _unit_vector(_float_values(signature.get("axis")))
|
|
|
|
|
|
if len(axis) != 3:
|
|
|
|
|
|
return []
|
|
|
|
|
|
instances = _sorted_pattern_instances(signature, axis)
|
|
|
|
|
|
if len(instances) < 2:
|
|
|
|
|
|
return []
|
|
|
|
|
|
unit_scale = _unit_scale(signature)
|
|
|
|
|
|
can_execute = bool(_capability_execution_ready("pattern.segment_spacing", execution_ready) and not str(raw_object.get("blockReason") or "").strip())
|
|
|
|
|
|
specs: list[dict[str, object]] = []
|
|
|
|
|
|
for segment_index in range(len(instances) - 1):
|
|
|
|
|
|
left = instances[segment_index]
|
|
|
|
|
|
right = instances[segment_index + 1]
|
|
|
|
|
|
left_label = _segment_instance_label(left, segment_index + 1)
|
|
|
|
|
|
right_label = _segment_instance_label(right, segment_index + 2)
|
|
|
|
|
|
segment_label = f"{left_label}-{right_label}间距"
|
|
|
|
|
|
current = max(0.0, float(right["projection"]) - float(left["projection"]))
|
|
|
|
|
|
if current <= 0:
|
|
|
|
|
|
continue
|
|
|
|
|
|
current_display = current / unit_scale if unit_scale > 0 else current
|
|
|
|
|
|
scope_modes = _segment_scope_modes(
|
|
|
|
|
|
signature,
|
|
|
|
|
|
instances,
|
|
|
|
|
|
segment_index,
|
|
|
|
|
|
current,
|
|
|
|
|
|
current_display,
|
|
|
|
|
|
unit_scale,
|
|
|
|
|
|
left_label=left_label,
|
|
|
|
|
|
right_label=right_label,
|
|
|
|
|
|
segment_label=segment_label,
|
|
|
|
|
|
can_execute=can_execute,
|
|
|
|
|
|
)
|
|
|
|
|
|
default_mode = scope_modes.get("fix_left_move_right", {}) if isinstance(scope_modes, Mapping) else {}
|
|
|
|
|
|
max_display = default_mode.get("max_value")
|
|
|
|
|
|
range_hint = str(default_mode.get("range_hint") or "")
|
|
|
|
|
|
enabled_tip = str(default_mode.get("enabled_tip") or range_hint)
|
|
|
|
|
|
segment_signature = default_mode.get("scdm_geometry_signature")
|
|
|
|
|
|
if not isinstance(segment_signature, Mapping):
|
|
|
|
|
|
segment_signature = _segment_signature(
|
|
|
|
|
|
signature,
|
|
|
|
|
|
segment_index,
|
|
|
|
|
|
current,
|
|
|
|
|
|
unit_scale,
|
|
|
|
|
|
left_label=left_label,
|
|
|
|
|
|
right_label=right_label,
|
|
|
|
|
|
moving_side="after",
|
|
|
|
|
|
motion_semantics="fix_left_move_right_group",
|
|
|
|
|
|
)
|
|
|
|
|
|
specs.append(
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": f"scdm:pattern.segment_spacing:{segment_index}",
|
|
|
|
|
|
"label": segment_label,
|
|
|
|
|
|
"current_raw": current_display,
|
|
|
|
|
|
"scdm_current_raw": current,
|
|
|
|
|
|
"scdm_unit_scale": unit_scale,
|
|
|
|
|
|
"current_text": _format_value(current_display, value_type="positive"),
|
|
|
|
|
|
"target_text": _format_value(current_display, value_type="positive"),
|
|
|
|
|
|
"editable": True,
|
|
|
|
|
|
"enabled": can_execute,
|
|
|
|
|
|
"status_text": "可修改" if can_execute else "暂未接入",
|
|
|
|
|
|
"scope_text": "固定前项,移动后侧",
|
|
|
|
|
|
"scope_modes": scope_modes,
|
|
|
|
|
|
"scope_default": "fix_left_move_right",
|
|
|
|
|
|
"action": "apply_scdm_property_edit",
|
|
|
|
|
|
"value_type": "positive",
|
|
|
|
|
|
"enabled_tip": enabled_tip,
|
|
|
|
|
|
"disabled_tip": "" if can_execute else "SCDM 已识别该局部间距,但当前修改执行器尚未开放。",
|
|
|
|
|
|
"range_hint": range_hint,
|
|
|
|
|
|
"min_value": 0.0,
|
|
|
|
|
|
"min_exclusive": True,
|
|
|
|
|
|
"max_value": max_display,
|
|
|
|
|
|
"scdm_object_id": raw_object.get("objectId"),
|
|
|
|
|
|
"scdm_source_backend_id": raw_object.get("sourceBackendId"),
|
|
|
|
|
|
"scdm_capability_key": "pattern.segment_spacing",
|
|
|
|
|
|
"scdm_backend_operation": "change_pattern_segment_spacing",
|
|
|
|
|
|
"scdm_post_check": "target_pattern_segment_spacing",
|
|
|
|
|
|
"scdm_geometry_signature": segment_signature,
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
return specs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _sorted_pattern_instances(signature: Mapping[str, object], axis: list[float]) -> list[dict[str, object]]:
|
|
|
|
|
|
value = signature.get("patternInstances")
|
|
|
|
|
|
if not isinstance(value, (list, tuple)):
|
|
|
|
|
|
return []
|
|
|
|
|
|
result: list[dict[str, object]] = []
|
|
|
|
|
|
for index, item in enumerate(value):
|
|
|
|
|
|
if not isinstance(item, Mapping):
|
|
|
|
|
|
continue
|
|
|
|
|
|
center = _float_values(item.get("center") or item.get("instanceCenter"))
|
|
|
|
|
|
if len(center) != 3:
|
|
|
|
|
|
continue
|
|
|
|
|
|
result.append(
|
|
|
|
|
|
{
|
|
|
|
|
|
"index": index,
|
|
|
|
|
|
"source": item,
|
|
|
|
|
|
"center": center,
|
|
|
|
|
|
"projection": _point_projection(center, axis),
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
result.sort(key=lambda item: float(item["projection"]))
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _segment_max_spacing_display(
|
|
|
|
|
|
signature: Mapping[str, object],
|
|
|
|
|
|
instances: list[dict[str, object]],
|
|
|
|
|
|
segment_index: int,
|
|
|
|
|
|
current_display: float,
|
|
|
|
|
|
unit_scale: float,
|
|
|
|
|
|
*,
|
|
|
|
|
|
moving_side: str = "after",
|
|
|
|
|
|
) -> float | None:
|
|
|
|
|
|
fit = signature.get("supportPatternFit")
|
|
|
|
|
|
if not isinstance(fit, Mapping):
|
|
|
|
|
|
return None
|
|
|
|
|
|
projection_min = _float_or_none(fit.get("supportProjectionMinLocal"))
|
|
|
|
|
|
projection_max = _float_or_none(fit.get("supportProjectionMaxLocal"))
|
|
|
|
|
|
member_span = _float_or_none(fit.get("memberSpanLocal"))
|
|
|
|
|
|
if projection_min is None or projection_max is None or member_span is None or member_span <= 0:
|
|
|
|
|
|
return None
|
|
|
|
|
|
first_projection = float(instances[0]["projection"])
|
|
|
|
|
|
last_projection = float(instances[-1]["projection"])
|
|
|
|
|
|
first_projection_display = first_projection / unit_scale if unit_scale > 0 else first_projection
|
|
|
|
|
|
last_projection_display = last_projection / unit_scale if unit_scale > 0 else last_projection
|
|
|
|
|
|
backward_capacity = first_projection_display - projection_min - (member_span * 0.5)
|
|
|
|
|
|
forward_capacity = projection_max - (member_span * 0.5) - last_projection_display
|
|
|
|
|
|
if moving_side in {"before", "left"}:
|
|
|
|
|
|
extra = backward_capacity
|
|
|
|
|
|
elif moving_side in {"split", "both", "center"}:
|
|
|
|
|
|
extra = 2.0 * min(backward_capacity, forward_capacity)
|
|
|
|
|
|
else:
|
|
|
|
|
|
extra = forward_capacity
|
|
|
|
|
|
return max(current_display, current_display + max(0.0, extra))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _segment_scope_modes(
|
|
|
|
|
|
signature: Mapping[str, object],
|
|
|
|
|
|
instances: list[dict[str, object]],
|
|
|
|
|
|
segment_index: int,
|
|
|
|
|
|
current_backend: float,
|
|
|
|
|
|
current_display: float,
|
|
|
|
|
|
unit_scale: float,
|
|
|
|
|
|
*,
|
|
|
|
|
|
left_label: str,
|
|
|
|
|
|
right_label: str,
|
|
|
|
|
|
segment_label: str,
|
|
|
|
|
|
can_execute: bool,
|
|
|
|
|
|
) -> dict[str, dict[str, object]]:
|
|
|
|
|
|
modes: dict[str, dict[str, object]] = {}
|
|
|
|
|
|
for key, label, moving_side, semantics, description in (
|
|
|
|
|
|
(
|
|
|
|
|
|
"fix_left_move_right",
|
|
|
|
|
|
"固定前项,移动后侧",
|
|
|
|
|
|
"after",
|
|
|
|
|
|
"fix_left_move_right_group",
|
|
|
|
|
|
f"固定 {left_label},平移 {right_label} 及其右侧所有阵列成员,右侧已有间距保持不变。",
|
|
|
|
|
|
),
|
|
|
|
|
|
(
|
|
|
|
|
|
"fix_right_move_left",
|
|
|
|
|
|
"固定后项,移动前侧",
|
|
|
|
|
|
"before",
|
|
|
|
|
|
"fix_right_move_left_group",
|
|
|
|
|
|
f"固定 {right_label},平移 {left_label} 及其左侧所有阵列成员,左侧已有间距保持不变。",
|
|
|
|
|
|
),
|
|
|
|
|
|
(
|
|
|
|
|
|
"split_keep_center",
|
|
|
|
|
|
"两侧均分,中心不变",
|
|
|
|
|
|
"split",
|
|
|
|
|
|
"split_groups_keep_segment_center",
|
|
|
|
|
|
f"{left_label} 及左侧向前移动一半,{right_label} 及右侧向后移动一半,保持这段间距中心不变。",
|
|
|
|
|
|
),
|
|
|
|
|
|
):
|
|
|
|
|
|
max_display = _segment_max_spacing_display(
|
|
|
|
|
|
signature,
|
|
|
|
|
|
instances,
|
|
|
|
|
|
segment_index,
|
|
|
|
|
|
current_display,
|
|
|
|
|
|
unit_scale,
|
|
|
|
|
|
moving_side=moving_side,
|
|
|
|
|
|
)
|
|
|
|
|
|
mode_signature = _segment_signature(
|
|
|
|
|
|
signature,
|
|
|
|
|
|
segment_index,
|
|
|
|
|
|
current_backend,
|
|
|
|
|
|
unit_scale,
|
|
|
|
|
|
left_label=left_label,
|
|
|
|
|
|
right_label=right_label,
|
|
|
|
|
|
moving_side=moving_side,
|
|
|
|
|
|
motion_semantics=semantics,
|
|
|
|
|
|
max_display=max_display,
|
|
|
|
|
|
)
|
|
|
|
|
|
range_hint = f"{label}:{description} 对象段:{segment_label},沿阵列方向由 {left_label} 到 {right_label}。"
|
|
|
|
|
|
if max_display is not None and max_display > 0:
|
|
|
|
|
|
range_hint += f" 当前支撑面约允许该策略最大间距 {max_display:g}。"
|
|
|
|
|
|
modes[key] = {
|
|
|
|
|
|
"label": label,
|
|
|
|
|
|
"enabled": can_execute,
|
|
|
|
|
|
"enabled_tip": range_hint,
|
|
|
|
|
|
"disabled_tip": "" if can_execute else "SCDM 已识别该局部间距,但当前修改执行器尚未开放。",
|
|
|
|
|
|
"range_hint": range_hint,
|
|
|
|
|
|
"max_value": max_display,
|
|
|
|
|
|
"scdm_geometry_signature": mode_signature,
|
|
|
|
|
|
}
|
|
|
|
|
|
modes["move_single_right"] = {
|
|
|
|
|
|
"label": "只移动后项(未开放)",
|
|
|
|
|
|
"enabled": False,
|
|
|
|
|
|
"disabled_tip": (
|
|
|
|
|
|
f"只移动后项(未开放):只移动 {right_label} 会同时改变它和右侧下一个成员的间距,容易破坏阵列规律;"
|
|
|
|
|
|
"需要交互确认后再开放。"
|
|
|
|
|
|
),
|
|
|
|
|
|
"range_hint": f"只移动后项(未开放):该策略暂不执行。对象段:{segment_label}。",
|
|
|
|
|
|
"scdm_geometry_signature": _segment_signature(
|
|
|
|
|
|
signature,
|
|
|
|
|
|
segment_index,
|
|
|
|
|
|
current_backend,
|
|
|
|
|
|
unit_scale,
|
|
|
|
|
|
left_label=left_label,
|
|
|
|
|
|
right_label=right_label,
|
|
|
|
|
|
moving_side="single_right",
|
|
|
|
|
|
motion_semantics="move_only_right_instance_blocked",
|
|
|
|
|
|
),
|
|
|
|
|
|
}
|
|
|
|
|
|
return modes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _segment_signature(
|
|
|
|
|
|
signature: Mapping[str, object],
|
|
|
|
|
|
segment_index: int,
|
|
|
|
|
|
current_backend: float,
|
|
|
|
|
|
unit_scale: float,
|
|
|
|
|
|
*,
|
|
|
|
|
|
left_label: str,
|
|
|
|
|
|
right_label: str,
|
|
|
|
|
|
moving_side: str,
|
|
|
|
|
|
motion_semantics: str,
|
|
|
|
|
|
max_display: object = None,
|
|
|
|
|
|
) -> dict[str, object]:
|
|
|
|
|
|
result = dict(signature)
|
|
|
|
|
|
segment_fit = dict(result.get("supportPatternFit") if isinstance(result.get("supportPatternFit"), Mapping) else {})
|
|
|
|
|
|
max_number = _float_or_none(max_display)
|
|
|
|
|
|
if max_number is not None and max_number > 0:
|
|
|
|
|
|
segment_fit["maxSegmentSpacingLocal"] = max_number
|
|
|
|
|
|
segment_fit["maxSegmentSpacing"] = max_number * unit_scale if unit_scale > 0 else max_number
|
|
|
|
|
|
result["supportPatternFit"] = segment_fit
|
|
|
|
|
|
result["segmentIndex"] = segment_index
|
|
|
|
|
|
result["segmentLabel"] = f"{left_label}-{right_label}"
|
|
|
|
|
|
result["segmentLeftLabel"] = left_label
|
|
|
|
|
|
result["segmentRightLabel"] = right_label
|
|
|
|
|
|
result["segmentSpacing"] = current_backend
|
|
|
|
|
|
result["movingSide"] = moving_side
|
|
|
|
|
|
result["motionSemantics"] = motion_semantics
|
|
|
|
|
|
axis = _unit_vector(_float_values(signature.get("axis")))
|
|
|
|
|
|
instances = _sorted_pattern_instances(signature, axis) if len(axis) == 3 else []
|
|
|
|
|
|
if segment_index < len(instances) - 1:
|
|
|
|
|
|
result["segmentLeft"] = _segment_instance_reference(instances[segment_index], label=left_label)
|
|
|
|
|
|
result["segmentRight"] = _segment_instance_reference(instances[segment_index + 1], label=right_label)
|
|
|
|
|
|
result["localUnitScale"] = unit_scale
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _segment_instance_reference(item: Mapping[str, object], *, label: str = "") -> dict[str, object]:
|
|
|
|
|
|
source = item.get("source")
|
|
|
|
|
|
if not isinstance(source, Mapping):
|
|
|
|
|
|
return {}
|
|
|
|
|
|
return {
|
|
|
|
|
|
"displayLabel": label,
|
|
|
|
|
|
"sourceObjectId": source.get("sourceObjectId"),
|
|
|
|
|
|
"faceIds": _int_values(source.get("faceIds")),
|
|
|
|
|
|
"bodyIndex": _int_or_none(source.get("bodyIndex")),
|
|
|
|
|
|
"componentLocators": source.get("componentLocators") or source.get("bodyLocators") or [],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _segment_instance_label(item: Mapping[str, object], ordinal: int) -> str:
|
|
|
|
|
|
source = item.get("source")
|
|
|
|
|
|
if not isinstance(source, Mapping):
|
|
|
|
|
|
return f"成员{ordinal}"
|
|
|
|
|
|
instance_kind = str(source.get("instanceKind") or "").strip().lower()
|
|
|
|
|
|
if instance_kind in {"body", "part", "component"}:
|
|
|
|
|
|
local_solid_ids = sorted(set(_int_values(source.get("localSolidIds") or [source.get("localSolidId")])))
|
|
|
|
|
|
if local_solid_ids:
|
|
|
|
|
|
return f"Solid{local_solid_ids[0]}{'组' if len(local_solid_ids) > 1 else ''}"
|
|
|
|
|
|
local_part_ids = sorted(set(_int_values(source.get("localPartIds") or [source.get("localPartId")])))
|
|
|
|
|
|
if local_part_ids:
|
|
|
|
|
|
return f"Part{local_part_ids[0]}{'组' if len(local_part_ids) > 1 else ''}"
|
|
|
|
|
|
component_label = _component_locator_label(source.get("componentLocators") or source.get("bodyLocators"), include_index=False)
|
|
|
|
|
|
if component_label:
|
|
|
|
|
|
return component_label
|
|
|
|
|
|
body_index = _int_or_none(source.get("bodyIndex"))
|
|
|
|
|
|
if body_index is not None:
|
|
|
|
|
|
return f"零件{body_index}"
|
|
|
|
|
|
face_ids = sorted(set(_int_values(source.get("faceIds"))))
|
|
|
|
|
|
if face_ids:
|
|
|
|
|
|
return f"Face{face_ids[0]}{'组' if len(face_ids) > 1 else ''}"
|
|
|
|
|
|
component_label = _component_locator_label(source.get("componentLocators") or source.get("bodyLocators"))
|
|
|
|
|
|
if component_label:
|
|
|
|
|
|
return component_label
|
|
|
|
|
|
body_index = _int_or_none(source.get("bodyIndex"))
|
|
|
|
|
|
if body_index is not None:
|
|
|
|
|
|
return f"零件{body_index}"
|
|
|
|
|
|
source_id = str(source.get("sourceObjectId") or "").strip()
|
|
|
|
|
|
if source_id:
|
|
|
|
|
|
return source_id
|
|
|
|
|
|
return f"成员{ordinal}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _component_locator_label(value: object, *, include_index: bool = True) -> str:
|
|
|
|
|
|
if not isinstance(value, (list, tuple)):
|
|
|
|
|
|
return ""
|
|
|
|
|
|
for locator in value:
|
|
|
|
|
|
if not isinstance(locator, Mapping):
|
|
|
|
|
|
continue
|
|
|
|
|
|
for key in ("componentName", "name", "displayName"):
|
|
|
|
|
|
text = str(locator.get(key) or "").strip()
|
|
|
|
|
|
if text:
|
|
|
|
|
|
return text
|
|
|
|
|
|
if not include_index:
|
|
|
|
|
|
return ""
|
|
|
|
|
|
for locator in value:
|
|
|
|
|
|
if not isinstance(locator, Mapping):
|
|
|
|
|
|
continue
|
|
|
|
|
|
component_index = _int_or_none(locator.get("componentIndex"))
|
|
|
|
|
|
if component_index is not None:
|
|
|
|
|
|
return f"组件{component_index + 1}"
|
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _unit_vector(values: list[float]) -> list[float]:
|
|
|
|
|
|
if len(values) != 3:
|
|
|
|
|
|
return []
|
|
|
|
|
|
length = sum(item * item for item in values) ** 0.5
|
|
|
|
|
|
if length <= 1.0e-12:
|
|
|
|
|
|
return []
|
|
|
|
|
|
return [item / length for item in values]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _point_projection(point: list[float], axis: list[float]) -> float:
|
|
|
|
|
|
return sum(float(point[index]) * float(axis[index]) for index in range(3))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _float_or_none(value: object) -> float | None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
return float(str(value).strip())
|
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-19 10:28:09 +08:00
|
|
|
|
def _value_type(value_kind: str, key: str) -> str:
|
|
|
|
|
|
if value_kind == "vector3":
|
|
|
|
|
|
return "vector3"
|
|
|
|
|
|
if value_kind == "command":
|
|
|
|
|
|
return "command"
|
2026-08-19 18:02:47 +08:00
|
|
|
|
positive_suffixes = (".diameter", ".radius", ".width", ".depth", ".height", ".distance", ".thickness", ".spacing", ".segment_spacing")
|
|
|
|
|
|
if key.endswith(positive_suffixes):
|
2026-08-19 10:28:09 +08:00
|
|
|
|
return "positive"
|
|
|
|
|
|
return "number"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _capability_execution_ready(key: str, execution_ready: bool | Iterable[str]) -> bool:
|
|
|
|
|
|
if isinstance(execution_ready, bool):
|
|
|
|
|
|
return execution_ready
|
|
|
|
|
|
try:
|
|
|
|
|
|
return key in {str(item) for item in execution_ready}
|
|
|
|
|
|
except TypeError:
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _format_value(value: object, *, value_type: str) -> str:
|
|
|
|
|
|
if value is None:
|
|
|
|
|
|
return ""
|
|
|
|
|
|
if value_type == "vector3":
|
|
|
|
|
|
values = _float_values(value)
|
|
|
|
|
|
return f"({values[0]:g}, {values[1]:g}, {values[2]:g})" if len(values) == 3 else ""
|
|
|
|
|
|
if isinstance(value, float):
|
|
|
|
|
|
return f"{value:g}"
|
|
|
|
|
|
return str(value)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-19 18:02:47 +08:00
|
|
|
|
def _current_value_available(value: object, *, value_type: str) -> bool:
|
|
|
|
|
|
if value is None or value == "":
|
|
|
|
|
|
return False
|
|
|
|
|
|
if value_type == "vector3":
|
|
|
|
|
|
return len(_float_values(value)) == 3
|
|
|
|
|
|
if value_type in {"number", "positive"}:
|
|
|
|
|
|
try:
|
|
|
|
|
|
return float(str(value).strip()) > 0 if value_type == "positive" else True
|
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
|
return False
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-19 10:28:09 +08:00
|
|
|
|
def _float_values(value: object) -> list[float]:
|
|
|
|
|
|
if isinstance(value, (str, bytes)) or value is None:
|
|
|
|
|
|
return []
|
|
|
|
|
|
try:
|
|
|
|
|
|
values = list(value) # type: ignore[arg-type]
|
|
|
|
|
|
except TypeError:
|
|
|
|
|
|
return []
|
|
|
|
|
|
result: list[float] = []
|
|
|
|
|
|
for item in values[:3]:
|
|
|
|
|
|
try:
|
|
|
|
|
|
result.append(float(item))
|
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
|
return []
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _int_values(value: object) -> list[int]:
|
|
|
|
|
|
if isinstance(value, (str, bytes)) or value is None:
|
|
|
|
|
|
return []
|
|
|
|
|
|
try:
|
|
|
|
|
|
values = list(value) # type: ignore[arg-type]
|
|
|
|
|
|
except TypeError:
|
|
|
|
|
|
return []
|
|
|
|
|
|
result: list[int] = []
|
|
|
|
|
|
for item in values:
|
|
|
|
|
|
try:
|
|
|
|
|
|
result.append(int(item))
|
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
|
continue
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-19 18:02:47 +08:00
|
|
|
|
def _int_or_none(value: object) -> int | None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
return int(value)
|
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-19 10:28:09 +08:00
|
|
|
|
__all__ = ["property_specs_from_scdm_cache"]
|