feat: 完善 SCDM-first 参数化编辑交付版
This commit is contained in:
@@ -8,11 +8,13 @@ def property_specs_from_scdm_cache(
|
||||
*,
|
||||
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}
|
||||
if not face_ids and not 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):
|
||||
@@ -20,30 +22,56 @@ def property_specs_from_scdm_cache(
|
||||
|
||||
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):
|
||||
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 "") == "pattern.segment_spacing":
|
||||
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]) -> bool:
|
||||
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")))
|
||||
return bool((face_ids and object_faces & face_ids) or (edge_ids and object_edges & edge_ids))
|
||||
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(
|
||||
@@ -197,6 +225,8 @@ def _pattern_segment_spacing_specs(
|
||||
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)
|
||||
@@ -268,6 +298,138 @@ def _pattern_segment_spacing_specs(
|
||||
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)):
|
||||
@@ -287,6 +449,7 @@ def _sorted_pattern_instances(signature: Mapping[str, object], axis: list[float]
|
||||
"projection": _point_projection(center, axis),
|
||||
}
|
||||
)
|
||||
# 用阵列轴投影排序,比原始 cache 顺序更接近用户看到的左到右/前到后顺序。
|
||||
result.sort(key=lambda item: float(item["projection"]))
|
||||
return result
|
||||
|
||||
@@ -314,7 +477,7 @@ def _segment_max_spacing_display(
|
||||
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"}:
|
||||
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)
|
||||
@@ -337,6 +500,8 @@ def _segment_scope_modes(
|
||||
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",
|
||||
@@ -391,25 +556,58 @@ def _segment_scope_modes(
|
||||
"max_value": max_display,
|
||||
"scdm_geometry_signature": mode_signature,
|
||||
}
|
||||
modes["move_single_right"] = {
|
||||
"label": "只移动后项(未开放)",
|
||||
"enabled": False,
|
||||
"disabled_tip": (
|
||||
f"只移动后项(未开放):只移动 {right_label} 会同时改变它和右侧下一个成员的间距,容易破坏阵列规律;"
|
||||
"需要交互确认后再开放。"
|
||||
for key, label, moving_side, semantics, moved_label, neighbor_warning in (
|
||||
(
|
||||
"move_single_left",
|
||||
"只移动前项",
|
||||
"single_left",
|
||||
"move_only_left_instance",
|
||||
left_label,
|
||||
"会改变它与左侧相邻成员的距离",
|
||||
),
|
||||
"range_hint": f"只移动后项(未开放):该策略暂不执行。对象段:{segment_label}。",
|
||||
"scdm_geometry_signature": _segment_signature(
|
||||
(
|
||||
"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="single_right",
|
||||
motion_semantics="move_only_right_instance_blocked",
|
||||
),
|
||||
}
|
||||
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
|
||||
|
||||
|
||||
@@ -464,7 +662,7 @@ def _segment_instance_reference(item: Mapping[str, object], *, label: str = "")
|
||||
def _segment_instance_label(item: Mapping[str, object], ordinal: int) -> str:
|
||||
source = item.get("source")
|
||||
if not isinstance(source, Mapping):
|
||||
return f"成员{ordinal}"
|
||||
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")])))
|
||||
@@ -478,7 +676,7 @@ def _segment_instance_label(item: Mapping[str, object], ordinal: int) -> str:
|
||||
return component_label
|
||||
body_index = _int_or_none(source.get("bodyIndex"))
|
||||
if body_index is not None:
|
||||
return f"零件{body_index}"
|
||||
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 ''}"
|
||||
@@ -487,11 +685,11 @@ def _segment_instance_label(item: Mapping[str, object], ordinal: int) -> str:
|
||||
return component_label
|
||||
body_index = _int_or_none(source.get("bodyIndex"))
|
||||
if body_index is not None:
|
||||
return f"零件{body_index}"
|
||||
return f"Solid{body_index}"
|
||||
source_id = str(source.get("sourceObjectId") or "").strip()
|
||||
if source_id:
|
||||
return source_id
|
||||
return f"成员{ordinal}"
|
||||
return f"阵列成员{ordinal}"
|
||||
|
||||
|
||||
def _component_locator_label(value: object, *, include_index: bool = True) -> str:
|
||||
|
||||
Reference in New Issue
Block a user