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

820 lines
33 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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] = (),
selected_solid_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}
solid_ids = {int(item) for item in selected_solid_ids}
if not face_ids and not edge_ids and not solid_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,
solid_ids=solid_ids,
):
continue
capabilities = item.get("capabilities")
if not isinstance(capabilities, list):
continue
for capability in capabilities:
if isinstance(capability, Mapping):
if str(capability.get("key") or "") in {"pattern.segment_spacing", "pattern.instance_position"}:
continue
spec = _capability_spec(item, capability, execution_ready=execution_ready)
if spec is not None:
specs.append(spec)
specs.extend(
_pattern_instance_position_specs(
item,
selected_face_ids=face_ids,
selected_solid_ids=solid_ids,
execution_ready=execution_ready,
)
)
specs.extend(_pattern_segment_spacing_specs(item, execution_ready=execution_ready))
return specs
def _object_matches(
raw_object: Mapping[str, object],
*,
face_ids: set[int],
edge_ids: set[int],
solid_ids: set[int],
) -> bool:
signature = raw_object.get("geometrySignature")
if not isinstance(signature, Mapping):
return False
object_faces = set(_int_values(signature.get("faceIds")))
object_faces.update(_int_values(signature.get("supportFaceIds")))
object_edges = set(_int_values(signature.get("edgeIds")))
if (face_ids and object_faces & face_ids) or (edge_ids and object_edges & edge_ids):
return True
if not solid_ids:
return False
object_type = str(raw_object.get("objectType") or "").strip().lower()
if object_type not in {"pattern", "linear_pattern"}:
return False
return bool(_pattern_local_solid_ids(signature) & solid_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)
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)
command_value = value_type == "command"
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)
capability_block = str(capability.get("blockReason") or "").strip()
object_block = str(raw_object.get("blockReason") or "").strip()
block_reason = capability_block or object_block
if not command_value and not _current_value_available(current_display, value_type=value_type):
block_reason = block_reason or f"SCDM 已识别“{label}”,但没有返回可用于编辑的当前值。"
backend_operation = str(capability.get("backendOperation") or "")
post_check = str(capability.get("postCheck") or "")
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};超过后会跑出承载面。"
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,
"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,
"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,
"range_hint": range_hint,
"min_value": 0.0 if value_type == "positive" else None,
"min_exclusive": True if value_type == "positive" else False,
"max_value": max_value,
"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,
"scdm_geometry_signature": signature if isinstance(signature, Mapping) else {},
}
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):
# UI 上展示的是相邻实例之间的“段间距”,不是整列统一 spacing。
# 每一段都带自己的移动语义和安全范围,避免“第 1-2 间距”改成整列平移。
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 _pattern_instance_position_specs(
raw_object: Mapping[str, object],
*,
selected_face_ids: set[int],
selected_solid_ids: set[int],
execution_ready: bool | Iterable[str],
) -> list[dict[str, object]]:
if str(raw_object.get("objectType") or "").strip().lower() not in {"pattern", "linear_pattern"}:
return []
signature = raw_object.get("geometrySignature")
if not isinstance(signature, Mapping):
return []
unit_scale = _unit_scale(signature)
can_execute = bool(_capability_execution_ready("pattern.instance_position", execution_ready) and not str(raw_object.get("blockReason") or "").strip())
specs: list[dict[str, object]] = []
instances = _pattern_instances_in_original_order(signature)
for ordinal, instance in enumerate(instances, start=1):
if selected_face_ids and not (set(_int_values(instance.get("faceIds"))) & selected_face_ids):
continue
if selected_solid_ids and not (_instance_local_solid_ids(instance) & selected_solid_ids):
continue
center = _float_values(instance.get("center") or instance.get("instanceCenter"))
if len(center) != 3:
continue
label = _segment_instance_label({"source": instance}, ordinal)
current_display = [value / unit_scale for value in center] if unit_scale > 0 else list(center)
instance_signature = _pattern_instance_signature(signature, instance, unit_scale=unit_scale, label=label)
locatable = _pattern_instance_has_locator(instance_signature)
enabled = bool(can_execute and locatable)
disabled_tip = ""
if not can_execute:
disabled_tip = "SCDM 已识别该阵列实例,但当前修改执行器尚未开放。"
elif not locatable:
disabled_tip = "SCDM 已识别该阵列实例,但缓存里没有可定位的 Face / Body / Component,不能稳定移动。"
range_hint = f"移动阵列实例:只平移 {label},不自动保持整体阵列等距;需要保持间距时请使用“阵列间距”或“局部间距”。"
specs.append(
{
"key": f"scdm:pattern.instance_position:{ordinal - 1}",
"label": f"{label}位置",
"current_raw": current_display,
"scdm_current_raw": center,
"scdm_unit_scale": unit_scale,
"current_text": _format_value(current_display, value_type="vector3"),
"target_text": _format_value(current_display, value_type="vector3"),
"editable": True,
"enabled": enabled,
"status_text": "可修改" if enabled else "暂未接入",
"scope_text": "只移动该实例",
"action": "apply_scdm_property_edit",
"value_type": "vector3",
"enabled_tip": range_hint if enabled else "",
"disabled_tip": disabled_tip,
"range_hint": range_hint,
"min_value": None,
"min_exclusive": False,
"max_value": None,
"scdm_object_id": raw_object.get("objectId"),
"scdm_source_backend_id": raw_object.get("sourceBackendId"),
"scdm_capability_key": "pattern.instance_position",
"scdm_backend_operation": "move_pattern_instance",
"scdm_post_check": "target_pattern_instance_center",
"scdm_geometry_signature": instance_signature,
}
)
return specs
def _pattern_instances_in_original_order(signature: Mapping[str, object]) -> list[Mapping[str, object]]:
value = signature.get("patternInstances")
if not isinstance(value, (list, tuple)):
return []
return [item for item in value if isinstance(item, Mapping)]
def _pattern_instance_signature(
signature: Mapping[str, object],
instance: Mapping[str, object],
*,
unit_scale: float,
label: str,
) -> dict[str, object]:
result = dict(instance)
result["objectType"] = "pattern_instance"
result["displayLabel"] = label
result["patternObjectType"] = signature.get("objectType")
result["patternKind"] = signature.get("patternKind")
result["instanceKind"] = instance.get("instanceKind") or signature.get("instanceKind")
result["axis"] = signature.get("axis")
result["localUnitScale"] = unit_scale
center = _float_values(instance.get("center") or instance.get("instanceCenter"))
if len(center) == 3:
result["center"] = center
result["instanceCenter"] = center
return result
def _pattern_instance_has_locator(signature: Mapping[str, object]) -> bool:
if signature.get("componentLocators") or signature.get("bodyLocators") or signature.get("scdmFaceLocators"):
return True
if _int_values(signature.get("faceOrdinals")) or _int_values(signature.get("globalFaceOrdinals")):
return True
if _int_or_none(signature.get("faceOrdinal")) is not None or _int_or_none(signature.get("globalFaceOrdinal")) is not None:
return True
instance_kind = str(signature.get("instanceKind") or "").strip().lower()
return instance_kind in {"body", "part", "component"} and _int_or_none(signature.get("bodyIndex")) is not None
def _pattern_local_solid_ids(signature: Mapping[str, object]) -> set[int]:
ids = set(_int_values(signature.get("localSolidIds")))
local_solid = _int_or_none(signature.get("localSolidId"))
if local_solid is not None:
ids.add(local_solid)
ids.update(_int_values(signature.get("bodyIndices")))
body_index = _int_or_none(signature.get("bodyIndex"))
if body_index is not None:
ids.add(body_index)
for instance in _pattern_instances_in_original_order(signature):
ids.update(_instance_local_solid_ids(instance))
return ids
def _instance_local_solid_ids(instance: Mapping[str, object]) -> set[int]:
ids = set(_int_values(instance.get("localSolidIds")))
local_solid = _int_or_none(instance.get("localSolidId"))
if local_solid is not None:
ids.add(local_solid)
body_index = _int_or_none(instance.get("bodyIndex"))
if body_index is not None:
ids.add(body_index)
return ids
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),
}
)
# 用阵列轴投影排序,比原始 cache 顺序更接近用户看到的左到右/前到后顺序。
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", "single_left", "only_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]] = {}
# 同一个“间距”参数有多种建模意图:固定哪一侧、是否保持中心。
# 这些模式会直接进入 scdm_edit_job,不能只作为 UI 文案存在。
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,
}
for key, label, moving_side, semantics, moved_label, neighbor_warning in (
(
"move_single_left",
"只移动前项",
"single_left",
"move_only_left_instance",
left_label,
"会改变它与左侧相邻成员的距离",
),
(
"move_single_right",
"只移动后项",
"single_right",
"move_only_right_instance",
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}:只平移 {moved_label},把 {left_label}-{right_label} 这段调到目标间距;"
f"{neighbor_warning},不用于保持整列等距。对象段:{segment_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 if can_execute else "",
"disabled_tip": "" if can_execute else "SCDM 已识别该局部间距,但当前修改执行器尚未开放。",
"range_hint": range_hint,
"max_value": max_display,
"scdm_geometry_signature": mode_signature,
}
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"Solid{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"Solid{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
def _value_type(value_kind: str, key: str) -> str:
if value_kind == "vector3":
return "vector3"
if value_kind == "command":
return "command"
positive_suffixes = (".diameter", ".radius", ".width", ".depth", ".height", ".distance", ".thickness", ".spacing", ".segment_spacing")
if key.endswith(positive_suffixes):
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)
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
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
def _int_or_none(value: object) -> int | None:
try:
return int(value)
except (TypeError, ValueError):
return None
__all__ = ["property_specs_from_scdm_cache"]