989 lines
37 KiB
Python
989 lines
37 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Callable
|
|
|
|
from cadpy import cad_ref_syntax as syntax
|
|
from cadpy.reporting import (
|
|
EntryReportOptions,
|
|
entry_facts_payload,
|
|
entry_positioning_payload,
|
|
entry_report_payload,
|
|
entry_summary_payload,
|
|
major_planes_payload,
|
|
)
|
|
from cadpy.selector_types import SelectorProfile
|
|
from cadpy.step_targets import (
|
|
CadRefError,
|
|
ResolvedStepTarget,
|
|
cad_path_from_target,
|
|
cad_ref_error_payload,
|
|
entry_target_from_target,
|
|
resolve_step_target,
|
|
step_path_from_target,
|
|
)
|
|
from cadpy import analysis
|
|
from cadpy import lookup
|
|
|
|
REPO_ROOT = Path.cwd().resolve()
|
|
|
|
|
|
@dataclass
|
|
class EntryContext:
|
|
cad_path: str
|
|
kind: str
|
|
source_path: Path
|
|
step_path: Path | None
|
|
manifest: dict[str, object]
|
|
selector_index: lookup.SelectorIndex | None
|
|
|
|
|
|
@dataclass
|
|
class TargetSelection:
|
|
context: EntryContext
|
|
selector_type: str
|
|
row: dict[str, object]
|
|
normalized_selector: str
|
|
display_selector: str
|
|
copy_text: str
|
|
|
|
|
|
EntryContextProvider = Callable[[str, SelectorProfile], EntryContext | None]
|
|
|
|
|
|
def inspect_cad_refs(
|
|
entry_target: str,
|
|
refs_text: str = "",
|
|
*,
|
|
detail: bool = False,
|
|
include_topology: bool = False,
|
|
facts: bool = False,
|
|
positioning: bool = False,
|
|
planes: bool = False,
|
|
plane_coordinate_tolerance: float = 1e-3,
|
|
plane_min_area_ratio: float = 0.05,
|
|
plane_limit: int = 12,
|
|
context_provider: EntryContextProvider | None = None,
|
|
) -> dict[str, object]:
|
|
entry = entry_target_from_target(entry_target)
|
|
parsed_tokens = _parse_entry_ref_tokens(entry.cad_path, refs_text)
|
|
if not parsed_tokens:
|
|
raise CadRefError(
|
|
"No selector ref found. Expected refs like #o<path>, #o<path>.s<n>, #o<path>.f<n>, "
|
|
"#o<path>.e<n>, #o<path>.v<n>, #m<n>, or #s<n>/#f<n>/#e<n>/#v<n> "
|
|
"for single-occurrence entries."
|
|
)
|
|
|
|
contexts: dict[str, EntryContext] = {}
|
|
errors: list[dict[str, object]] = []
|
|
token_results: list[dict[str, object]] = []
|
|
refs_required_by_cad_path: dict[str, bool] = {}
|
|
report_options = EntryReportOptions(
|
|
facts=facts,
|
|
positioning=positioning,
|
|
planes=planes,
|
|
topology=include_topology,
|
|
plane_coordinate_tolerance=plane_coordinate_tolerance,
|
|
plane_min_area_ratio=plane_min_area_ratio,
|
|
plane_limit=plane_limit,
|
|
)
|
|
|
|
for parsed in parsed_tokens:
|
|
selectors = parsed.selectors or ()
|
|
refs_required_by_cad_path[parsed.cad_path] = (
|
|
refs_required_by_cad_path.get(parsed.cad_path, False)
|
|
or bool(selectors)
|
|
or report_options.refs_required
|
|
)
|
|
|
|
for parsed in parsed_tokens:
|
|
context = contexts.get(parsed.cad_path)
|
|
if context is None:
|
|
try:
|
|
context = _load_entry_context(
|
|
parsed.cad_path,
|
|
profile=SelectorProfile.REFS if refs_required_by_cad_path.get(parsed.cad_path) else SelectorProfile.SUMMARY,
|
|
context_provider=context_provider,
|
|
)
|
|
except CadRefError as exc:
|
|
error = cad_ref_error_payload(exc)
|
|
error.setdefault("line", parsed.line)
|
|
error.setdefault("cadPath", parsed.cad_path)
|
|
error.setdefault("selector", None)
|
|
error.setdefault("kind", "input")
|
|
errors.append(error)
|
|
token_results.append(
|
|
{
|
|
"line": parsed.line,
|
|
"token": parsed.token,
|
|
"cadPath": parsed.cad_path,
|
|
"stepPath": "",
|
|
"summary": {},
|
|
"selections": [],
|
|
"warnings": [],
|
|
}
|
|
)
|
|
continue
|
|
contexts[parsed.cad_path] = context
|
|
|
|
token_result: dict[str, object] = {
|
|
"line": parsed.line,
|
|
"token": parsed.token,
|
|
"cadPath": parsed.cad_path,
|
|
"stepPath": _relative_to_repo(context.step_path) if context.step_path is not None else "",
|
|
"stepHash": context.manifest.get("stepHash"),
|
|
"summary": _entry_summary(context),
|
|
"selections": [],
|
|
"warnings": [],
|
|
}
|
|
report_payload = entry_report_payload(
|
|
context.manifest,
|
|
kind=context.kind,
|
|
options=report_options,
|
|
selector_index=context.selector_index,
|
|
)
|
|
token_result["summary"] = report_payload["summary"]
|
|
if facts and "entryFacts" in report_payload:
|
|
token_result["entryFacts"] = report_payload["entryFacts"]
|
|
if planes and "planes" in report_payload:
|
|
token_result["planes"] = report_payload["planes"]
|
|
|
|
if parsed.selectors:
|
|
for raw_selector in parsed.selectors:
|
|
selection, selection_error = _inspect_selector(
|
|
parsed.cad_path,
|
|
raw_selector,
|
|
context,
|
|
detail=detail,
|
|
facts=facts,
|
|
positioning=positioning,
|
|
)
|
|
token_result["selections"].append(selection)
|
|
if selection_error is not None:
|
|
errors.append(
|
|
{
|
|
"line": parsed.line,
|
|
"cadPath": parsed.cad_path,
|
|
"selector": raw_selector,
|
|
**selection_error,
|
|
}
|
|
)
|
|
else:
|
|
if include_topology and "topology" in report_payload:
|
|
token_result["topology"] = report_payload["topology"]
|
|
if positioning and "entryPositioning" in report_payload:
|
|
token_result["entryPositioning"] = report_payload["entryPositioning"]
|
|
|
|
token_results.append(token_result)
|
|
|
|
return {
|
|
"ok": not errors,
|
|
"tokens": token_results,
|
|
"errors": errors,
|
|
}
|
|
|
|
|
|
def _parse_entry_ref_tokens(cad_path: str, refs_text: str = "") -> list[syntax.ParsedToken]:
|
|
text = str(refs_text or "").strip()
|
|
if not text:
|
|
return [
|
|
syntax.ParsedToken(
|
|
line=1,
|
|
token=syntax.build_cad_token(cad_path),
|
|
cad_path=cad_path,
|
|
selectors=(),
|
|
)
|
|
]
|
|
tokens: list[syntax.ParsedToken] = []
|
|
for line_no, line in enumerate(text.splitlines(), start=1):
|
|
normalized_line = line.strip()
|
|
if not normalized_line:
|
|
continue
|
|
parsed_tokens = syntax.parse_cad_tokens(normalized_line)
|
|
if len(parsed_tokens) != 1 or parsed_tokens[0].token.strip() != normalized_line:
|
|
raise CadRefError(f"Invalid selector ref {normalized_line!r}; expected #o1.2, #f1, #m1, or #o1.2.f1.")
|
|
parsed = parsed_tokens[0]
|
|
tokens.append(
|
|
syntax.ParsedToken(
|
|
line=line_no,
|
|
token=parsed.token,
|
|
cad_path=cad_path,
|
|
selectors=parsed.selectors,
|
|
)
|
|
)
|
|
return tokens
|
|
|
|
|
|
def _relative_to_repo(path: Path) -> str:
|
|
resolved = path.resolve()
|
|
try:
|
|
return resolved.relative_to(REPO_ROOT).as_posix()
|
|
except ValueError:
|
|
return resolved.as_posix()
|
|
|
|
|
|
def _load_entry_context(
|
|
cad_path: str,
|
|
*,
|
|
profile: SelectorProfile,
|
|
context_provider: EntryContextProvider | None = None,
|
|
) -> EntryContext:
|
|
if context_provider is not None:
|
|
context = context_provider(cad_path, profile)
|
|
if context is not None:
|
|
return context
|
|
|
|
target = resolve_step_target(cad_path)
|
|
return _load_step_context(target, profile=profile)
|
|
|
|
|
|
def _load_step_context(
|
|
target: ResolvedStepTarget,
|
|
*,
|
|
profile: SelectorProfile,
|
|
) -> EntryContext:
|
|
from cadpy.step_artifacts import ensure_step_topology_artifact
|
|
|
|
artifact = ensure_step_topology_artifact(
|
|
target,
|
|
require_selector=(profile != SelectorProfile.SUMMARY),
|
|
)
|
|
if artifact.selector_bundle is None:
|
|
manifest = artifact.manifest
|
|
selector_index = None
|
|
else:
|
|
manifest = artifact.selector_bundle.manifest
|
|
selector_index = lookup.build_selector_index(manifest, buffers=artifact.selector_bundle.buffers)
|
|
|
|
resolved_kind = _entry_kind_from_manifest(
|
|
manifest,
|
|
fallback=_entry_kind_from_manifest(artifact.manifest, fallback=target.kind),
|
|
)
|
|
return EntryContext(
|
|
cad_path=target.cad_path,
|
|
kind=resolved_kind,
|
|
source_path=target.source_path,
|
|
step_path=target.step_path,
|
|
manifest=manifest,
|
|
selector_index=selector_index,
|
|
)
|
|
|
|
|
|
def _entry_kind_from_manifest(manifest: dict[str, object], *, fallback: str) -> str:
|
|
entry_kind = str(manifest.get("entryKind") or "").strip().lower()
|
|
if entry_kind in {"part", "assembly"}:
|
|
return entry_kind
|
|
assembly = manifest.get("assembly")
|
|
if isinstance(assembly, dict):
|
|
return "assembly"
|
|
return fallback if fallback in {"part", "assembly"} else "part"
|
|
|
|
|
|
def _entry_summary(context: EntryContext) -> dict[str, object]:
|
|
return entry_summary_payload(
|
|
context.manifest,
|
|
kind=context.kind,
|
|
selector_index=context.selector_index,
|
|
)
|
|
|
|
|
|
def _selection_label(selector_type: str, display_selector: str) -> str:
|
|
noun = {
|
|
"occurrence": "Occurrence",
|
|
"shape": "Shape",
|
|
"face": "Face",
|
|
"edge": "Edge",
|
|
"vertex": "Corner",
|
|
}.get(selector_type, "Reference")
|
|
return f"{noun} {display_selector}"
|
|
|
|
|
|
def _selection_summary(selector_type: str, row: dict[str, object]) -> str:
|
|
if selector_type == "occurrence":
|
|
name = str(row.get("name") or row.get("sourceName") or "").strip()
|
|
return name or str(row.get("id") or "")
|
|
if selector_type == "shape":
|
|
kind = str(row.get("kind") or "shape")
|
|
volume = row.get("volume")
|
|
area = row.get("area")
|
|
if volume not in {None, ""}:
|
|
return f"{kind} volume={volume}"
|
|
if area not in {None, ""}:
|
|
return f"{kind} area={area}"
|
|
return kind
|
|
if selector_type == "face":
|
|
return f"{row.get('surfaceType')} area={row.get('area')}"
|
|
if selector_type == "edge":
|
|
return f"{row.get('curveType')} length={row.get('length')}"
|
|
return f"corner edges={row.get('edgeCount')}"
|
|
|
|
|
|
def _assembly_mate_rows(manifest: dict[str, object]) -> list[dict[str, object]]:
|
|
rows = manifest.get("assemblyMates")
|
|
if not isinstance(rows, list):
|
|
return []
|
|
return [dict(row) for row in rows if isinstance(row, dict)]
|
|
|
|
|
|
def _assembly_mate_by_selector(context: EntryContext, raw_selector: str) -> dict[str, object] | None:
|
|
normalized_selector = str(raw_selector or "").strip().replace("#", "", 1)
|
|
if not normalized_selector:
|
|
return None
|
|
for row in _assembly_mate_rows(context.manifest):
|
|
if str(row.get("id") or "").strip() == normalized_selector:
|
|
return row
|
|
return None
|
|
|
|
|
|
def _looks_like_assembly_mate_selector(raw_selector: str) -> bool:
|
|
selector = str(raw_selector or "").strip().replace("#", "", 1)
|
|
return len(selector) > 1 and selector[0] == "m" and selector[1:].isdigit()
|
|
|
|
|
|
def _assembly_mate_label(row: dict[str, object], selector: str) -> str:
|
|
return (
|
|
str(row.get("sourceLabel") or "").strip()
|
|
or str(row.get("name") or "").strip()
|
|
or str(row.get("label") or "").strip()
|
|
or selector
|
|
)
|
|
|
|
|
|
def _assembly_mate_summary(row: dict[str, object]) -> str:
|
|
relation = str(row.get("type") or row.get("relation") or "mate").strip() or "mate"
|
|
fixed = str(row.get("fixed") or "").strip()
|
|
moving = str(row.get("moving") or "").strip()
|
|
endpoints = f"{fixed} -> {moving}" if fixed and moving else fixed or moving
|
|
return " ".join(part for part in (relation, endpoints) if part)
|
|
|
|
|
|
def _assembly_mate_detail(row: dict[str, object]) -> dict[str, object]:
|
|
payload: dict[str, object] = {
|
|
"id": row.get("id"),
|
|
"label": row.get("label"),
|
|
"sourceLabel": row.get("sourceLabel"),
|
|
"type": row.get("type") or row.get("relation"),
|
|
"fixed": row.get("fixed"),
|
|
"moving": row.get("moving"),
|
|
}
|
|
for key in ("parameters", "fixedEndpoint", "movingEndpoint"):
|
|
if key in row:
|
|
payload[key] = row.get(key)
|
|
return payload
|
|
|
|
|
|
def _inspect_assembly_mate(
|
|
cad_path: str,
|
|
raw_selector: str,
|
|
context: EntryContext,
|
|
*,
|
|
detail: bool,
|
|
positioning: bool,
|
|
) -> tuple[dict[str, object], dict[str, object] | None]:
|
|
selector = str(raw_selector or "").strip().replace("#", "", 1)
|
|
row = _assembly_mate_by_selector(context, selector)
|
|
if row is None:
|
|
return (
|
|
{
|
|
"status": "error",
|
|
"selectorType": "mate" if _looks_like_assembly_mate_selector(selector) else "opaque",
|
|
"normalizedSelector": selector,
|
|
"displaySelector": selector,
|
|
},
|
|
{
|
|
"kind": "selector",
|
|
"message": f"Selector '{raw_selector}' did not resolve against {cad_path}.",
|
|
},
|
|
)
|
|
|
|
mate_id = str(row.get("id") or selector).strip() or selector
|
|
selection: dict[str, object] = {
|
|
"status": "resolved",
|
|
"selectorType": "mate",
|
|
"normalizedSelector": mate_id,
|
|
"displaySelector": mate_id,
|
|
"copyText": syntax.build_cad_token(cad_path, mate_id),
|
|
"label": f"Mate {_assembly_mate_label(row, mate_id)}",
|
|
"summary": _assembly_mate_summary(row),
|
|
}
|
|
if detail:
|
|
selection["detail"] = _assembly_mate_detail(row)
|
|
if positioning:
|
|
selection["positioning"] = {
|
|
"selectorType": "mate",
|
|
"selector": mate_id,
|
|
"fixedEndpoint": row.get("fixedEndpoint"),
|
|
"movingEndpoint": row.get("movingEndpoint"),
|
|
}
|
|
return selection, None
|
|
|
|
|
|
def _occurrence_detail(row: dict[str, object], selector_index: lookup.SelectorIndex) -> dict[str, object]:
|
|
occurrence_id = str(row.get("id") or "").strip()
|
|
child_rows = [
|
|
child
|
|
for child in selector_index.occurrences
|
|
if str(child.get("parentId") or "").strip() == occurrence_id
|
|
]
|
|
descendant_ids: list[str] = []
|
|
stack = list(reversed(child_rows))
|
|
while stack:
|
|
child = stack.pop()
|
|
child_id = str(child.get("id") or "").strip()
|
|
if child_id:
|
|
descendant_ids.append(lookup.display_selector(child_id, selector_index))
|
|
stack.extend(
|
|
grandchild
|
|
for grandchild in reversed(selector_index.occurrences)
|
|
if str(grandchild.get("parentId") or "").strip() == child_id
|
|
)
|
|
return {
|
|
"path": row.get("path"),
|
|
"name": row.get("name"),
|
|
"sourceName": row.get("sourceName"),
|
|
"parentId": lookup.display_selector(str(row.get("parentId") or ""), selector_index),
|
|
"childCount": len(child_rows),
|
|
"descendantOccurrenceIds": descendant_ids,
|
|
"transform": row.get("transform"),
|
|
"bbox": row.get("bbox"),
|
|
"shapeCount": row.get("shapeCount"),
|
|
"faceCount": row.get("faceCount"),
|
|
"edgeCount": row.get("edgeCount"),
|
|
"vertexCount": row.get("vertexCount"),
|
|
}
|
|
|
|
|
|
def _shape_detail(row: dict[str, object], selector_index: lookup.SelectorIndex) -> dict[str, object]:
|
|
return {
|
|
"occurrenceId": lookup.display_selector(str(row.get("occurrenceId") or ""), selector_index),
|
|
"kind": row.get("kind"),
|
|
"bbox": row.get("bbox"),
|
|
"center": row.get("center"),
|
|
"area": row.get("area"),
|
|
"volume": row.get("volume"),
|
|
"faceCount": row.get("faceCount"),
|
|
"edgeCount": row.get("edgeCount"),
|
|
"vertexCount": row.get("vertexCount"),
|
|
}
|
|
|
|
|
|
def _face_detail(row: dict[str, object], selector_index: lookup.SelectorIndex) -> dict[str, object]:
|
|
adjacent_edges = [
|
|
lookup.display_selector(selector, selector_index)
|
|
for selector in lookup.face_adjacent_edge_selectors(row, selector_index)
|
|
]
|
|
return {
|
|
"occurrenceId": lookup.display_selector(str(row.get("occurrenceId") or ""), selector_index),
|
|
"shapeId": lookup.display_selector(str(row.get("shapeId") or ""), selector_index),
|
|
"surfaceType": row.get("surfaceType"),
|
|
"area": row.get("area"),
|
|
"center": row.get("center"),
|
|
"normal": row.get("normal"),
|
|
"bbox": row.get("bbox"),
|
|
"params": row.get("params"),
|
|
"adjacentEdgeSelectors": adjacent_edges,
|
|
}
|
|
|
|
|
|
def _edge_detail(row: dict[str, object], selector_index: lookup.SelectorIndex) -> dict[str, object]:
|
|
adjacent_faces = [
|
|
lookup.display_selector(selector, selector_index)
|
|
for selector in lookup.edge_adjacent_face_selectors(row, selector_index)
|
|
]
|
|
adjacent_vertices = [
|
|
lookup.display_selector(selector, selector_index)
|
|
for selector in lookup.edge_adjacent_vertex_selectors(row, selector_index)
|
|
]
|
|
return {
|
|
"occurrenceId": lookup.display_selector(str(row.get("occurrenceId") or ""), selector_index),
|
|
"shapeId": lookup.display_selector(str(row.get("shapeId") or ""), selector_index),
|
|
"curveType": row.get("curveType"),
|
|
"length": row.get("length"),
|
|
"center": row.get("center"),
|
|
"bbox": row.get("bbox"),
|
|
"params": row.get("params"),
|
|
"adjacentFaceSelectors": adjacent_faces,
|
|
"adjacentVertexSelectors": adjacent_vertices,
|
|
}
|
|
|
|
|
|
def _vertex_detail(row: dict[str, object], selector_index: lookup.SelectorIndex) -> dict[str, object]:
|
|
adjacent_edges = [
|
|
lookup.display_selector(selector, selector_index)
|
|
for selector in lookup.vertex_adjacent_edge_selectors(row, selector_index)
|
|
]
|
|
adjacent_faces = [
|
|
lookup.display_selector(selector, selector_index)
|
|
for selector in lookup.vertex_adjacent_face_selectors(row, selector_index)
|
|
]
|
|
return {
|
|
"occurrenceId": lookup.display_selector(str(row.get("occurrenceId") or ""), selector_index),
|
|
"shapeId": lookup.display_selector(str(row.get("shapeId") or ""), selector_index),
|
|
"center": row.get("center"),
|
|
"bbox": row.get("bbox"),
|
|
"adjacentEdgeSelectors": adjacent_edges,
|
|
"adjacentFaceSelectors": adjacent_faces,
|
|
}
|
|
|
|
|
|
def _inspect_selector(
|
|
cad_path: str,
|
|
raw_selector: str,
|
|
context: EntryContext,
|
|
*,
|
|
detail: bool,
|
|
facts: bool,
|
|
positioning: bool,
|
|
) -> tuple[dict[str, object], dict[str, object] | None]:
|
|
parsed_selector = syntax.parse_selector(raw_selector)
|
|
if parsed_selector is None:
|
|
return (
|
|
{
|
|
"status": "error",
|
|
"selectorType": "unknown",
|
|
"normalizedSelector": raw_selector,
|
|
"displaySelector": raw_selector,
|
|
},
|
|
{
|
|
"kind": "selector",
|
|
"message": f"Unsupported selector '{raw_selector}'.",
|
|
},
|
|
)
|
|
|
|
if parsed_selector.selector_type == "opaque":
|
|
mate_selection, mate_error = _inspect_assembly_mate(
|
|
cad_path,
|
|
raw_selector,
|
|
context,
|
|
detail=detail,
|
|
positioning=positioning,
|
|
)
|
|
if mate_error is None or _looks_like_assembly_mate_selector(raw_selector):
|
|
return mate_selection, mate_error
|
|
|
|
if context.selector_index is None:
|
|
raise CadRefError(f"Selector index unavailable for {cad_path}")
|
|
|
|
lookup_result = lookup.lookup_selector(raw_selector, context.selector_index)
|
|
normalized_selector = lookup.canonicalize_selector(raw_selector, context.selector_index) or parsed_selector.canonical
|
|
display_selector = lookup.display_selector(normalized_selector, context.selector_index)
|
|
if lookup_result is None:
|
|
return (
|
|
{
|
|
"status": "error",
|
|
"selectorType": parsed_selector.selector_type,
|
|
"normalizedSelector": normalized_selector,
|
|
"displaySelector": display_selector,
|
|
},
|
|
{
|
|
"kind": "selector",
|
|
"message": f"Selector '{raw_selector}' did not resolve against {cad_path}.",
|
|
},
|
|
)
|
|
|
|
selector_type, row = lookup_result
|
|
selection: dict[str, object] = {
|
|
"status": "resolved",
|
|
"selectorType": selector_type,
|
|
"normalizedSelector": normalized_selector,
|
|
"displaySelector": display_selector,
|
|
"copyText": syntax.build_cad_token(cad_path, display_selector),
|
|
"label": _selection_label(selector_type, display_selector),
|
|
"summary": _selection_summary(selector_type, row),
|
|
}
|
|
if detail:
|
|
if selector_type == "occurrence":
|
|
selection["detail"] = _occurrence_detail(row, context.selector_index)
|
|
elif selector_type == "shape":
|
|
selection["detail"] = _shape_detail(row, context.selector_index)
|
|
elif selector_type == "face":
|
|
selection["detail"] = _face_detail(row, context.selector_index)
|
|
elif selector_type == "edge":
|
|
selection["detail"] = _edge_detail(row, context.selector_index)
|
|
elif selector_type == "vertex":
|
|
selection["detail"] = _vertex_detail(row, context.selector_index)
|
|
if facts:
|
|
selection["geometryFacts"] = analysis.geometry_facts_for_row(selector_type, row, context.selector_index)
|
|
if positioning:
|
|
selection["positioning"] = analysis.positioning_facts_for_row(selector_type, row, context.selector_index)
|
|
return selection, None
|
|
|
|
|
|
def _entry_facts(context: EntryContext) -> dict[str, object]:
|
|
return entry_facts_payload(context.manifest, kind=context.kind, selector_index=context.selector_index)
|
|
|
|
|
|
def _entry_positioning(context: EntryContext) -> dict[str, object]:
|
|
return entry_positioning_payload(context.manifest, kind=context.kind, selector_index=context.selector_index)
|
|
|
|
|
|
def load_entry_context_for_target(
|
|
target: str,
|
|
*,
|
|
profile: SelectorProfile = SelectorProfile.REFS,
|
|
context_provider: EntryContextProvider | None = None,
|
|
) -> EntryContext:
|
|
return _load_entry_context(cad_path_from_target(target), profile=profile, context_provider=context_provider)
|
|
|
|
|
|
def _parse_single_target_token(entry_target_text: str, selector: str = "") -> syntax.ParsedToken:
|
|
entry_target = entry_target_from_target(entry_target_text)
|
|
selector_text = str(selector or "").strip()
|
|
if not selector_text:
|
|
selectors: tuple[str, ...] = ()
|
|
else:
|
|
parsed_tokens = syntax.parse_cad_tokens(selector_text)
|
|
if len(parsed_tokens) != 1 or parsed_tokens[0].token.strip() != selector_text:
|
|
raise CadRefError(f"Expected exactly one selector ref such as #o1.2.f1; got {selector!r}.")
|
|
selectors = parsed_tokens[0].selectors
|
|
return syntax.ParsedToken(
|
|
line=1,
|
|
token=syntax.build_cad_token(entry_target.cad_path, ",".join(selectors)),
|
|
cad_path=entry_target.cad_path,
|
|
selectors=selectors,
|
|
)
|
|
|
|
|
|
def _default_root_occurrence(index: lookup.SelectorIndex) -> dict[str, object]:
|
|
roots = [
|
|
row
|
|
for row in index.occurrences
|
|
if row.get("parentId") in {None, ""}
|
|
]
|
|
if len(roots) == 1:
|
|
return roots[0]
|
|
if len(index.occurrences) == 1:
|
|
return index.occurrences[0]
|
|
raise CadRefError("Target has no selector and does not have exactly one root occurrence.")
|
|
|
|
|
|
def resolve_target_selection(
|
|
entry_target: str,
|
|
selector: str = "",
|
|
*,
|
|
default_root_occurrence: bool = False,
|
|
context_provider: EntryContextProvider | None = None,
|
|
) -> TargetSelection:
|
|
parsed = _parse_single_target_token(entry_target, selector)
|
|
context = _load_entry_context(parsed.cad_path, profile=SelectorProfile.REFS, context_provider=context_provider)
|
|
if context.selector_index is None:
|
|
raise CadRefError(f"Selector index unavailable for {parsed.cad_path}")
|
|
|
|
selectors = parsed.selectors
|
|
if not selectors:
|
|
if not default_root_occurrence:
|
|
raise CadRefError(f"Expected a selector ref for target {entry_target!r}.")
|
|
row = _default_root_occurrence(context.selector_index)
|
|
normalized_selector = str(row.get("id") or "")
|
|
display_selector = lookup.display_selector(normalized_selector, context.selector_index)
|
|
return TargetSelection(
|
|
context=context,
|
|
selector_type="occurrence",
|
|
row=row,
|
|
normalized_selector=normalized_selector,
|
|
display_selector=display_selector,
|
|
copy_text=syntax.build_cad_token(parsed.cad_path, display_selector),
|
|
)
|
|
if len(selectors) != 1:
|
|
raise CadRefError(f"Expected exactly one selector in target {entry_target!r}.")
|
|
|
|
raw_selector = selectors[0]
|
|
lookup_result = lookup.lookup_selector(raw_selector, context.selector_index)
|
|
if lookup_result is None:
|
|
raise CadRefError(f"Selector '{raw_selector}' did not resolve against {parsed.cad_path}.")
|
|
selector_type, row = lookup_result
|
|
normalized_selector = lookup.canonicalize_selector(raw_selector, context.selector_index) or raw_selector
|
|
display_selector = lookup.display_selector(normalized_selector, context.selector_index)
|
|
return TargetSelection(
|
|
context=context,
|
|
selector_type=selector_type,
|
|
row=row,
|
|
normalized_selector=normalized_selector,
|
|
display_selector=display_selector,
|
|
copy_text=syntax.build_cad_token(parsed.cad_path, display_selector),
|
|
)
|
|
|
|
|
|
def _selection_positioning_payload(selection: TargetSelection) -> dict[str, object]:
|
|
return analysis.positioning_facts_for_row(
|
|
selection.selector_type,
|
|
selection.row,
|
|
selection.context.selector_index,
|
|
)
|
|
|
|
|
|
def _selection_result_payload(selection: TargetSelection) -> dict[str, object]:
|
|
return {
|
|
"cadPath": selection.context.cad_path,
|
|
"stepPath": _relative_to_repo(selection.context.step_path) if selection.context.step_path is not None else "",
|
|
"selectorType": selection.selector_type,
|
|
"normalizedSelector": selection.normalized_selector,
|
|
"displaySelector": selection.display_selector,
|
|
"copyText": selection.copy_text,
|
|
}
|
|
|
|
|
|
def _axis_or_infer(axis: str | None, *positioning: dict[str, object]) -> str:
|
|
if axis is not None and str(axis).strip():
|
|
normalized = str(axis).strip().lower()
|
|
if normalized not in analysis.AXIS_INDEX:
|
|
raise CadRefError(f"Axis must be one of x, y, z; got {axis!r}.")
|
|
return normalized
|
|
inferred = analysis.infer_positioning_axis(*positioning)
|
|
if inferred is None:
|
|
raise CadRefError("Could not infer a shared axis; pass --axis x, --axis y, or --axis z.")
|
|
return inferred
|
|
|
|
|
|
def _primary_vector(positioning: dict[str, object]) -> object:
|
|
for key in ("normal", "direction", "axisVector"):
|
|
value = positioning.get(key)
|
|
if value is not None and value != "":
|
|
return value
|
|
return None
|
|
|
|
|
|
def inspect_target_frame(
|
|
entry_target: str,
|
|
selector: str = "",
|
|
*,
|
|
context_provider: EntryContextProvider | None = None,
|
|
) -> dict[str, object]:
|
|
selection = resolve_target_selection(entry_target, selector, default_root_occurrence=True, context_provider=context_provider)
|
|
if selection.selector_type == "occurrence":
|
|
occurrence_row = selection.row
|
|
else:
|
|
occurrence_id = str(selection.row.get("occurrenceId") or "")
|
|
occurrence_row = selection.context.selector_index.occurrence_by_id.get(occurrence_id) if selection.context.selector_index else None
|
|
if occurrence_row is None:
|
|
raise CadRefError(f"Target {entry_target!r} does not resolve to an occurrence-backed selector.")
|
|
frame = analysis.positioning_facts_for_row("occurrence", occurrence_row, selection.context.selector_index)
|
|
result = {
|
|
"ok": True,
|
|
**_selection_result_payload(selection),
|
|
"frame": frame,
|
|
}
|
|
if selection.selector_type != "occurrence":
|
|
result["selectionPositioning"] = _selection_positioning_payload(selection)
|
|
return result
|
|
|
|
|
|
def measure_targets(
|
|
entry_target: str,
|
|
from_selector: str,
|
|
to_selector: str,
|
|
*,
|
|
axis: str | None = None,
|
|
context_provider: EntryContextProvider | None = None,
|
|
) -> dict[str, object]:
|
|
from_selection = resolve_target_selection(entry_target, from_selector, default_root_occurrence=True, context_provider=context_provider)
|
|
to_selection = resolve_target_selection(entry_target, to_selector, default_root_occurrence=True, context_provider=context_provider)
|
|
from_positioning = _selection_positioning_payload(from_selection)
|
|
to_positioning = _selection_positioning_payload(to_selection)
|
|
resolved_axis = _axis_or_infer(axis, from_positioning, to_positioning)
|
|
from_coordinate = analysis.positioning_coordinate(from_positioning, resolved_axis)
|
|
to_coordinate = analysis.positioning_coordinate(to_positioning, resolved_axis)
|
|
if from_coordinate is None or to_coordinate is None:
|
|
raise CadRefError(f"Could not compute {resolved_axis}-axis coordinates for both targets.")
|
|
|
|
from_point = analysis.positioning_point(from_positioning)
|
|
to_point = analysis.positioning_point(to_positioning)
|
|
euclidean_distance = None
|
|
if from_point is not None and to_point is not None:
|
|
euclidean_distance = sum(
|
|
(float(to_point[index]) - float(from_point[index])) ** 2
|
|
for index in range(3)
|
|
) ** 0.5
|
|
|
|
vector_relationship = analysis.vector_relationship(
|
|
_primary_vector(from_positioning),
|
|
_primary_vector(to_positioning),
|
|
)
|
|
signed_distance = float(to_coordinate[0] - from_coordinate[0])
|
|
return {
|
|
"ok": True,
|
|
"axis": resolved_axis,
|
|
"from": {
|
|
**_selection_result_payload(from_selection),
|
|
"positioning": from_positioning,
|
|
"coordinate": from_coordinate[0],
|
|
"coordinateSource": from_coordinate[1],
|
|
},
|
|
"to": {
|
|
**_selection_result_payload(to_selection),
|
|
"positioning": to_positioning,
|
|
"coordinate": to_coordinate[0],
|
|
"coordinateSource": to_coordinate[1],
|
|
},
|
|
"measurement": {
|
|
"signedDistance": signed_distance,
|
|
"absoluteDistance": abs(signed_distance),
|
|
"euclideanDistance": euclidean_distance,
|
|
"vectorRelationship": vector_relationship,
|
|
},
|
|
}
|
|
|
|
|
|
def align_targets(
|
|
entry_target: str,
|
|
moving_selector: str,
|
|
target_selector: str,
|
|
*,
|
|
mode: str = "flush",
|
|
offset: float = 0.0,
|
|
axis: str | None = None,
|
|
context_provider: EntryContextProvider | None = None,
|
|
) -> dict[str, object]:
|
|
moving_selection = resolve_target_selection(
|
|
entry_target,
|
|
moving_selector,
|
|
default_root_occurrence=True,
|
|
context_provider=context_provider,
|
|
)
|
|
target_selection = resolve_target_selection(
|
|
entry_target,
|
|
target_selector,
|
|
default_root_occurrence=True,
|
|
context_provider=context_provider,
|
|
)
|
|
moving_positioning = _selection_positioning_payload(moving_selection)
|
|
target_positioning = _selection_positioning_payload(target_selection)
|
|
normalized_mode = str(mode or "flush").strip().lower()
|
|
if normalized_mode not in {"flush", "center"}:
|
|
raise CadRefError(f"Unsupported alignment mode {mode!r}; expected 'flush' or 'center'.")
|
|
|
|
if normalized_mode == "center":
|
|
moving_point = analysis.positioning_point(moving_positioning)
|
|
target_point = analysis.positioning_point(target_positioning)
|
|
if moving_point is None or target_point is None:
|
|
raise CadRefError("Center alignment requires both targets to expose a point, center, origin, or translation.")
|
|
translation_vector = [float(target_point[index]) - float(moving_point[index]) for index in range(3)]
|
|
resolved_axis = None
|
|
if axis is not None and str(axis).strip():
|
|
resolved_axis = _axis_or_infer(axis)
|
|
axis_index = analysis.AXIS_INDEX[resolved_axis]
|
|
translation_vector = [
|
|
value if index == axis_index else 0.0
|
|
for index, value in enumerate(translation_vector)
|
|
]
|
|
else:
|
|
resolved_axis = _axis_or_infer(axis, moving_positioning, target_positioning)
|
|
moving_coordinate = analysis.positioning_coordinate(moving_positioning, resolved_axis)
|
|
target_coordinate = analysis.positioning_coordinate(target_positioning, resolved_axis)
|
|
if moving_coordinate is None or target_coordinate is None:
|
|
raise CadRefError(f"Could not compute {resolved_axis}-axis coordinates for both alignment targets.")
|
|
offset_sign = 1
|
|
target_alignment = target_positioning.get("axisAlignment")
|
|
if isinstance(target_alignment, dict) and target_alignment.get("axis") == resolved_axis:
|
|
offset_sign = int(target_alignment.get("sign") or 1)
|
|
distance = float(target_coordinate[0] + (float(offset) * offset_sign) - moving_coordinate[0])
|
|
translation_vector = [0.0, 0.0, 0.0]
|
|
translation_vector[analysis.AXIS_INDEX[resolved_axis]] = distance
|
|
|
|
vector_relationship = analysis.vector_relationship(
|
|
_primary_vector(moving_positioning),
|
|
_primary_vector(target_positioning),
|
|
)
|
|
rotation_required = None
|
|
if vector_relationship is not None:
|
|
rotation_required = vector_relationship.get("relation") != "opposed"
|
|
|
|
return {
|
|
"ok": True,
|
|
"mode": normalized_mode,
|
|
"axis": resolved_axis,
|
|
"offset": float(offset),
|
|
"moving": {
|
|
**_selection_result_payload(moving_selection),
|
|
"positioning": moving_positioning,
|
|
},
|
|
"target": {
|
|
**_selection_result_payload(target_selection),
|
|
"positioning": target_positioning,
|
|
},
|
|
"alignment": {
|
|
"translationVector": translation_vector,
|
|
"transformTranslationDelta": {
|
|
"3": translation_vector[0],
|
|
"7": translation_vector[1],
|
|
"11": translation_vector[2],
|
|
},
|
|
"vectorRelationship": vector_relationship,
|
|
"rotationRequired": rotation_required,
|
|
},
|
|
}
|
|
|
|
|
|
def diff_entry_targets(
|
|
left_target: str,
|
|
right_target: str,
|
|
*,
|
|
planes: bool = False,
|
|
plane_coordinate_tolerance: float = 1e-3,
|
|
plane_min_area_ratio: float = 0.05,
|
|
plane_limit: int = 12,
|
|
context_provider: EntryContextProvider | None = None,
|
|
) -> dict[str, object]:
|
|
left_context = load_entry_context_for_target(
|
|
left_target,
|
|
profile=SelectorProfile.REFS,
|
|
context_provider=context_provider,
|
|
)
|
|
right_context = load_entry_context_for_target(
|
|
right_target,
|
|
profile=SelectorProfile.REFS,
|
|
context_provider=context_provider,
|
|
)
|
|
|
|
diff_payload = analysis.selector_manifest_diff(left_context.manifest, right_context.manifest)
|
|
bbox_left = left_context.manifest.get("bbox")
|
|
bbox_right = right_context.manifest.get("bbox")
|
|
size_left = analysis.bbox_size(bbox_left)
|
|
size_right = analysis.bbox_size(bbox_right)
|
|
center_left = analysis.bbox_center(bbox_left)
|
|
center_right = analysis.bbox_center(bbox_right)
|
|
|
|
result: dict[str, object] = {
|
|
"ok": True,
|
|
"left": {
|
|
"cadPath": left_context.cad_path,
|
|
"kind": left_context.kind,
|
|
"stepPath": _relative_to_repo(left_context.step_path) if left_context.step_path is not None else "",
|
|
"summary": _entry_summary(left_context),
|
|
"entryFacts": _entry_facts(left_context),
|
|
},
|
|
"right": {
|
|
"cadPath": right_context.cad_path,
|
|
"kind": right_context.kind,
|
|
"stepPath": _relative_to_repo(right_context.step_path) if right_context.step_path is not None else "",
|
|
"summary": _entry_summary(right_context),
|
|
"entryFacts": _entry_facts(right_context),
|
|
},
|
|
"diff": {
|
|
"kindChanged": left_context.kind != right_context.kind,
|
|
**diff_payload,
|
|
"sizeDelta": (
|
|
[float(size_right[index] - size_left[index]) for index in range(3)]
|
|
if size_left is not None and size_right is not None
|
|
else None
|
|
),
|
|
"centerDelta": (
|
|
[float(center_right[index] - center_left[index]) for index in range(3)]
|
|
if center_left is not None and center_right is not None
|
|
else None
|
|
),
|
|
},
|
|
}
|
|
|
|
if planes and left_context.selector_index is not None and right_context.selector_index is not None:
|
|
report_options = EntryReportOptions(
|
|
planes=True,
|
|
plane_coordinate_tolerance=plane_coordinate_tolerance,
|
|
plane_min_area_ratio=plane_min_area_ratio,
|
|
plane_limit=plane_limit,
|
|
)
|
|
result["diff"]["leftMajorPlanes"] = major_planes_payload(left_context.selector_index, report_options)
|
|
result["diff"]["rightMajorPlanes"] = major_planes_payload(right_context.selector_index, report_options)
|
|
return result
|